├── .editorconfig ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── ctutlz ├── __init__.py ├── _version.py ├── ctlog.py ├── log_list_schema.json ├── really_all_logs.json ├── really_all_logs.md ├── rfc6962.py ├── scripts │ ├── __init__.py │ ├── ctloglist.py │ ├── decompose_cert.py │ └── verify_scts.py ├── sct │ ├── __init__.py │ ├── ee_cert.py │ ├── signature_input.py │ └── verification.py ├── tls │ ├── __init__.py │ ├── handshake.py │ ├── handshake_openssl_build.py │ └── sctlist.py └── utils │ ├── __init__.py │ ├── encoding.py │ ├── logger.py │ ├── string.py │ └── tdf_bytes.py ├── fabfile.py ├── requirements-all.txt ├── setup.py ├── tests ├── __init__.py ├── data │ ├── test_ctlog │ │ ├── all_logs_list_2018-03-03.json │ │ ├── known-logs-2017-08-01.html │ │ ├── known-logs_2017-08-24.html │ │ ├── known-logs_2018-02-27.html │ │ ├── known-logs_2018-03-03.html │ │ └── log_list_2018-03-03.json │ ├── test_decompose_cert │ │ ├── cert.b64 │ │ ├── cert.der │ │ └── cert.pem │ ├── test_sct_ee_cert │ │ ├── cert_no_ev.der │ │ ├── ev_cert.der │ │ ├── issued_by_letsencrypt.der │ │ ├── issued_by_letsencrypt_2.der │ │ └── issued_by_letsencrypt_not.der │ └── test_sct_verify_signature │ │ ├── google.com │ │ ├── pubkey.pem │ │ ├── signature.der │ │ └── signature_input.bin │ │ ├── pubkey.pem │ │ ├── pubkey_possl.pem │ │ ├── signature.der │ │ └── signature_input_valid.bin ├── test_ctlog.py ├── test_decompose_cert.py ├── test_rfc6962.py ├── test_sct_ee_cert.py ├── test_sct_verify_signature.py ├── test_utils_encoding.py └── test_utils_tdf_bytes.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | tab_width = 8 8 | indent_size = 2 9 | indent_style = space 10 | 11 | [*.[ch]] 12 | indent_size = 4 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [*.py] 18 | charset = utf-8 19 | 20 | [*.py] 21 | indent_style = space 22 | indent_size = 4 23 | 24 | [.travis.yml] 25 | indent_style = space 26 | indent_size = 2 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.c 9 | *.o 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | # cf. https://stackoverflow.com/a/37841046 29 | README 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | install: 8 | - pip install tox-travis 9 | script: 10 | - tox 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # podman build --tag ctutlz . && \ 2 | # podman run -it --rm --name ctutlz --volume ./:/ctutlz ctutlz:latest \ 3 | # verify-scts www.google.com 4 | 5 | FROM ubuntu:16.04 6 | 7 | RUN apt-get update && apt-get --yes dist-upgrade && apt-get --yes install \ 8 | gcc \ 9 | make \ 10 | openssl \ 11 | python3 \ 12 | python3-cryptography \ 13 | python3-pip \ 14 | python3-cffi \ 15 | libffi-dev \ 16 | libssl-dev \ 17 | 18 | # for development 19 | fabric python-pip wget 20 | 21 | # for development 22 | RUN pip2 install fabsetup==0.9.0 23 | 24 | WORKDIR /ctutlz 25 | 26 | ADD . . 27 | 28 | RUN pip3 install cffi==1.11.5 && \ 29 | pip3 install -r requirements-all.txt 30 | 31 | CMD /bin/bash 32 | # devel-command: 33 | # `pip3 install -e . && python3 ctutlz/scripts/verify_scts.py www.google.com` 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2018 Theodor Nolte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ---> __[CT Deployment Study](https://theno.github.io/presi-ct-deployment)__ <--- 2 | 3 | ---- 4 | 5 | # ctutlz 6 | 7 | Python utils library and tools for 8 | [Certificate Transparency](https://www.certificate-transparency.org/). 9 | 10 | [![Build Status](https://travis-ci.org/theno/ctutlz.svg?branch=master)](https://travis-ci.org/theno/ctutlz) 11 | [![Supported Python Versions](https://img.shields.io/pypi/pyversions/ctutlz.svg)](https://pypi.python.org/pypi/ctutlz) 12 | [![PyPI Version](https://img.shields.io/pypi/v/ctutlz.svg)](https://pypi.python.org/pypi/ctutlz) 13 | 14 | This is the first implementation in Python which scrapes the SCTs at the TLS 15 | handshake by [certificate extension][1], by [TLS extension][2], and 16 | by [OCSP stapling][3] directly using the OpenSSL C-API (without forking 17 | subprocesses to call any OpenSSL commands). 18 | 19 | [1]: https://www.certificate-transparency.org/how-ct-works#TOC-X.509v3-Extension 20 | [2]: https://www.certificate-transparency.org/how-ct-works#TOC-TLS-Extension 21 | [3]: https://www.certificate-transparency.org/how-ct-works#TOC-OCSP-Stapling 22 | 23 | ---- 24 | * [Usage](#usage) 25 | * [verify-scts](#verify-scts) 26 | * [ctloglist](#ctloglist) 27 | * [decompose-cert](#decompose-cert) 28 | * [API](#api) 29 | * [Installation](#installation) 30 | * [Installation and Development 2022-10](#installation-and-development-2022-10) 31 | * [Development](#development) 32 | * [Fabfile](#fabfile) 33 | * [Devel-Commands](#devel-commands) 34 | ---- 35 | 36 | ## Usage 37 | 38 | ### verify-scts 39 | 40 | ``` 41 | > verify-scts --help 42 | 43 | usage: verify-scts [-h] [--short | --debug] 44 | [--cert-only | --tls-only | --ocsp-only] 45 | [--log-list | --latest-logs] 46 | hostname [hostname ...] 47 | 48 | Verify Signed Certificate Timestamps (SCTs) delivered from one or several 49 | hosts by X.509v3 extension, TLS extension, or OCSP stapling 50 | 51 | positional arguments: 52 | hostname host name of the server (example: 'ritter.vg') 53 | 54 | optional arguments: 55 | -h, --help show this help message and exit 56 | --short show short results and warnings/errors only 57 | --debug show more for diagnostic purposes 58 | --cert-only only verify SCTs included in the certificate 59 | --tls-only only verify SCTs gathered from TLS handshake 60 | --ocsp-only only verify SCTs gathered via OCSP request 61 | --log-list 62 | filename of a log list in JSON format 63 | --latest-logs for SCT verification against known CT Logs (compliant 64 | with Chrome's CT policy) download latest version of 65 | https://www.gstatic.com/ 66 | ct/log_list/v2/all_logs_list.json -- use built-in log 67 | list really_all_logs.json from 2020-04-05 if --latest- 68 | logs or --log-list are not set 69 | 70 | 71 | ``` 72 | 73 | #### Examples: 74 | ##### Simple google.com verification 75 | 76 | > verify-scts google.com --short 77 | 78 | # google.com 79 | 80 | * no EV cert 81 | * not issued by Let's Encrypt 82 | 83 | ## SCTs by Certificate 84 | 85 | ``` 86 | LogID b64 : sh4FzIuizYogTodm+Su5iiUgZ2va+nDnsklTLe+LkF4= 87 | Sign. b64 : MEUCIDsJPECetlDd6KUBhpZFsOfhQYoI45i+T9Lod1wsY8gN 88 | AiEA/ohyB+GuG+Z4MJNxH94xQUUpd2jpiDbG1r6FneDRpkE= 89 | Log found : Google 'Argon2020' log 90 | Chrome : True 91 | Result : Verified OK 92 | ``` 93 | 94 | ``` 95 | LogID b64 : Xqdz+d9WwOe1Nkh90EngMnqRmgyEoRIShBh1loFxRVg= 96 | Sign. b64 : MEUCIQChTO0dZC+zFcuvt3RPvuvMZ7RohbeizyRy5OhMpC/N 97 | kgIgTUhJTv5zdKBXDCgrgPoIYarBkYmTsirQDhALSEHHmZU= 98 | Log found : Cloudflare 'Nimbus2020' Log 99 | Chrome : True 100 | Result : Verified OK 101 | ``` 102 | 103 | ## SCTs by TLS 104 | 105 | no SCTs 106 | 107 | ## SCTs by OCSP 108 | 109 | no SCTs 110 | 111 | ##### Domains to try for different TLS-features 112 | 113 | ```bash 114 | > verify-scts ritter.vg sslanalyzer.comodoca.com www.db.com 115 | # has ⇧ ⇧ ⇧ 116 | # scts by: TLS-extension OCSP-extension certificate (precert) 117 | ``` 118 | 119 | ##### Output markdown into PDF 120 | 121 | ```bash 122 | # nice: convert the markdown formatted output into other formats with pandoc 123 | domain=ritter.vg 124 | fmt=pdf # {pdf,html,rst,...} 125 | verify-scts $domain 2>&1 | pandoc --from=markdown -o $domain-scts.$fmt 126 | ``` 127 | 128 | ### ctloglist 129 | 130 | ``` 131 | > ctloglist --help 132 | 133 | usage: ctloglist [-h] [-v] [--short | --debug] [--json | --schema] 134 | 135 | Download, merge and summarize known logs for Certificate Transparency (CT) 136 | 137 | optional arguments: 138 | -h, --help show this help message and exit 139 | -v, --version print version number 140 | --short show short results 141 | --debug show more for diagnostic purposes 142 | --json print merged log lists as json 143 | --schema print json schema 144 | 145 | Print output to stdout, warning and errors to stderr. Currently there exist 146 | three log lists with differing infos: 1. listing of webpage 147 | https://www.certificate-transparency.org/known-logs 2. log_list.json 3. 148 | all_logs_list.json. This three log lists will be merged into one list in the 149 | future. 150 | ``` 151 | Discussion: 152 | https://groups.google.com/forum/?fromgroups#!topic/certificate-transparency/zBv7EK0522w 153 | 154 | Created with `ctloglist`: 155 | * [really_all_logs.md](https://github.com/theno/ctutlz/blob/master/ctutlz/really_all_logs.md) 156 | * [really_all_logs.json](https://github.com/theno/ctutlz/blob/master/ctutlz/really_all_logs.json) 157 | 158 | Examples: 159 | 160 | ```bash 161 | # list really all known logs 162 | # infos aggregated from: 163 | # * log_list.json 164 | # * all_logs.json 165 | # * from log list webpage 166 | 167 | # overview 168 | > ctloglist --short 169 | 170 | # full, aggregated info 171 | > ctloglist 172 | 173 | # write into a json file 174 | > ctloglist --json > really_all_logs.json 175 | ``` 176 | 177 | ```bash 178 | # only show inconsistencies of the ct log lists 179 | > ctloglist 1>/dev/null 180 | ``` 181 | 182 | ### decompose-cert 183 | 184 | ``` 185 | > decompose-cert --help 186 | 187 | usage: decompose-cert [-h] [-v] --cert [--tbscert ] 188 | [--sign-algo ] [--signature ] 189 | 190 | Decompose an ASN.1 certificate into its components tbsCertificate in DER 191 | format, signatureAlgorithm in DER format, and signatureValue as bytes 192 | according to https://tools.ietf.org/html/rfc5280#section-4.1 193 | 194 | optional arguments: 195 | -h, --help show this help message and exit 196 | -v, --version print version number 197 | --tbscert write extracted tbsCertificate to this file (DER 198 | encoded) 199 | --sign-algo 200 | write extracted signatureAlgorithm to this file (DER 201 | encoded) 202 | --signature 203 | write extracted signatureValue to this file 204 | 205 | required arguments: 206 | --cert Certificate in PEM, Base64, or DER format 207 | ``` 208 | 209 | ### API 210 | 211 | Import module in your python code, for example: 212 | 213 | ```python 214 | > python3.6 215 | 216 | >>> from ctutlz.ctlog import download_log_list 217 | >>> from ctutlz.scripts.verify_scts import verify_scts_by_tls 218 | >>> from ctutlz.tls.handshake import do_handshake 219 | >>> 220 | >>> ctlogs = download_log_list() 221 | >>> handshake_res = do_handshake('google.com') 222 | >>> verifications = verify_scts_by_tls(handshake_res, ctlogs) 223 | >>> for ver in verifications: 224 | ... print(f'{ver.verified}: {ver.log.description}') 225 | ... 226 | True: Google 'Pilot' log 227 | True: Symantec log 228 | >>> 229 | >>> from ctutlz.rfc6962 import SignedCertificateTimestamp, MerkleTreeLeaf 230 | ``` 231 | 232 | ## Installation 233 | 234 | Install the latest version of the pypi python package 235 | [ctutlz](https://pypi.python.org/pypi/ctutlz): 236 | 237 | ```bash 238 | pip install ctutlz 239 | ``` 240 | 241 | ## Installation and Development 2022-10 242 | 243 | ctutlz is outdated and needs to be upgraded to use a current version of 244 | OpenSSL. 245 | 246 | Till then you can build and run ctutlz with podman and a Dockerfile 247 | using an old Ubuntu 16.04 and OpenSSL 1.0.2g: 248 | 249 | ```bash 250 | podman build --tag ctutlz . 251 | podman run -it --rm --name ctutlz --volume ./:/ctutlz ctutlz:latest 252 | root@:/ctutlz# pip3 install -e . 253 | root@:/ctutlz# python3 ctutlz/scripts/verify_scts.py google.com 254 | ``` 255 | 256 | ## Development 257 | 258 | Clone the source code [repository](https://github.com/theno/ctutlz): 259 | 260 | ``` 261 | git clone https://github.com/theno/ctutlz.git 262 | cd ctutlz 263 | ``` 264 | 265 | ### Fabfile 266 | 267 | The `fabfile.py` contains devel-tasks to be executed with 268 | [Fabric](http://www.fabfile.org/) (maybe you need to 269 | [install](http://www.fabfile.org/installing.html) it): 270 | 271 | ``` 272 | > fab -l 273 | 274 | Available commands: 275 | 276 | clean Delete temporary files not under version control. 277 | pypi Build package and upload to pypi. 278 | pythons Install latest pythons with pyenv. 279 | test Run unit tests. 280 | tox Run tox. 281 | 282 | # Show task details, e.g. for task `test`: 283 | > fab -d test 284 | 285 | Run unit tests. 286 | 287 | Keyword-Args: 288 | args: Optional arguments passed to pytest 289 | py: python version to run the tests against 290 | 291 | Example: 292 | 293 | fab test:args=-s,py=py27 294 | ``` 295 | 296 | At first, set up python versions with [pyenv](https://github.com/pyenv/pyenv) 297 | and virtualenvs for development with 298 | [tox](https://tox.readthedocs.io/en/latest/): 299 | ``` 300 | fab pythons 301 | fab tox 302 | ``` 303 | Tox creates virtualenvs of different Python versions (if they not exist 304 | already) and runs the unit tests against each virtualenv. 305 | 306 | On Ubuntu 16.04 you must install `libpython-dev` and `libpython3-dev` in order 307 | to make the tests passing for Python-2.7 and Python-3.5: 308 | 309 | ```bash 310 | sudo apt-get install libpython-dev libpython3-dev 311 | 312 | # Then, rebuild the non-working Python-2.7 and Python-3.5 virtualenv and 313 | # run the unit tests: 314 | fab tox:'-e py27 -e py35 --recreate' 315 | ``` 316 | 317 | ### Devel-Commands 318 | 319 | Run unit tests against several pythons with tox (needs pythons defined 320 | in envlist of `tox.ini` to be installed with pyenv): 321 | 322 | ```bash 323 | python3.6 -m tox 324 | 325 | # only against one python version: 326 | python3.6 -m tox -e py27 327 | 328 | # rebuild virtual environments: 329 | python3.6 -m tox -r 330 | ``` 331 | 332 | Run unit tests with pytest (uses tox virtualenv, replace `py36` by e.g. 333 | `py27` where applicable): 334 | 335 | ```bash 336 | PYTHONPATH='.' .tox/py36/bin/python -m pytest 337 | 338 | # show output 339 | PYTHONPATH='.' .tox/py36/bin/python -m pytest -s 340 | ``` 341 | 342 | Run tool `verify-scts` from source: 343 | 344 | ```bash 345 | PYTHONPATH='.' .tox/py36/bin/python ctutlz/scripts/verify_scts.py -h 346 | ``` 347 | 348 | ### Update really_all_logs 349 | 350 | ``` 351 | .tox/py36/bin/ctloglist > ctutlz/really_all_logs.md 352 | .tox/py36/bin/ctloglist --json > ctutlz/really_all_logs.json 353 | ``` 354 | -------------------------------------------------------------------------------- /ctutlz/__init__.py: -------------------------------------------------------------------------------- 1 | from ctutlz._version import __version__ 2 | -------------------------------------------------------------------------------- /ctutlz/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.8" 2 | -------------------------------------------------------------------------------- /ctutlz/ctlog.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import requests 4 | from os.path import abspath, expanduser, join, isfile, dirname 5 | 6 | import html2text 7 | from utlz import load_json, namedtuple, text_with_newlines 8 | from utlz.types import Enum 9 | 10 | from ctutlz.utils.encoding import decode_from_b64, encode_to_b64 11 | from ctutlz.utils.encoding import digest_from_b64 12 | from ctutlz.utils.logger import logger 13 | 14 | # https://groups.google.com/forum/#!topic/certificate-transparency/zZwGExvQeiE 15 | # PENDING: 16 | # The Log has requested inclusion in the Log list distributor’s trusted Log list, 17 | # but has not yet been accepted. 18 | # A PENDING Log does not count as ‘currently qualified’, and does not count as ‘once qualified’. 19 | # QUALIFIED: 20 | # The Log has been accepted by the Log list distributor, and added to the CT checking code 21 | # used by the Log list distributor. 22 | # A QUALIFIED Log counts as ‘currently qualified’. 23 | # USABLE: 24 | # SCTs from the Log can be relied upon from the perspective of the Log list distributor. 25 | # A USABLE Log counts as ‘currently qualified’. 26 | # FROZEN (READONLY in JSON-schema): 27 | # The Log is trusted by the Log list distributor, but is read-only, i.e. has stopped accepting 28 | # certificate submissions. 29 | # A FROZEN Log counts as ‘currently qualified’. 30 | # RETIRED: 31 | # The Log was trusted by the Log list distributor up until a specific retirement timestamp. 32 | # A RETIRED Log counts as ‘once qualified’ if the SCT in question was issued before the retirement timestamp. 33 | # A RETIRED Log does not count as ‘currently qualified’. 34 | # REJECTED: 35 | # The Log is not and will never be trusted by the Log list distributor. 36 | # A REJECTED Log does not count as ‘currently qualified’, and does not count as ‘once qualified’. 37 | KnownCTStates = Enum( 38 | PENDING='pending', 39 | QUALIFIED='qualified', 40 | USABLE='usable', 41 | READONLY='readonly', # frozen 42 | RETIRED='retired', 43 | REJECTED='rejected' 44 | ) 45 | 46 | Log = namedtuple( 47 | typename='Log', 48 | field_names=[ # each of type: str 49 | 'key', # base-64 encoded, type: str 50 | 'log_id', 51 | 'mmd', # v1: maximum_merge_delay 52 | 'url', 53 | 54 | # optional ones: 55 | 'description=None', 56 | 'dns=None', 57 | 'temporal_interval=None', 58 | 'log_type=None', 59 | 'state=None', # JSON-schema has: pending, qualified, usable, readonly, retired, rejected 60 | 61 | 'operated_by=None' 62 | ], 63 | lazy_vals={ 64 | 'key_der': lambda self: decode_from_b64(self.key), # type: bytes 65 | 'log_id_der': lambda self: digest_from_b64(self.key), # type: bytes 66 | 'pubkey': lambda self: '\n'.join([ # type: str 67 | '-----BEGIN PUBLIC KEY-----', 68 | text_with_newlines(text=self.key, 69 | line_length=64), 70 | '-----END PUBLIC KEY-----']), 71 | 'scts_accepted_by_chrome': 72 | lambda self: 73 | None if self.state is None else 74 | True if next(iter(self.state)) in [KnownCTStates.USABLE, 75 | KnownCTStates.QUALIFIED, 76 | KnownCTStates.READONLY] else 77 | False, 78 | } 79 | ) 80 | 81 | 82 | # plurale tantum constructor 83 | def Logs(log_dicts): 84 | ''' 85 | Arg log_dicts example: 86 | { 87 | "logs": [ 88 | { 89 | "description": "Google 'Argon2017' log", 90 | "log_id": "+tTJfMSe4vishcXqXOoJ0CINu/TknGtQZi/4aPhrjCg=", 91 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVG18id3qnfC6X/RtYHo3TwIlvxz2b4WurxXfaW7t26maKZfymXYe5jNGHif0vnDdWde6z/7Qco6wVw+dN4liow==", 92 | "url": "https://ct.googleapis.com/logs/argon2017/", 93 | "mmd": 86400, 94 | "state": { 95 | "rejected": { 96 | "timestamp": "2018-02-27T00:00:00Z" 97 | } 98 | }, 99 | "temporal_interval": { 100 | "start_inclusive": "2017-01-01T00:00:00Z", 101 | "end_exclusive": "2018-01-01T00:00:00Z" 102 | }, 103 | "operated_by": { 104 | "name": "Google", 105 | "email": [ 106 | "google-ct-logs@googlegroups.com" 107 | ], 108 | } 109 | }, 110 | ''' 111 | logs_out = [] 112 | for log in log_dicts: 113 | logs_out += [Log(**kwargs) for kwargs in log['logs']] 114 | 115 | return logs_out 116 | 117 | 118 | def set_operator_names(logs_dict): 119 | ''' 120 | Fold the logs listing by operator into list of logs. 121 | Append operator information to each log 122 | 123 | Arg log_dicts example: 124 | { 125 | "operators": [ 126 | { 127 | "name": "Google", 128 | "email": [ 129 | "google-ct-logs@googlegroups.com" 130 | ], 131 | "logs": [ 132 | { 133 | "description": "Google 'Argon2017' log", 134 | "log_id": "+tTJfMSe4vishcXqXOoJ0CINu/TknGtQZi/4aPhrjCg=", 135 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVG18id3qnfC6X/RtYHo3TwIlvxz2b4WurxXfaW7t26maKZfymXYe5jNGHif0vnDdWde6z/7Qco6wVw+dN4liow==", 136 | "url": "https://ct.googleapis.com/logs/argon2017/", 137 | "mmd": 86400, 138 | "state": { 139 | "rejected": { 140 | "timestamp": "2018-02-27T00:00:00Z" 141 | } 142 | }, 143 | "temporal_interval": { 144 | "start_inclusive": "2017-01-01T00:00:00Z", 145 | "end_exclusive": "2018-01-01T00:00:00Z" 146 | } 147 | }, 148 | ''' 149 | logs_dict['logs'] = [] 150 | for operator in logs_dict['operators']: 151 | operator_name = operator['name'] 152 | operator_email = operator['email'] 153 | for log in operator['logs']: 154 | log['operated_by'] = { 155 | 'name': operator_name, 156 | 'email': operator_email 157 | } 158 | logs_dict['logs'].append(log) 159 | del logs_dict['operators'] 160 | 161 | 162 | '''logs included in chrome browser''' 163 | BASE_URL = 'https://www.gstatic.com/ct/log_list/v3/' 164 | URL_LOG_LIST = BASE_URL + 'log_list.json' 165 | URL_ALL_LOGS = BASE_URL + 'all_logs_list.json' 166 | 167 | 168 | def download_log_list(url=URL_ALL_LOGS): 169 | '''Download json file with known logs accepted by chrome and return the 170 | logs as a list of `Log` items. 171 | 172 | Return: dict, the 'logs_dict' 173 | 174 | Arg log_dicts example: 175 | { 176 | "operators": [ 177 | { 178 | "name": "Google", 179 | "email": [ 180 | "google-ct-logs@googlegroups.com" 181 | ], 182 | "logs": [ 183 | { 184 | "description": "Google 'Argon2017' log", 185 | "log_id": "+tTJfMSe4vishcXqXOoJ0CINu/TknGtQZi/4aPhrjCg=", 186 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVG18id3qnfC6X/RtYHo3TwIlvxz2b4WurxXfaW7t26maKZfymXYe5jNGHif0vnDdWde6z/7Qco6wVw+dN4liow==", 187 | "url": "https://ct.googleapis.com/logs/argon2017/", 188 | "mmd": 86400, 189 | "state": { 190 | "rejected": { 191 | "timestamp": "2018-02-27T00:00:00Z" 192 | } 193 | }, 194 | "temporal_interval": { 195 | "start_inclusive": "2017-01-01T00:00:00Z", 196 | "end_exclusive": "2018-01-01T00:00:00Z" 197 | } 198 | }, 199 | ''' 200 | response = requests.get(url) 201 | response_str = response.text 202 | data = json.loads(response_str) 203 | data['url'] = url 204 | 205 | return data 206 | 207 | 208 | def read_log_list(filename): 209 | '''Read log list from file `filename` and return as logs_dict. 210 | 211 | Return: dict, the 'logs_dict' 212 | 213 | logs_dict example: { 214 | 'logs: [ 215 | { 216 | "description": "Google 'Aviator' log", 217 | "key": "MFkwE..." 218 | "url": "ct.googleapis.com/aviator/", 219 | "maximum_merge_delay": 86400, 220 | "operated_by": [0], 221 | "final_sth": { 222 | ... 223 | }, 224 | "dns_api_endpoint": ... 225 | }, 226 | ], 227 | 'operators': [ 228 | ... 229 | ] 230 | } 231 | ''' 232 | filename = abspath(expanduser(filename)) 233 | data = load_json(filename) 234 | return data 235 | 236 | 237 | def get_log_list(list_name='really_all_logs.json'): 238 | '''Try to read log list from local file. If file not exists download 239 | log list. 240 | 241 | Return: dict, the 'logs_dict' 242 | 243 | logs_dict example: { 244 | 'logs: [ 245 | { 246 | "description": "Google 'Aviator' log", 247 | "key": "MFkwE..." 248 | "url": "ct.googleapis.com/aviator/", 249 | "maximum_merge_delay": 86400, 250 | "operated_by": [0], 251 | "final_sth": { 252 | ... 253 | }, 254 | "dns_api_endpoint": ... 255 | }, 256 | ], 257 | 'operators': [ 258 | ... 259 | ] 260 | } 261 | ''' 262 | thisdir = dirname(__file__) 263 | filename = join(thisdir, list_name) 264 | if isfile(filename): 265 | logs_dict = read_log_list(filename) 266 | else: 267 | logs_dict = download_log_list(''.join([BASE_URL, list_name])) 268 | return logs_dict 269 | 270 | 271 | def print_schema(): 272 | thisdir = dirname(__file__) 273 | filename = join(thisdir, 'log_list_schema.json') 274 | with open(filename, 'r') as fh: 275 | json_str = fh.read() 276 | # print(json_str.strip()) 277 | print('TODO') 278 | -------------------------------------------------------------------------------- /ctutlz/log_list_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "id": "https://www.gstatic.com/ct/log_list/v3/log_list_schema.json", 4 | "$schema": "http://json-schema.org/draft-07/schema", 5 | "required": [ 6 | "operators" 7 | ], 8 | "definitions": { 9 | "state": { 10 | "type": "object", 11 | "properties": { 12 | "timestamp": { 13 | "description": "The time at which the log entered this state.", 14 | "type": "string", 15 | "format": "date-time", 16 | "examples": [ 17 | "2018-01-01T00:00:00Z" 18 | ] 19 | } 20 | }, 21 | "required": [ 22 | "timestamp" 23 | ] 24 | } 25 | }, 26 | "properties": { 27 | "version": { 28 | "type": "string", 29 | "title": "Version of this log list", 30 | "description": "The version will change whenever a change is made to any part of this log list.", 31 | "examples": [ 32 | "1", 33 | "1.0.0", 34 | "1.0.0b" 35 | ] 36 | }, 37 | "log_list_timestamp": { 38 | "description": "The time at which this version of the log list was published.", 39 | "type": "string", 40 | "format": "date-time", 41 | "examples": [ 42 | "2018-01-01T00:00:00Z" 43 | ] 44 | }, 45 | "operators": { 46 | "title": "CT log operators", 47 | "description": "People/organizations that run Certificate Transparency logs.", 48 | "type": "array", 49 | "items": { 50 | "type": "object", 51 | "required": [ 52 | "name", 53 | "email", 54 | "logs" 55 | ], 56 | "properties": { 57 | "name": { 58 | "title": "Name of this log operator", 59 | "type": "string" 60 | }, 61 | "email": { 62 | "title": "CT log operator email addresses", 63 | "description": "The log operator can be contacted using any of these email addresses.", 64 | "type": "array", 65 | "minItems": 1, 66 | "uniqueItems": true, 67 | "items": { 68 | "type": "string", 69 | "format": "email" 70 | } 71 | }, 72 | "logs": { 73 | "description": "Details of Certificate Transparency logs run by this operator.", 74 | "type": "array", 75 | "items": { 76 | "type": "object", 77 | "required": [ 78 | "key", 79 | "log_id", 80 | "mmd", 81 | "url" 82 | ], 83 | "properties": { 84 | "description": { 85 | "title": "Description of the CT log", 86 | "description": "A human-readable description that can be used to identify this log.", 87 | "type": "string" 88 | }, 89 | "key": { 90 | "title": "The public key of the CT log", 91 | "description": "The log's public key as a DER-encoded ASN.1 SubjectPublicKeyInfo structure, then encoded as base64 (https://tools.ietf.org/html/rfc5280#section-4.1.2.7).", 92 | "type": "string" 93 | }, 94 | "log_id": { 95 | "title": "The SHA-256 hash of the CT log's public key, base64-encoded", 96 | "description": "This is the LogID found in SCTs issued by this log (https://tools.ietf.org/html/rfc6962#section-3.2).", 97 | "type": "string", 98 | "minLength": 44, 99 | "maxLength": 44 100 | }, 101 | "mmd": { 102 | "title": "The Maximum Merge Delay, in seconds", 103 | "description": "The CT log should not take longer than this to incorporate a certificate (https://tools.ietf.org/html/rfc6962#section-3).", 104 | "type": "number", 105 | "minimum": 1, 106 | "default": 86400 107 | }, 108 | "url": { 109 | "title": "The base URL of the CT log's HTTP API", 110 | "description": "The API endpoints are defined in https://tools.ietf.org/html/rfc6962#section-4.", 111 | "type": "string", 112 | "format": "uri", 113 | "examples": [ 114 | "https://ct.googleapis.com/pilot/" 115 | ] 116 | }, 117 | "dns": { 118 | "title": "The domain name of the CT log's DNS API", 119 | "description": "The API endpoints are defined in https://github.com/google/certificate-transparency-rfcs/blob/master/dns/draft-ct-over-dns.md.", 120 | "type": "string", 121 | "format": "hostname", 122 | "examples": [ 123 | "pilot.ct.googleapis.com" 124 | ] 125 | }, 126 | "temporal_interval": { 127 | "description": "The log will only accept certificates that expire (have a NotAfter date) between these dates.", 128 | "type": "object", 129 | "required": [ 130 | "start_inclusive", 131 | "end_exclusive" 132 | ], 133 | "properties": { 134 | "start_inclusive": { 135 | "description": "All certificates must expire on this date or later.", 136 | "type": "string", 137 | "format": "date-time", 138 | "examples": [ 139 | "2018-01-01T00:00:00Z" 140 | ] 141 | }, 142 | "end_exclusive": { 143 | "description": "All certificates must expire before this date.", 144 | "type": "string", 145 | "format": "date-time", 146 | "examples": [ 147 | "2019-01-01T00:00:00Z" 148 | ] 149 | } 150 | } 151 | }, 152 | "log_type": { 153 | "description": "The purpose of this log, e.g. test.", 154 | "type": "string", 155 | "enum": [ 156 | "prod", 157 | "test" 158 | ] 159 | }, 160 | "state": { 161 | "title": "The state of the log from the log list distributor's perspective.", 162 | "type": "object", 163 | "properties": { 164 | "pending": { 165 | "$ref": "#/definitions/state" 166 | }, 167 | "qualified": { 168 | "$ref": "#/definitions/state" 169 | }, 170 | "usable": { 171 | "$ref": "#/definitions/state" 172 | }, 173 | "readonly": { 174 | "allOf": [ 175 | { 176 | "$ref": "#/definitions/state" 177 | }, 178 | { 179 | "required": [ 180 | "final_tree_head" 181 | ], 182 | "properties": { 183 | "final_tree_head": { 184 | "description": "The tree head (tree size and root hash) at which the log was made read-only.", 185 | "type": "object", 186 | "required": [ 187 | "tree_size", 188 | "sha256_root_hash" 189 | ], 190 | "properties": { 191 | "tree_size": { 192 | "type": "number", 193 | "minimum": 0 194 | }, 195 | "sha256_root_hash": { 196 | "type": "string", 197 | "minLength": 44, 198 | "maxLength": 44 199 | } 200 | } 201 | } 202 | } 203 | } 204 | ] 205 | }, 206 | "retired": { 207 | "$ref": "#/definitions/state" 208 | }, 209 | "rejected": { 210 | "$ref": "#/definitions/state" 211 | } 212 | }, 213 | "oneOf": [ 214 | { 215 | "required": [ 216 | "pending" 217 | ] 218 | }, 219 | { 220 | "required": [ 221 | "qualified" 222 | ] 223 | }, 224 | { 225 | "required": [ 226 | "usable" 227 | ] 228 | }, 229 | { 230 | "required": [ 231 | "readonly" 232 | ] 233 | }, 234 | { 235 | "required": [ 236 | "retired" 237 | ] 238 | }, 239 | { 240 | "required": [ 241 | "rejected" 242 | ] 243 | } 244 | ] 245 | }, 246 | "previous_operators": { 247 | "title": "Previous operators that ran this log in the past, if any.", 248 | "description": "If the log has changed operators, this will contain a list of the previous operators, along with the timestamp when they stopped operating the log.", 249 | "type": "array", 250 | "uniqueItems": true, 251 | "items": { 252 | "type": "object", 253 | "required": [ 254 | "name", 255 | "end_time" 256 | ], 257 | "properties": { 258 | "name": { 259 | "title": "Name of the log operator", 260 | "type": "string" 261 | }, 262 | "end_time": { 263 | "description": "The time at which this operator stopped operating this log.", 264 | "type": "string", 265 | "format": "date-time", 266 | "examples": [ 267 | "2018-01-01T00:00:00Z" 268 | ] 269 | } 270 | } 271 | } 272 | } 273 | } 274 | } 275 | } 276 | } 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /ctutlz/rfc6962.py: -------------------------------------------------------------------------------- 1 | '''namedtuple defs which represent the data structures defined in RFC 6962 - 2 | Certificate Transparency. 3 | ''' 4 | 5 | import struct 6 | 7 | from pyasn1.codec.der.encoder import encode as der_encoder 8 | from pyasn1.codec.der.decoder import decode as der_decoder 9 | from pyasn1_modules import rfc5280 10 | from utlz import flo, namedtuple as namedtuple_utlz 11 | 12 | from ctutlz.utils.tdf_bytes import TdfBytesParser, namedtuple 13 | from ctutlz.utils.encoding import decode_from_b64, encode_to_b64 14 | from ctutlz.utils.string import to_hex 15 | from ctutlz.sct.ee_cert import tbscert_without_ct_extensions 16 | 17 | 18 | # tdf := "TLS Data Format" (cf. https://tools.ietf.org/html/rfc5246#section-4) 19 | 20 | 21 | # 3.1. Log Entries 22 | # https://tools.ietf.org/html/rfc6962#section-3.1 23 | 24 | 25 | def _parse_log_entry_type(tdf): 26 | with TdfBytesParser(tdf) as parser: 27 | parser.read('val', '!H') # (65535) -> 2 bytes 28 | return parser.result() 29 | 30 | 31 | LogEntryType = namedtuple( 32 | typename='LogEntryType', 33 | field_names='arg', 34 | lazy_vals={ 35 | '_parse_func': lambda _: _parse_log_entry_type, 36 | 37 | 'val': lambda self: self._parse['val'], 38 | 39 | 'is_x509_entry': lambda self: self.val == 0, 40 | 'is_precert_entry': lambda self: self.val == 1, 41 | 42 | '__str__': lambda self: lambda: 43 | 'x509_entry' if self.is_x509_entry else 44 | 'precert_entry' if self.is_precert_entry else 45 | flo(''), 46 | } 47 | ) 48 | 49 | 50 | def _parse_log_entry(tdf): 51 | with TdfBytesParser(tdf) as parser: 52 | entry_type = LogEntryType( 53 | parser.delegate('entry_type', _parse_log_entry_type)) 54 | 55 | # parse entry 56 | if entry_type.is_x509_entry: 57 | parser.delegate('entry', _parse_x509_chain_entry) 58 | parser.res['x509_entry'] = parser.res['entry'] 59 | elif entry_type.is_precert_entry: 60 | parser.delegate('entry', _parse_precert_chain_entry) 61 | parser.res['precert_entry'] = parser.res['entry'] 62 | else: 63 | raise Exception(flo('Unknown entry_type: {entry_type}')) 64 | 65 | return parser.result() 66 | 67 | 68 | LogEntry = namedtuple( 69 | typename='LogEntry', 70 | field_names='arg', 71 | lazy_vals={ 72 | '_parse_func': lambda _: _parse_log_entry, 73 | 74 | 'entry_type': lambda self: LogEntryType(self._parse['entry_type']), 75 | 'entry': lambda self: 76 | ASN1Cert(self._parse['entry']) 77 | if self.entry_type.is_x509_entry else 78 | PreCert(self._parse['entry']) 79 | if self.entry_type.is_precert_entry else 80 | None, 81 | } 82 | ) 83 | 84 | 85 | def _parse_asn1_cert(tdf): 86 | with TdfBytesParser(tdf) as parser: 87 | parser.read('len1', '!B') 88 | parser.read('len2', '!B') 89 | parser.read('len3', '!B') 90 | 91 | der_len = struct.unpack('=I', struct.pack('!4B', 92 | 0, 93 | parser.res['len1'], 94 | parser.res['len2'], 95 | parser.res['len3']))[0] 96 | parser.res['der_len'] = der_len 97 | parser.read('der', flo('!{der_len}s')) 98 | 99 | return parser.result() 100 | 101 | 102 | ASN1Cert = namedtuple( 103 | typename='ASN1Cert', 104 | field_names='arg', 105 | lazy_vals={ 106 | '_parse_func': lambda _: _parse_asn1_cert, 107 | 108 | 'der': lambda self: self._parse['der'], 109 | 'pyasn1': lambda self: der_decoder(self.der, rfc5280.Certificate()), 110 | } 111 | ) 112 | 113 | 114 | def _parse_asn1_cert_list(tdf): 115 | with TdfBytesParser(tdf) as parser: 116 | parser.read('len1', '!B') 117 | parser.read('len2', '!B') 118 | parser.read('len3', '!B') 119 | 120 | der_list_len = struct.unpack('=I', struct.pack('!4B', 121 | 0, 122 | parser.res['len1'], 123 | parser.res['len2'], 124 | parser.res['len3']))[0] 125 | der_end_offset = parser.offset + der_list_len 126 | 127 | list_of_parse_asn1_cert = [] 128 | while parser.offset < der_end_offset: 129 | parse_asn1_cert = parser.delegate(_parse_asn1_cert) 130 | list_of_parse_asn1_cert.append(parse_asn1_cert) 131 | 132 | parser.res['der_list_len'] = der_list_len 133 | parser.res['list_of_parse_asn1_cert'] = list_of_parse_asn1_cert 134 | 135 | return parser.result() 136 | 137 | 138 | ASN1CertList = namedtuple( 139 | typename='ASN1CertList', 140 | field_names='arg', 141 | lazy_vals={ 142 | '_parse_func': lambda _: _parse_asn1_cert_list, 143 | 144 | 'certs': lambda self: [ 145 | ASN1Cert(parse_asn1_cert) 146 | for parse_asn1_cert 147 | in self._parse['list_of_parse_asn1_cert'] 148 | ], 149 | } 150 | ) 151 | 152 | 153 | def _parse_x509_chain_entry(tdf): 154 | with TdfBytesParser(tdf) as parser: 155 | parser.delegate('leaf_certificate', _parse_asn1_cert), 156 | parser.delegate('certificate_chain', _parse_asn1_cert_list), 157 | return parser.result() 158 | 159 | 160 | X509ChainEntry = namedtuple( 161 | typename='X509ChainEntry', 162 | field_names='arg', 163 | lazy_vals={ 164 | '_parse_func': lambda _: _parse_x509_chain_entry, 165 | 166 | 'leaf_certificate': lambda self: 167 | ASN1Cert(self._parse['leaf_certificate']), 168 | 'certificate_chain': lambda self: 169 | ASN1CertList(self._parse['certificate_chain']), 170 | } 171 | ) 172 | 173 | 174 | def _parse_precert_chain_entry(tdf): 175 | with TdfBytesParser(tdf) as parser: 176 | parser.delegate('pre_certificate', _parse_asn1_cert), 177 | parser.delegate('precert_chain', _parse_asn1_cert_list), 178 | return parser.result() 179 | 180 | 181 | PrecertChainEntry = namedtuple( 182 | typename='PrecertChainEntry', 183 | field_names='arg', 184 | lazy_vals={ 185 | '_parse_func': lambda _: _parse_precert_chain_entry, 186 | 187 | 'pre_certificate': lambda self: 188 | ASN1Cert(self._parse['pre_certificate']), 189 | 'precertificate_chain': lambda self: 190 | ASN1CertList(self._parse['precert_chain']), 191 | } 192 | ) 193 | 194 | 195 | # 3.2 Structure of the Signed Certificate Timestamp 196 | # https://tools.ietf.org/html/rfc6962#section-3.2 197 | 198 | 199 | def _parse_signature_type(tdf): 200 | with TdfBytesParser(tdf) as parser: 201 | parser.read('val', '!B') 202 | return parser.result() 203 | 204 | 205 | SignatureType = namedtuple( 206 | typename='SignatureType', 207 | field_names='arg', 208 | lazy_vals={ 209 | '_parse_func': lambda _: _parse_signature_type, 210 | 211 | 'val': lambda self: self._parse['val'], 212 | 213 | 'is_certificate_timestamp': lambda self: self.val == 0, 214 | 'is_tree_hash': lambda self: self.val == 1, 215 | 216 | '__str__': lambda self: lambda: 217 | 'certificate_timestamp' if self.is_certificate_timestamp else 218 | 'tree_hash' if self.is_tree_hash else 219 | '', 220 | } 221 | ) 222 | 223 | 224 | def _parse_version(tdf): 225 | with TdfBytesParser(tdf) as parser: 226 | parser.read('val', '!B') 227 | return parser.result() 228 | 229 | 230 | Version = namedtuple( 231 | typename='Version', 232 | field_names='arg', 233 | lazy_vals={ 234 | '_parse_func': lambda _: _parse_version, 235 | 236 | 'val': lambda self: int(self._parse['val']), 237 | 238 | 'is_v1': lambda self: self.val == 0, 239 | 240 | '__str__': lambda self: lambda: 241 | 'v1' if self.is_v1 else 242 | '', 243 | } 244 | ) 245 | 246 | 247 | def _parse_log_id(tdf): 248 | with TdfBytesParser(tdf) as parser: 249 | parser.read('val', '!32s') 250 | return parser.result() 251 | 252 | 253 | LogID = namedtuple( 254 | typename='LogID', 255 | lazy_vals={ 256 | '_parse_func': lambda _: _parse_log_id, 257 | 258 | # type: int, '!L', [32] 259 | # https://docs.python.org/3/library/struct.html#format-characters 260 | 'val': lambda self: bytes(self._parse['val']), 261 | }, 262 | ) 263 | 264 | 265 | def _parse_tbs_certificate(tdf): 266 | with TdfBytesParser(tdf) as parser: 267 | parser.read('len1', '!B') 268 | parser.read('len2', '!B') 269 | parser.read('len3', '!B') 270 | len_der = struct.unpack('=I', struct.pack('!4B', 271 | 0, 272 | parser.res['len1'], 273 | parser.res['len2'], 274 | parser.res['len3']))[0] 275 | from_ = parser.offset 276 | parser.offset += len_der 277 | until = parser.offset 278 | parser.res['der'] = tdf[from_:until] 279 | return parser.result() 280 | 281 | 282 | TBSCertificate = namedtuple( 283 | typename='TBSCertificate', 284 | field_names='arg', 285 | lazy_vals={ 286 | '_parse_func': lambda _: _parse_tbs_certificate, 287 | 288 | 'der': lambda self: bytes(self._parse['der']), 289 | 'pyasn1': lambda self: der_decoder(self.der, 290 | asn1Spec=rfc5280.TBSCertificate()), 291 | 292 | 'len': lambda self: len(self.der), 293 | 'lens': lambda self: struct.unpack('!4B', struct.pack('!I', self.len)), 294 | 'len1': lambda self: self.lens[1], 295 | 'len2': lambda self: self.lens[2], 296 | 'len3': lambda self: self.lens[3], 297 | 298 | 'without_ct_extensions': lambda self: 299 | der_encoder( 300 | TBSCertificate(tbscert_without_ct_extensions(self.pyasn1))), 301 | } 302 | ) 303 | 304 | 305 | def _parse_pre_cert(tdf): 306 | with TdfBytesParser(tdf) as parser: 307 | parser.read('issuer_key_hash', '!32s') 308 | parser.delegate('tbs_certificate', _parse_tbs_certificate) 309 | return parser.result() 310 | 311 | 312 | PreCert = namedtuple( 313 | typename='PreCert', 314 | field_names='arg', 315 | lazy_vals={ 316 | '_parse_func': lambda _: _parse_pre_cert, 317 | 318 | 'issuer_key_hash': lambda self: bytes(self._parse['issuer_key_hash']), 319 | 'tbs_certificate': lambda self: 320 | TBSCertificate(self._parse['tbs_certificate']), 321 | } 322 | ) 323 | 324 | 325 | def _parse_ct_extensions(tdf): 326 | with TdfBytesParser(tdf) as parser: 327 | parser.read('len', '!H') 328 | parser.res['val'] = None # "Currently, no extensions are specified" 329 | return parser.result() 330 | 331 | 332 | CtExtensions = namedtuple( 333 | typename='CtExtensions', 334 | field_names='arg', 335 | lazy_vals={ 336 | '_parse_func': lambda _: _parse_ct_extensions, 337 | 338 | 'len': lambda self: self._parse['len'], 339 | 'val': lambda self: self._parse['val'], 340 | } 341 | ) 342 | 343 | 344 | def _parse_signed_certificate_timestamp(tdf): 345 | with TdfBytesParser(tdf) as parser: 346 | parser.delegate('version', _parse_version) 347 | parser.delegate('id', _parse_log_id) 348 | parser.read('timestamp', '!Q') 349 | 350 | parser.delegate('ct_extensions', _parse_ct_extensions) 351 | 352 | # digitally-signed struct 353 | parser.read('signature_alg_hash', '!B'), 354 | parser.read('signature_alg_sign', '!B'), 355 | signature_len = parser.read('signature_len', '!H') 356 | parser.read('signature', flo('!{signature_len}s')) 357 | 358 | return parser.result() 359 | 360 | 361 | SignedCertificateTimestamp = namedtuple( 362 | typename='SignedCertificateTimestamp', 363 | field_names='arg', 364 | lazy_vals={ 365 | '_parse_func': lambda _: _parse_signed_certificate_timestamp, 366 | 367 | 'version': lambda self: Version(self._parse['version']), 368 | 'id': lambda self: LogID(self._parse['id']), 369 | 'timestamp': lambda self: int(self._parse['timestamp']), 370 | 'extensions': lambda self: CtExtensions(self._parse['ct_extensions']), 371 | 372 | # digitally-signed struct 373 | # https://tools.ietf.org/html/rfc5246#section-4.7 374 | 'signature_algorithm_hash': lambda self: 375 | int(self._parse['signature_alg_hash']), 376 | 'signature_algorithm_signature': lambda self: 377 | int(self._parse['signature_alg_sign']), 378 | 'signature_len': lambda self: int(self._parse['signature_len']), 379 | 'signature': lambda self: bytes(self._parse['signature']), 380 | 381 | 'log_id': lambda self: self.id, 382 | 'log_id_b64': lambda self: encode_to_b64(self.log_id.tdf), # type: str 383 | 'version_hex': lambda self: to_hex(self.version.tdf), 384 | 'timestamp_hex': lambda self: to_hex(self.timestamp), 385 | 'extensions_len': lambda self: self.extensions.len, 386 | 'extensions_len_hex': lambda self: to_hex(self.extensions_len), 387 | 'signature_alg_hash_hex': lambda self: 388 | to_hex(self.signature_algorithm_hash), 389 | 'signature_alg_sign_hex': lambda self: 390 | to_hex(self.signature_algorithm_sign), 391 | 'signature_b64': lambda self: encode_to_b64(self.signature), # str 392 | 393 | 'b64': lambda self: encode_to_b64(self.tdf) # str 394 | } 395 | ) 396 | 397 | 398 | def _parse_signature_input(tdf): 399 | with TdfBytesParser(tdf) as parser: 400 | parser.delegate('sct_version', _parse_version) 401 | parser.delegate('signature_type', _parse_signature_type) 402 | 403 | # rest of the SignatureInput is identical to an TimestampedEntry 404 | parser.delegate('_tmp', _parse_timestamped_entry) 405 | parser.res.update(parser.res['_tmp']) 406 | del parser.res['_tmp'] 407 | 408 | return parser.result() 409 | 410 | 411 | # 'digitally-signed struct' of the SignedCertificateTimestamp 412 | SignatureInput = namedtuple( 413 | typename='SignatureInput', 414 | field_names='arg', 415 | lazy_vals={ 416 | '_parse_func': lambda _: _parse_signature_input, 417 | 418 | 'sct_version': lambda self: Version(self._parse['sct_version']), 419 | 'signature_type': lambda self: 420 | SignatureType(self._parse['signature_type']), 421 | 'timestamp': lambda self: int(self._parse['timestamp']), 422 | 'entry_type': lambda self: LogEntryType(self._parse['entry_type']), 423 | 'signed_entry': lambda self: 424 | ASN1Cert(self._parse['signed_entry']) 425 | if self.entry_type.is_x509_entry else 426 | PreCert(self._parse['signed_entry']) 427 | if self.entry_type.is_precert_entry else 428 | None, 429 | 430 | 'precert_entry': lambda self: self._parse.get('precert_entry', None), 431 | 'x509_entry': lambda self: self._parse.get('x509_entry', None), 432 | } 433 | ) 434 | 435 | 436 | # 3.4 Merkle Tree 437 | # https://tools.ietf.org/html/rfc6962#section-3.4 438 | 439 | 440 | def _parse_merkle_leaf_type(tdf): 441 | with TdfBytesParser(tdf) as parser: 442 | parser.read('val', '!B') # (255) 443 | return parser.result() 444 | 445 | 446 | MerkleLeafType = namedtuple( 447 | typename='MerkleLeafType', 448 | field_names='arg', 449 | lazy_vals={ 450 | '_parse_func': lambda _: _parse_merkle_leaf_type, 451 | 452 | 'val': lambda self: int(self._parse['val']), 453 | 454 | 'is_timestamped_entry': lambda self: self.val == 0, 455 | 456 | '__str__': lambda self: lambda: 457 | 'timestamped_entry' if self.is_timestamped_entry else 458 | '', 459 | } 460 | ) 461 | 462 | 463 | def _parse_timestamped_entry(tdf): 464 | with TdfBytesParser(tdf) as parser: 465 | parser.read('timestamp', '!Q') # uint64 -> 8 bytes 466 | entry_type = LogEntryType( 467 | parser.delegate('entry_type', _parse_log_entry_type)) 468 | 469 | # parse leaf_entry 470 | if entry_type.is_x509_entry: 471 | parser.delegate('signed_entry', _parse_asn1_cert) 472 | parser.res['x509_entry'] = parser.res['signed_entry'] 473 | elif entry_type.is_precert_entry: 474 | parser.delegate('signed_entry', _parse_pre_cert) 475 | parser.res['precert_entry'] = parser.res['signed_entry'] 476 | else: 477 | raise Exception(flo('Unknown entry_type number: {entry_type}')) 478 | 479 | # TODO DEBUG ctlog_get_entries.py related (it looks like some log 480 | # answers are missing 481 | # the ct_extensions, 482 | # or an error in parse routines) 483 | try: 484 | parser.delegate('extensions', _parse_ct_extensions) 485 | except struct.error: 486 | pass 487 | 488 | return parser.result() 489 | 490 | 491 | TimestampedEntry = namedtuple( 492 | typename='TimestampedEntry', 493 | field_names='arg', 494 | lazy_vals={ 495 | '_parse_func': lambda _: _parse_timestamped_entry, 496 | 497 | 'timestamp': lambda self: int(self._parse.get('timestamp')), 498 | 'entry_type': lambda self: LogEntryType(self._parse['entry_type']), 499 | 'signed_entry': lambda self: 500 | ASN1Cert(self._parse['signed_entry']) 501 | if self.entry_type.is_x509_entry else 502 | PreCert(self._parse['signed_entry']) 503 | if self.entry_type.is_precert_entry else 504 | None, 505 | 'extensions': lambda self: CtExtensions(self._parse.get('extensions')), 506 | 507 | 'precert_entry': lambda self: self._parse.get('precert_entry', None), 508 | 'x509_entry': lambda self: self._parse.get('x509_entry', None), 509 | } 510 | ) 511 | 512 | 513 | def _parse_merkle_tree_leaf(tdf): 514 | with TdfBytesParser(tdf) as parser: 515 | parser.delegate('version', _parse_version) 516 | leaf_type = parser.delegate('leaf_type', _parse_merkle_leaf_type) 517 | 518 | if MerkleLeafType(leaf_type).is_timestamped_entry: 519 | parser.delegate('leaf_entry', _parse_timestamped_entry) 520 | else: 521 | raise Exception('unknown leaf_type: {leaf_type}!') 522 | 523 | return parser.result() 524 | 525 | 526 | MerkleTreeLeaf = namedtuple( 527 | typename='MerkleTreeLeaf', 528 | field_names='arg', 529 | lazy_vals={ 530 | '_parse_func': lambda _: _parse_merkle_tree_leaf, 531 | 532 | 'version': lambda self: Version(self._parse['version']), 533 | 'leaf_type': lambda self: MerkleLeafType(self._parse['leaf_type']), 534 | 'leaf_entry': lambda self: TimestampedEntry(self._parse['leaf_entry']), 535 | 536 | # alias for 'leaf_entry' 537 | 'timestamped_entry': lambda self: self.leaf_entry, 538 | 539 | '__str__': lambda self: lambda: 540 | self.__repr__(), 541 | } 542 | ) 543 | 544 | 545 | # 4.6. Retrieve Entries from Log 546 | # https://tools.ietf.org/html/rfc6962#section-4.6 547 | 548 | GetEntriesInput = namedtuple_utlz( 549 | typename='GetEntriesInput', 550 | field_names=[ 551 | 'start', 552 | 'end', 553 | ], 554 | ) 555 | 556 | 557 | GetEntriesResponseEntry = namedtuple_utlz( 558 | typename='GetEntriesResponseEntry', 559 | field_names=[ 560 | 'json_dict', 561 | ], 562 | lazy_vals={ 563 | # The base-64 encoded MerkleTreeLeaf structure 564 | 'leaf_input_b64': lambda self: self.json_dict['leaf_input'], 565 | 'leaf_input_tdf': lambda self: decode_from_b64(self.leaf_input_b64), 566 | 'leaf_input': lambda self: MerkleTreeLeaf(self.leaf_input_tdf), 567 | 568 | 'is_x509_chain_entry': lambda self: 569 | self.leaf_input.timestamped_entry.entry_type == 0, 570 | 'is_precert_chain_entry': lambda self: not self.is_x509_chain_entry, 571 | 572 | # The base-64 encoded unsigned data pertaining to the log entry. In the 573 | # case of an X509ChainEntry, this is the "certificate_chain". In the 574 | # case of a PrecertChainEntry, this is the whole "PrecertChainEntry" 575 | 'extra_data_b64': lambda self: self.json_dict['extra_data'], 576 | 'extra_data_tdf': lambda self: decode_from_b64(self.extra_data_b64), 577 | 'extra_data': lambda self: 578 | X509ChainEntry(self.extra_data_tdf) if self.is_x509_chain_entry else 579 | PrecertChainEntry(self.extra_data_tdf), 580 | 581 | # '__str__': lambda self: '', 582 | } 583 | ) 584 | 585 | 586 | GetEntriesResponse = namedtuple_utlz( 587 | typename='GetEntriesResponse', 588 | field_names=[ 589 | 'json_dict', 590 | ], 591 | lazy_vals={ 592 | 'entries': lambda self: [GetEntriesResponseEntry(entry) 593 | for entry 594 | in self.json_dict['entries']], 595 | 596 | # for convenience 597 | 'first_entry': lambda self: self.entries[0], 598 | }, 599 | ) 600 | -------------------------------------------------------------------------------- /ctutlz/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/ctutlz/scripts/__init__.py -------------------------------------------------------------------------------- /ctutlz/scripts/ctloglist.py: -------------------------------------------------------------------------------- 1 | '''Download, merge and summarize known logs for Certificate Transparency (CT). 2 | 3 | Print output to stdout, warnings and errors to stderr. 4 | 5 | The source of information is: 6 | https://www.gstatic.com/ct/log_list/v2/all_logs_list.json 7 | 8 | from page https://www.certificate-transparency.org/known-logs 9 | ''' 10 | 11 | import argparse 12 | import datetime 13 | import json 14 | import logging 15 | 16 | from utlz import first_paragraph, red 17 | 18 | from ctutlz.ctlog import download_log_list 19 | from ctutlz.ctlog import set_operator_names, print_schema 20 | from ctutlz.ctlog import URL_ALL_LOGS, Logs 21 | from ctutlz.utils.logger import VERBOSE, init_logger, setup_logging, logger 22 | from ctutlz._version import __version__ 23 | 24 | 25 | def create_parser(): 26 | parser = argparse.ArgumentParser(description=first_paragraph(__doc__)) 27 | parser.epilog = __doc__.split('\n', 1)[-1] 28 | parser.add_argument('-v', '--version', 29 | action='version', 30 | default=False, 31 | version=__version__, 32 | help='print version number') 33 | 34 | me1 = parser.add_mutually_exclusive_group() 35 | me1.add_argument('--short', 36 | dest='loglevel', 37 | action='store_const', 38 | const=logging.INFO, 39 | default=VERBOSE, # default loglevel if nothing set 40 | help='show short results') 41 | me1.add_argument('--debug', 42 | dest='loglevel', 43 | action='store_const', 44 | const=logging.DEBUG, 45 | help='show more for diagnostic purposes') 46 | 47 | me2 = parser.add_mutually_exclusive_group() 48 | me2.add_argument('--json', 49 | action='store_true', 50 | dest='print_json', 51 | help='print merged log lists as json') 52 | me2.add_argument('--schema', 53 | action='store_true', 54 | dest='print_schema', 55 | help='print json schema') 56 | return parser 57 | 58 | 59 | def warn_inconsistency(url, val_a, val_b): 60 | 61 | # suppress warning doubles (i know it's hacky) 62 | key = url + ''.join(sorted('%s%s' % (val_a, val_b))) 63 | if not hasattr(warn_inconsistency, 'seen'): 64 | warn_inconsistency.seen = {} 65 | if not warn_inconsistency.seen.get(key, False): 66 | warn_inconsistency.seen[key] = True 67 | else: 68 | return 69 | 70 | logger.warning(red('inconsistent data for log %s: %s != %s' % (url, val_a, val_b))) 71 | 72 | 73 | def data_structure_from_log(log): 74 | log_data = dict(log._asdict()) 75 | 76 | log_data['id_b64'] = log.id_b64 77 | log_data['pubkey'] = log.pubkey 78 | log_data['scts_accepted_by_chrome'] = \ 79 | log.scts_accepted_by_chrome 80 | 81 | return log_data 82 | 83 | 84 | def list_from_lists(log_lists): 85 | log_list = [] 86 | for item_dict in log_lists: 87 | for log in item_dict['logs']: 88 | log_data = data_structure_from_log(log) 89 | log_list.append(log_data) 90 | return log_list 91 | 92 | 93 | def show_log(log, order=3): 94 | logger.verbose('#' * order + ' %s\n' % log.url) 95 | 96 | logdict = log._asdict() 97 | 98 | for key, value in logdict.items(): 99 | if key == 'id_b64_non_calculated' and value == log.id_b64: 100 | value = None # don't log this value 101 | if key == 'operated_by': 102 | value = ', '.join(value) 103 | # avoid markdown syntax interpretation and improve readablity 104 | key = key.replace('_', ' ') 105 | if value is not None: 106 | logger.verbose('* __%s__: `%s`' % (key, value)) 107 | 108 | logger.verbose('* __scts accepted by chrome__: ' 109 | '%s' % log.scts_accepted_by_chrome) 110 | 111 | if log.key is not None: 112 | logger.verbose('* __id b64__: `%s`' % log.log_id) 113 | logger.verbose('* __pubkey__:\n```\n%s\n```' % log.pubkey) 114 | 115 | logger.verbose('') 116 | 117 | 118 | def show_logs(logs, heading, order=2): 119 | if len(logs) <= 0: 120 | return 121 | 122 | logger.info('#' * order + '%s\n' % ' ' + heading if heading else '') 123 | s_or_not = 's' 124 | if len(logs) == 1: 125 | s_or_not = '' 126 | # show log size 127 | logger.info('%i log%s\n' % (len(logs), s_or_not)) 128 | 129 | # list log urls 130 | for log in logs: 131 | if logger.level < logging.INFO: 132 | anchor = log.url.replace('/', '') 133 | logger.verbose('* [%s](#%s)' % (log.url, anchor)) 134 | else: 135 | logger.info('* %s' % log.url) 136 | logger.info('') 137 | for log in logs: 138 | show_log(log) 139 | 140 | logger.info('End of list') 141 | 142 | 143 | def ctloglist(print_json=None): 144 | '''Gather ct-log lists and print the merged log list. 145 | 146 | Args: 147 | print_json(boolean): If True, print merged log list as json data. 148 | Else print as markdown. 149 | ''' 150 | if not print_json: 151 | today = datetime.date.today() 152 | now = datetime.datetime.now() 153 | 154 | logger.info('# Known Certificate Transparency (CT) Logs\n') 155 | logger.verbose('Created with [ctloglist]' 156 | '(https://github.com/theno/ctutlz#ctloglist)\n') 157 | logger.verbose('* [all_logs_list.json](' 158 | 'https://www.gstatic.com/ct/log_list/v2/all_logs_list.json)' 159 | '\n') 160 | logger.info('Version (Date): %s\n' % today) 161 | logger.verbose('Datetime: %s\n' % now) 162 | logger.info('') # formatting: insert empty line 163 | 164 | # all_logs_list.json 165 | 166 | all_dict = download_log_list(URL_ALL_LOGS) 167 | orig_all_dict = dict(all_dict) 168 | set_operator_names(all_dict) 169 | 170 | all_logs = Logs([all_dict]) 171 | 172 | if print_json: 173 | 174 | json_str = json.dumps(orig_all_dict, indent=4, sort_keys=True) 175 | print(json_str) 176 | 177 | else: 178 | show_logs(all_logs, '') 179 | 180 | 181 | def main(): 182 | init_logger() 183 | parser = create_parser() 184 | args = parser.parse_args() 185 | setup_logging(args.loglevel) 186 | logger.debug(args) 187 | if args.print_schema: 188 | print_schema() 189 | else: 190 | ctloglist(args.print_json) 191 | 192 | 193 | if __name__ == '__main__': 194 | main() 195 | -------------------------------------------------------------------------------- /ctutlz/scripts/decompose_cert.py: -------------------------------------------------------------------------------- 1 | '''Decompose an ASN.1 certificate into its components tbsCertificate in DER 2 | format, signatureAlgorithm in DER format, and signatureValue as bytes according 3 | to https://tools.ietf.org/html/rfc5280#section-4.1 4 | ''' 5 | 6 | import argparse 7 | import base64 8 | 9 | import pyasn1_modules.rfc5280 10 | from pyasn1.codec.der.decoder import decode as der_decoder 11 | from pyasn1.codec.der.encoder import encode as der_encoder 12 | from utlz import flo 13 | 14 | from ctutlz._version import __version__ 15 | 16 | 17 | def create_parser(): 18 | '''Create the `ArgumentParser` for the command `decompose-cert`.''' 19 | parser = argparse.ArgumentParser(description=__doc__) 20 | parser.add_argument('-v', '--version', 21 | action='version', 22 | default=False, 23 | version=__version__, 24 | help='print version number') 25 | 26 | req = parser.add_argument_group('required arguments') 27 | req.add_argument('--cert', 28 | metavar='', 29 | dest='cert_filename', 30 | required=True, 31 | help='Certificate in PEM, Base64, or DER format') 32 | 33 | parser.add_argument('--tbscert', 34 | metavar='', 35 | dest='tbscert_filename', 36 | # required=True, 37 | help='write extracted tbsCertificate to this file ' 38 | '(DER encoded)') 39 | parser.add_argument('--sign-algo', 40 | metavar='', 41 | dest='sign_algo_filename', 42 | # required=True, 43 | help='write extracted signatureAlgorithm to this file ' 44 | '(DER encoded)') 45 | parser.add_argument('--signature', 46 | metavar='', 47 | dest='sign_value_filename', 48 | # required=True, 49 | help='write extracted signatureValue to this file') 50 | return parser 51 | 52 | 53 | def cert_der_from_data(cert_raw): 54 | '''Return DER encoded certificate from "raw" certificate data which could be 55 | encoded as PEM, Base64 (B64) or DER. 56 | 57 | Args: 58 | cert_raw(bytes): 59 | 60 | Return: 61 | DER encoded certificate as bytes 62 | ''' 63 | try: 64 | # assume PEM or B64 format (str) 65 | cert_raw_str = cert_raw.decode('ascii') 66 | cert_b64 = cert_raw_str.split( 67 | '-----BEGIN CERTIFICATE-----', 1 68 | )[-1].split( 69 | '-----END CERTIFICATE-----' 70 | )[0].strip() 71 | cert_b64 = ''.join(cert_b64.splitlines()) 72 | cert_der = base64.b64decode(cert_b64) 73 | except UnicodeDecodeError: 74 | # no PEM or B64 format; then, assume cert_raw is in DER format (bytes) 75 | cert_der = cert_raw 76 | return cert_der 77 | 78 | 79 | # FIXME: put functionality to here when, refactoring to optionally read from 80 | # stdin and write to stdout 81 | def decompose(): 82 | pass 83 | 84 | 85 | def main(): 86 | parser = create_parser() 87 | args = parser.parse_args() 88 | 89 | with open(args.cert_filename, 'rb') as fh: 90 | cert_raw = fh.read() 91 | cert_der = cert_der_from_data(cert_raw) 92 | cert, _ = der_decoder(cert_der, 93 | asn1Spec=pyasn1_modules.rfc5280.Certificate()) 94 | 95 | if args.tbscert_filename: 96 | tbscert_der = der_encoder(cert['tbsCertificate']) 97 | with open(args.tbscert_filename, 'wb') as fh: 98 | fh.write(tbscert_der) 99 | 100 | if args.sign_algo_filename: 101 | sign_algo_der = der_encoder(cert['signatureAlgorithm']) 102 | with open(args.sign_algo_filename, 'wb') as fh: 103 | fh.write(sign_algo_der) 104 | 105 | if args.sign_value_filename: 106 | signature_value = cert['signature'].asOctets() 107 | with open(args.sign_value_filename, 'wb') as fh: 108 | fh.write(signature_value) 109 | 110 | 111 | if __name__ == '__main__': 112 | main() 113 | -------------------------------------------------------------------------------- /ctutlz/scripts/verify_scts.py: -------------------------------------------------------------------------------- 1 | '''Verify Signed Certificate Timestamps (SCTs) delivered from one or several 2 | hosts by X.509v3 extension, TLS extension, or OCSP stapling. 3 | 4 | A lot of the functionality originally comes and have been learned from the 5 | script sct-verify.py written by Pier Carlo Chiodi: 6 | https://github.com/pierky/sct-verify/blob/master/sct-verify.py (under GPL) 7 | 8 | He also described the SCT verification steps very well in his blog: 9 | https://blog.pierky.com/certificate-transparency-manually-verify-sct-with-openssl/ 10 | ''' 11 | 12 | import argparse 13 | import logging 14 | import struct 15 | 16 | from utlz import first_paragraph, text_with_newlines 17 | 18 | from ctutlz.tls.handshake import do_handshake 19 | from ctutlz.ctlog import download_log_list, get_log_list, read_log_list 20 | from ctutlz.ctlog import Logs, set_operator_names 21 | from ctutlz.sct.verification import verify_scts 22 | from ctutlz.sct.signature_input import create_signature_input_precert 23 | from ctutlz.sct.signature_input import create_signature_input 24 | from ctutlz.utils.string import to_hex 25 | from ctutlz.utils.logger import VERBOSE, init_logger, setup_logging, logger 26 | from ctutlz._version import __version__ 27 | 28 | 29 | def create_parser(): 30 | parser = argparse.ArgumentParser(description=first_paragraph(__doc__)) 31 | parser.add_argument('hostname', 32 | nargs='+', 33 | help="host name of the server (example: 'ritter.vg')") 34 | parser.add_argument('-v', '--version', 35 | action='version', 36 | default=False, 37 | version=__version__, 38 | help='print version number') 39 | 40 | meg = parser.add_mutually_exclusive_group() 41 | meg.add_argument('--short', 42 | dest='loglevel', 43 | action='store_const', 44 | const=logging.INFO, 45 | default=VERBOSE, # default loglevel if nothing set 46 | help='show short results and warnings/errors only') 47 | meg.add_argument('--debug', 48 | dest='loglevel', 49 | action='store_const', 50 | const=logging.DEBUG, 51 | help='show more for diagnostic purposes') 52 | 53 | meg1 = parser.add_mutually_exclusive_group() 54 | meg1.add_argument('--cert-only', 55 | dest='verification_tasks', 56 | action='store_const', 57 | const=[verify_scts_by_cert], 58 | default=[verify_scts_by_cert, 59 | verify_scts_by_tls, 60 | verify_scts_by_ocsp], 61 | help='only verify SCTs included in the certificate') 62 | meg1.add_argument('--tls-only', 63 | dest='verification_tasks', 64 | action='store_const', 65 | const=[verify_scts_by_tls], 66 | help='only verify SCTs gathered from TLS handshake') 67 | meg1.add_argument('--ocsp-only', 68 | dest='verification_tasks', 69 | action='store_const', 70 | const=[verify_scts_by_ocsp], 71 | help='only verify SCTs gathered via OCSP request') 72 | 73 | meg2 = parser.add_mutually_exclusive_group() 74 | meg2.add_argument('--log-list', 75 | dest='log_list_filename', 76 | metavar='', 77 | help='filename of a log list in JSON format') 78 | meg2.add_argument('--latest-logs', 79 | dest='fetch_ctlogs', 80 | action='store_const', 81 | const=download_log_list, 82 | default=get_log_list, 83 | help='for SCT verification against known CT Logs ' 84 | "(compliant with Chrome's CT policy) " 85 | 'download latest version of ' 86 | 'https://www.gstatic.com/ ' 87 | 'ct/log_list/v2/all_logs_list.json ' 88 | '-- use built-in log list really_all_logs.json ' 89 | 'from 2020-04-05 if --latest-logs or --log-list ' 90 | 'are not set') 91 | return parser 92 | 93 | 94 | def verify_scts_by_cert(res, ctlogs): 95 | ''' 96 | Args: 97 | res(ctutlz.tls.TlsHandshakeResult) 98 | ctlogs([, ...]) 99 | 100 | Return: 101 | [, ...] 102 | ''' 103 | return verify_scts( 104 | ee_cert=res.ee_cert, 105 | scts=res.scts_by_cert, 106 | logs=ctlogs, 107 | issuer_cert=res.issuer_cert, 108 | more_issuer_cert_candidates=res.more_issuer_cert_candidates, 109 | sign_input_func=create_signature_input_precert) 110 | 111 | 112 | def verify_scts_by_tls(res, ctlogs): 113 | ''' 114 | Args: 115 | res(ctutlz.tls.TlsHandshakeResult) 116 | ctlogs([, ...]) 117 | 118 | Return: 119 | [, ...] 120 | ''' 121 | return verify_scts( 122 | ee_cert=res.ee_cert, 123 | scts=res.scts_by_tls, 124 | logs=ctlogs, 125 | issuer_cert=None, 126 | more_issuer_cert_candidates=None, 127 | sign_input_func=create_signature_input) 128 | 129 | 130 | def verify_scts_by_ocsp(res, ctlogs): 131 | ''' 132 | Args: 133 | res(ctutlz.tls.TlsHandshakeResult) 134 | ctlogs([, ...]) 135 | 136 | Return: 137 | [, ...] 138 | ''' 139 | return verify_scts( 140 | ee_cert=res.ee_cert, 141 | scts=res.scts_by_ocsp, 142 | logs=ctlogs, 143 | issuer_cert=None, 144 | more_issuer_cert_candidates=None, 145 | sign_input_func=create_signature_input) 146 | 147 | 148 | # for more convenient command output 149 | verify_scts_by_cert.__name__ = 'SCTs by Certificate' 150 | verify_scts_by_tls.__name__ = 'SCTs by TLS' 151 | verify_scts_by_ocsp.__name__ = 'SCTs by OCSP' 152 | 153 | 154 | def show_signature_verbose(signature): 155 | '''Print out signature as hex string to logger.verbose. 156 | 157 | Args: 158 | signature(bytes) 159 | ''' 160 | sig_offset = 0 161 | while sig_offset < len(signature): 162 | if len(signature) - sig_offset > 16: 163 | bytes_to_read = 16 164 | else: 165 | bytes_to_read = len(signature) - sig_offset 166 | sig_bytes = struct.unpack_from('!%ds' % bytes_to_read, 167 | signature, 168 | sig_offset)[0] 169 | if sig_offset == 0: 170 | logger.verbose('Signature : %s' % to_hex(sig_bytes)) 171 | else: 172 | logger.verbose(' %s' % to_hex(sig_bytes)) 173 | sig_offset = sig_offset + bytes_to_read 174 | 175 | 176 | def show_verification(verification): 177 | ''' 178 | Args: 179 | verification(ctutlz.sct.verification.SctVerificationResult) 180 | ''' 181 | sct = verification.sct 182 | 183 | sct_log_id1, sct_log_id2 = [to_hex(val) 184 | for val 185 | in struct.unpack("!16s16s", sct.log_id.tdf)] 186 | logger.info('```') 187 | logger.verbose('=' * 59) 188 | logger.verbose('Version : %s' % sct.version_hex) 189 | logger.verbose('LogID : %s' % sct_log_id1) 190 | logger.verbose(' %s' % sct_log_id2) 191 | logger.info('LogID b64 : %s' % sct.log_id_b64) 192 | logger.verbose('Timestamp : %s (%s)' % (sct.timestamp, sct.timestamp_hex)) 193 | logger.verbose( 194 | 'Extensions: %d (%s)' % (sct.extensions_len, sct.extensions_len_hex)) 195 | logger.verbose('Algorithms: %s/%s (hash/sign)' % (sct.signature_alg_hash_hex, sct.signature_algorithm_signature)) 196 | 197 | show_signature_verbose(sct.signature) 198 | prefix = 'Sign. b64 : ' 199 | logger.info(prefix + text_with_newlines(sct.signature_b64, line_length=16*3, 200 | newline='\n' + ' '*len(prefix))) 201 | 202 | logger.verbose('--') # visual gap between sct infos and verification result 203 | 204 | log = verification.log 205 | if log is None: 206 | logger.info('Log not found\n') 207 | else: 208 | logger.info('Log found : %s' % log.description) 209 | logger.verbose('Operator : %s' % log.operated_by['name']) 210 | logger.info('Chrome : %s' % log.scts_accepted_by_chrome) 211 | 212 | if verification.verified: 213 | logger.info('Result : Verified OK') 214 | else: 215 | logger.info('Result : Verification Failure') 216 | 217 | logger.info('```\n') 218 | 219 | 220 | def scrape_and_verify_scts(hostname, verification_tasks, ctlogs): 221 | logger.info('# %s\n' % hostname) 222 | 223 | res = do_handshake(hostname, 443, 224 | scts_tls=(verify_scts_by_tls in verification_tasks), 225 | scts_ocsp=(verify_scts_by_ocsp in verification_tasks)) 226 | if res.ee_cert_der: 227 | logger.debug('got certificate\n') 228 | if res.ee_cert.is_ev_cert: 229 | logger.info('* EV cert') 230 | else: 231 | logger.info('* no EV cert') 232 | if res.ee_cert.is_letsencrypt_cert: 233 | logger.info("* issued by Let's Encrypt\n") 234 | else: 235 | logger.info("* not issued by Let's Encrypt\n") 236 | 237 | if res.err: 238 | logger.warning(res.err) 239 | else: 240 | for verification_task in verification_tasks: 241 | logger.info('## %s\n' % verification_task.__name__) 242 | verifications = verification_task(res, ctlogs) 243 | if verifications: 244 | for verification in verifications: 245 | show_verification(verification) 246 | elif res.ee_cert_der is not None: 247 | logger.info('no SCTs\n') 248 | 249 | 250 | def main(): 251 | init_logger() 252 | parser = create_parser() 253 | args = parser.parse_args() 254 | setup_logging(args.loglevel) 255 | logger.debug(args) 256 | 257 | # set ctlogs, type: [, ...] 258 | all_dict = args.fetch_ctlogs() # call download_log_list() to populate the list 259 | set_operator_names(all_dict) 260 | ctlogs = Logs([all_dict]) 261 | if args.log_list_filename: 262 | logs_dict = read_log_list(args.log_list_filename) 263 | set_operator_names(logs_dict) 264 | ctlogs = Logs(logs_dict['logs']) 265 | 266 | for host in args.hostname: 267 | scrape_and_verify_scts(host, args.verification_tasks, ctlogs) 268 | 269 | 270 | if __name__ == '__main__': 271 | # when calling `verify-scts` directly from source as pointed out in the 272 | # README.md (section Devel-Commands) the c-code part needs to be compiled, 273 | # else the import of the c-module `ctutlz.tls.handshake_openssl` would fail. 274 | import ctutlz.tls.handshake_openssl_build 275 | ctutlz.tls.handshake_openssl_build.compile() 276 | 277 | main() 278 | -------------------------------------------------------------------------------- /ctutlz/sct/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/ctutlz/sct/__init__.py -------------------------------------------------------------------------------- /ctutlz/sct/ee_cert.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | import OpenSSL 4 | import pyasn1.error 5 | from pyasn1_modules import rfc5280 6 | from pyasn1.type.univ import ObjectIdentifier, Sequence 7 | from pyasn1.codec.der.encoder import encode as der_encoder 8 | from pyasn1.codec.der.decoder import decode as der_decoder 9 | 10 | from utlz import namedtuple 11 | 12 | from ctutlz.utils.string import string_without_prefix 13 | from ctutlz.utils.encoding import sha256_digest 14 | 15 | 16 | # https://hg.mozilla.org/mozilla-central/file/tip/security/certverifier/ExtendedValidation.cpp 17 | EV_OIDs = ['1.2.392.200091.100.721.1', 18 | '1.3.6.1.4.1.6334.1.100.1', 19 | '2.16.756.1.89.1.2.1.1', 20 | '1.3.6.1.4.1.23223.1.1.1', 21 | '1.3.6.1.4.1.23223.1.1.1', 22 | '1.3.6.1.4.1.23223.1.1.1', 23 | '2.16.840.1.113733.1.7.23.6', 24 | '1.3.6.1.4.1.14370.1.6', 25 | '2.16.840.1.113733.1.7.48.1', 26 | '2.16.840.1.114404.1.1.2.4.1', 27 | '2.16.840.1.114404.1.1.2.4.1', 28 | '2.16.840.1.114404.1.1.2.4.1', 29 | '1.3.6.1.4.1.6449.1.2.1.5.1', 30 | '1.3.6.1.4.1.6449.1.2.1.5.1', 31 | '1.3.6.1.4.1.6449.1.2.1.5.1', 32 | '2.16.840.1.114413.1.7.23.3', 33 | '2.16.840.1.114413.1.7.23.3', 34 | '2.16.840.1.114414.1.7.23.3', 35 | '2.16.840.1.114414.1.7.23.3', 36 | '2.16.840.1.114412.2.1', 37 | '1.3.6.1.4.1.8024.0.2.100.1.2', 38 | '1.3.6.1.4.1.782.1.2.1.8.1', 39 | '2.16.840.1.114028.10.1.2', 40 | '1.3.6.1.4.1.4146.1.1', 41 | '1.3.6.1.4.1.4146.1.1', 42 | '1.3.6.1.4.1.4146.1.1', 43 | '2.16.578.1.26.1.3.3', 44 | '1.3.6.1.4.1.22234.2.5.2.3.1', 45 | '1.3.6.1.4.1.17326.10.14.2.1.2', 46 | '1.3.6.1.4.1.17326.10.8.12.1.2', 47 | '1.3.6.1.4.1.34697.2.1', 48 | '1.3.6.1.4.1.34697.2.2', 49 | '1.3.6.1.4.1.34697.2.3', 50 | '1.3.6.1.4.1.34697.2.4', 51 | '1.2.616.1.113527.2.5.1.1', 52 | '1.2.616.1.113527.2.5.1.1', 53 | '1.3.6.1.4.1.14777.6.1.1', 54 | '1.3.6.1.4.1.14777.6.1.2', 55 | '1.3.6.1.4.1.7879.13.24.1', 56 | '1.3.6.1.4.1.40869.1.1.22.3', 57 | '1.3.6.1.4.1.4788.2.202.1', 58 | '2.16.840.1.113733.1.7.23.6', 59 | '1.3.6.1.4.1.14370.1.6', 60 | '2.16.840.1.113733.1.7.48.1', 61 | '1.3.6.1.4.1.13177.10.1.3.10', 62 | '1.3.6.1.4.1.40869.1.1.22.3', 63 | '2.16.792.3.0.4.1.1.4', 64 | '1.3.159.1.17.1', 65 | '1.3.6.1.4.1.36305.2', 66 | '1.3.6.1.4.1.36305.2', 67 | '2.16.840.1.114412.2.1', 68 | '2.16.840.1.114412.2.1', 69 | '2.16.840.1.114412.2.1', 70 | '2.16.840.1.114412.2.1', 71 | '2.16.840.1.114412.2.1', 72 | '1.3.6.1.4.1.8024.0.2.100.1.2', 73 | '1.3.6.1.4.1.6449.1.2.1.5.1', 74 | '1.3.6.1.4.1.6449.1.2.1.5.1', 75 | '1.3.6.1.4.1.6449.1.2.1.5.1', 76 | '1.3.6.1.4.1.4146.1.1', 77 | '2.16.840.1.114028.10.1.2', 78 | '2.16.528.1.1003.1.2.7', 79 | '2.16.840.1.114028.10.1.2', 80 | '2.16.840.1.114028.10.1.2', 81 | '2.16.156.112554.3', 82 | '1.3.6.1.4.1.36305.2', 83 | '1.3.6.1.4.1.36305.2', 84 | '1.2.392.200091.100.721.1', 85 | '2.16.756.5.14.7.4.8', 86 | '1.3.6.1.4.1.22234.3.5.3.1', 87 | '1.3.6.1.4.1.22234.3.5.3.2', 88 | '1.3.6.1.4.1.22234.2.14.3.11', 89 | '1.3.6.1.4.1.22234.2.14.3.11', 90 | '1.3.6.1.4.1.22234.2.14.3.11', 91 | '2.16.840.1.113733.1.7.23.6', 92 | '2.23.140.1.1', 93 | '2.23.140.1.1', 94 | '2.23.140.1.1', 95 | '2.23.140.1.1', 96 | '2.23.140.1.1', 97 | '1.3.171.1.1.10.5.2'] 98 | 99 | 100 | def is_ev_cert(ee_cert): 101 | '''Return True if ee_cert is an extended validation certificate, else False. 102 | 103 | Args: 104 | ee_cert (EndEntityCert) 105 | ''' 106 | oids = [] 107 | oid_certificate_policies = ObjectIdentifier('2.5.29.32') 108 | 109 | all_extensions = ee_cert.tbscert.pyasn1['extensions'] 110 | if all_extensions is not None: 111 | policy_extensions = [ext 112 | for ext 113 | in all_extensions 114 | if ext['extnID'] == oid_certificate_policies] 115 | if len(policy_extensions) > 0: 116 | policy_extension = policy_extensions[0] 117 | sequence_der = policy_extension['extnValue'] # type: Sequence() 118 | try: 119 | sequence, _ = der_decoder(sequence_der, Sequence()) 120 | except pyasn1.error.PyAsn1Error: 121 | sequence = [] # invalid encoded certificate policy extension 122 | 123 | for idx in range(len(sequence)): 124 | inner_sequence = sequence.getComponentByPosition(idx) 125 | oid = inner_sequence.getComponentByPosition(0) 126 | oids.append(str(oid)) 127 | 128 | intersection = list(set(oids) & set(EV_OIDs)) 129 | return intersection != [] 130 | 131 | 132 | def is_letsencrypt_cert(ee_cert): 133 | '''Return True if ee_cert was issued by Let's Encrypt. 134 | 135 | Args: 136 | ee_cert (EndEntityCert) 137 | ''' 138 | organization_name_oid = ObjectIdentifier(value='2.5.4.10') 139 | issuer = ee_cert.tbscert.pyasn1['issuer'] 140 | if issuer: 141 | for rdn in issuer['rdnSequence']: 142 | for item in rdn: 143 | if item.getComponentByName('type') == organization_name_oid: 144 | organisation_name = str(item.getComponentByName('value')) 145 | organisation_name = string_without_prefix('\x13\r', 146 | organisation_name) 147 | if organisation_name == "Let's Encrypt": 148 | return True 149 | return False 150 | 151 | 152 | def pyasn1_certificate_from_der(cert_der): 153 | '''Return pyasn1_modules.rfc5280.Certificate instance parsed from cert_der. 154 | ''' 155 | cert, _ = der_decoder(cert_der, asn1Spec=rfc5280.Certificate()) 156 | return cert 157 | 158 | 159 | def copy_pyasn1_instance(instance): 160 | der = der_encoder(instance) 161 | copy, _ = der_decoder(der, rfc5280.TBSCertificate()) 162 | return copy 163 | 164 | 165 | def pyopenssl_certificate_from_der(cert_der): 166 | '''Return OpenSSL.crypto.X509 instance parsed from cert_der. 167 | ''' 168 | cert = OpenSSL.crypto.load_certificate(type=OpenSSL.crypto.FILETYPE_ASN1, 169 | buffer=cert_der) 170 | return cert 171 | 172 | 173 | def tbscert_without_sctlist(tbscert): 174 | '''Return pyasn1_modules.rfc2580.TBSCertificate instance `cert_pyasn1` 175 | without sctlist extension (OID 1.3.6.1.4.1.11129.2.4.2). 176 | ''' 177 | sctlist_oid = ObjectIdentifier(value='1.3.6.1.4.1.11129.2.4.2') 178 | extensions = tbscert['extensions'] 179 | without_sctlist = extensions.subtype() 180 | for extension in extensions: 181 | if extension['extnID'] != sctlist_oid: 182 | without_sctlist.append(extension) 183 | copy = copy_pyasn1_instance(tbscert) 184 | copy['extensions'] = without_sctlist 185 | return copy 186 | 187 | 188 | def tbscert_without_ct_extensions(tbscert): 189 | '''Return pyasn1_modules.rfc5280.TBSCertificate instance `cert_pyasn1` 190 | without sctlist extension (OID 1.3.6.1.4.1.11129.2.4.3) and 191 | poison extension (OID 1.3.6.1.4.1.11129.2.4.2), if any. 192 | ''' 193 | sctlist_oid = ObjectIdentifier(value='1.3.6.1.4.1.11129.2.4.2') 194 | poison_oid = ObjectIdentifier(value='1.3.6.1.4.1.11129.2.4.3') 195 | ct_oids = [sctlist_oid, poison_oid] 196 | 197 | extensions = tbscert['extensions'] 198 | without_ct_extensions = extensions.subtype() 199 | for extension in extensions: 200 | if extension['extnID'] not in ct_oids: 201 | without_ct_extensions.append(extension) 202 | copy = copy_pyasn1_instance(tbscert) 203 | copy['extensions'] = without_ct_extensions 204 | return copy 205 | 206 | 207 | TbsCert = namedtuple( 208 | typename='TbsCert', 209 | field_names=[ 210 | 'pyasn1', 211 | ], 212 | lazy_vals={ 213 | 'der': lambda self: der_encoder(self.pyasn1), 214 | 'len': lambda self: len(self.der), 215 | 'lens': lambda self: struct.unpack('!4B', struct.pack('!I', self.len)), 216 | # cf. https://tools.ietf.org/html/rfc6962#section-3.2 <1..2^24-1> 217 | 'len1': lambda self: self.lens[1], 218 | 'len2': lambda self: self.lens[2], 219 | 'len3': lambda self: self.lens[3], 220 | 221 | 'without_ct_extensions': 222 | lambda self: TbsCert(tbscert_without_ct_extensions(self.pyasn1)), 223 | } 224 | ) 225 | 226 | 227 | EndEntityCert = namedtuple( 228 | typename='EndEntityCert', 229 | field_names=[ 230 | 'der', 231 | 'issuer_cert=None', # type: der 232 | ], 233 | lazy_vals={ 234 | 'len': lambda self: len(self.der), 235 | # cf. https://tools.ietf.org/html/rfc6962#section-3.2 <1..2^24-1> 236 | 'lens': lambda self: struct.unpack('!4B', struct.pack('!I', self.len)), 237 | 'len1': lambda self: self.lens[1], 238 | 'len2': lambda self: self.lens[2], 239 | 'len3': lambda self: self.lens[3], 240 | 241 | 'pyasn1': lambda self: pyasn1_certificate_from_der(self.der), 242 | 'tbscert': lambda self: TbsCert(self.pyasn1['tbsCertificate']), 243 | 244 | # FIXME: YAGNI? 245 | 'pyopenssl': lambda self: pyopenssl_certificate_from_der(self.der), 246 | 247 | 'is_ev_cert': lambda self: is_ev_cert(self), 248 | 'is_letsencrypt_cert': lambda self: is_letsencrypt_cert(self), 249 | } 250 | ) 251 | 252 | 253 | IssuerCert = namedtuple( 254 | typename='IssuerCert', 255 | field_names=[ 256 | 'der', 257 | ], 258 | lazy_vals={ 259 | 'pyasn1': lambda self: pyasn1_certificate_from_der(self.der), 260 | 'pubkey_pyasn1': lambda self: 261 | self.pyasn1['tbsCertificate']['subjectPublicKeyInfo'], 262 | 'pubkey_der': lambda self: der_encoder(self.pubkey_pyasn1), 263 | 'pubkey_hash': lambda self: sha256_digest(self.pubkey_der), 264 | } 265 | ) 266 | -------------------------------------------------------------------------------- /ctutlz/sct/signature_input.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from functools import reduce 3 | 4 | from utlz import flo 5 | 6 | 7 | def create_signature_input(ee_cert, sct, *_, **__): 8 | # cf. https://tools.ietf.org/html/rfc6962#section-3.2 9 | 10 | signature_type = 0 # 0 means certificate_timestamp 11 | entry_type = 0 # 0: ASN.1Cert, 1: PreCert 12 | 13 | def reduce_func(accum_value, current): 14 | fmt = accum_value[0] + current[0] 15 | values = accum_value[1] + (current[1], ) 16 | return fmt, values 17 | 18 | initializer = ('!', ()) 19 | 20 | # fmt = '!BBQh...', values = [, , ...] 21 | fmt, values = reduce(reduce_func, [ 22 | ('B', sct.version.val), 23 | ('B', signature_type), 24 | ('Q', sct.timestamp), 25 | ('h', entry_type), 26 | 27 | # signed_entry 28 | ('B', ee_cert.len1), 29 | ('B', ee_cert.len2), 30 | ('B', ee_cert.len3), 31 | (flo('{ee_cert.len}s'), ee_cert.der), 32 | 33 | ('h', sct.extensions_len), 34 | ], initializer) 35 | return struct.pack(fmt, *values) 36 | 37 | 38 | def create_signature_input_precert(ee_cert, sct, issuer_cert): 39 | # cf. https://tools.ietf.org/html/rfc6962#section-3.2 40 | 41 | signature_type = 0 # 0 means certificate_timestamp 42 | entry_type = 1 # 0: ASN.1Cert, 1: PreCert 43 | 44 | tbscert = ee_cert.tbscert.without_ct_extensions 45 | 46 | def reduce_func(accum_value, current): 47 | fmt = accum_value[0] + current[0] 48 | values = accum_value[1] + (current[1], ) 49 | return fmt, values 50 | 51 | initializer = ('!', ()) 52 | 53 | # fmt = '!BBQh...', values = [, , ...] 54 | fmt, values = reduce(reduce_func, [ 55 | ('B', sct.version.val), 56 | ('B', signature_type), 57 | ('Q', sct.timestamp), 58 | ('h', entry_type), 59 | 60 | # signed_entry 61 | 62 | # issuer_key_hash[32] 63 | ('32s', issuer_cert.pubkey_hash), 64 | 65 | # tbs_certificate (rfc6962, page 12) 66 | # * DER encoded TBSCertificate of the ee_cert 67 | # * without SCT extension 68 | ('B', tbscert.len1), 69 | ('B', tbscert.len2), 70 | ('B', tbscert.len3), 71 | (flo('{tbscert.len}s'), tbscert.der), 72 | 73 | ('h', sct.extensions_len), 74 | ], initializer) 75 | return struct.pack(fmt, *values) 76 | -------------------------------------------------------------------------------- /ctutlz/sct/verification.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from cryptography.hazmat.backends.openssl.backend import backend 4 | from cryptography.hazmat.primitives import serialization 5 | from cryptography.hazmat.primitives.asymmetric import ec, dsa, rsa 6 | from OpenSSL.crypto import verify, X509, PKey, Error as OpenSSL_crypto_Error 7 | 8 | 9 | SctVerificationResult = collections.namedtuple( 10 | typename='SctVerificationResult', 11 | field_names=[ 12 | 'ee_cert', # DER format 13 | 'sct', # type: Sct 14 | 'log', # type: Log 15 | 'verified', # True or False 16 | ] 17 | ) 18 | 19 | 20 | def find_log(sct, logs): 21 | for log in logs: 22 | if log.log_id_der == sct.log_id.tdf: 23 | return log 24 | return None 25 | 26 | 27 | def pkey_from_cryptography_key(crypto_key): 28 | ''' 29 | Modified version of `OpenSSL.crypto.PKey.from_cryptography_key()` of 30 | PyOpenSSL which also accepts EC Keys 31 | (cf. https://github.com/pyca/pyopenssl/pull/636). 32 | ''' 33 | pkey = PKey() 34 | if not isinstance(crypto_key, (rsa.RSAPublicKey, rsa.RSAPrivateKey, 35 | dsa.DSAPublicKey, dsa.DSAPrivateKey, 36 | ec.EllipticCurvePublicKey, 37 | ec.EllipticCurvePrivateKey)): 38 | raise TypeError("Unsupported key type") 39 | 40 | pkey._pkey = crypto_key._evp_pkey 41 | if isinstance(crypto_key, (rsa.RSAPublicKey, dsa.DSAPublicKey, 42 | ec.EllipticCurvePublicKey)): 43 | pkey._only_public = True 44 | pkey._initialized = True 45 | return pkey 46 | 47 | 48 | def verify_signature(signature_input, signature, 49 | pubkey_pem, digest_algo='sha256'): 50 | '''Verify if `signature` over `signature_input` was created using 51 | `digest_algo` by the private key of the `pubkey_pem`. 52 | 53 | A signature is the private key encrypted hash over the signature input data. 54 | 55 | Args: 56 | signature_input(bytes): signed data 57 | signature(bytes): 58 | pubkey_pem(str): PEM formatted pubkey 59 | digest_algo(str): name of the used digest hash algorithm 60 | (default: 'sha256') 61 | 62 | Return: 63 | True, if signature could be verified 64 | False, else 65 | ''' 66 | cryptography_key = serialization.load_pem_public_key(pubkey_pem, backend) 67 | pkey = pkey_from_cryptography_key(cryptography_key) 68 | 69 | auxiliary_cert = X509() 70 | auxiliary_cert.set_pubkey(pkey) 71 | 72 | try: 73 | verify(cert=auxiliary_cert, signature=signature, 74 | data=signature_input, digest=digest_algo) 75 | except OpenSSL_crypto_Error: 76 | return False 77 | 78 | return True 79 | 80 | 81 | def verify_sct(ee_cert, sct, logs, 82 | issuer_cert, more_issuer_cert_candidates, 83 | sign_input_func): 84 | log = find_log(sct, logs) 85 | if log: 86 | verified = verify_signature( 87 | signature_input=sign_input_func(ee_cert, sct, issuer_cert), 88 | signature=sct.signature, 89 | pubkey_pem=log.pubkey.encode('ascii') 90 | ) 91 | 92 | if not verified and more_issuer_cert_candidates is not None: 93 | # Sometimes the certificate chain is disordered (this is only 94 | # relevant in case of of sct-by-cert, because the signature input 95 | # then also depends by the issuer key hash) 96 | 97 | # Try to verify against all other certs of the chain (candidates). 98 | for issuer_cert in more_issuer_cert_candidates: 99 | verified = verify_signature( 100 | signature_input=sign_input_func(ee_cert, sct, issuer_cert), 101 | signature=sct.signature, 102 | pubkey_pem=log.pubkey.encode('ascii') 103 | ) 104 | if verified: 105 | break 106 | 107 | # # TODO DEVEL 108 | # print('CREATE TEST DATA FOR test_verify_sct()') 109 | # open('signature_input.bin', 'wb').write(sign_input_func(ee_cert, 110 | # sct, 111 | # issuer_cert)) 112 | # open('signature.der', 'wb').write(sct.signature) 113 | # open('pubkey.pem', 'wb').write(log.pubkey.encode('ascii')) 114 | 115 | return SctVerificationResult(ee_cert, sct, log, verified) 116 | return SctVerificationResult(ee_cert, sct, log=None, verified=False) 117 | 118 | 119 | def verify_scts(ee_cert, scts, logs, 120 | issuer_cert, more_issuer_cert_candidates, 121 | sign_input_func): 122 | if scts: 123 | return [verify_sct(ee_cert, sct, logs, 124 | issuer_cert, more_issuer_cert_candidates, 125 | sign_input_func) 126 | for sct 127 | in scts] 128 | return [] 129 | -------------------------------------------------------------------------------- /ctutlz/tls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/ctutlz/tls/__init__.py -------------------------------------------------------------------------------- /ctutlz/tls/handshake.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import socket 3 | import struct 4 | from functools import reduce 5 | 6 | import certifi 7 | import OpenSSL 8 | import pyasn1_modules.rfc2560 9 | import pyasn1_modules.rfc5280 10 | from pyasn1.codec import ber 11 | from pyasn1.codec.der.decoder import decode as der_decoder 12 | from pyasn1.type.univ import ObjectIdentifier, OctetString, Sequence 13 | from utlz import flo, namedtuple 14 | 15 | from ctutlz.rfc6962 import SignedCertificateTimestamp 16 | from ctutlz.sct.ee_cert import EndEntityCert, IssuerCert 17 | from ctutlz.tls.sctlist import SignedCertificateTimestampList, TlsExtension18 18 | 19 | 20 | def scts_from_cert(cert_der): 21 | '''Return list of SCTs of the SCTList SAN extension of the certificate. 22 | 23 | Args: 24 | cert_der(bytes): DER encoded ASN.1 Certificate 25 | 26 | Return: 27 | [, ...] 28 | ''' 29 | cert, _ = der_decoder( 30 | cert_der, asn1Spec=pyasn1_modules.rfc5280.Certificate()) 31 | sctlist_oid = ObjectIdentifier(value='1.3.6.1.4.1.11129.2.4.2') 32 | exts = [] 33 | if 'extensions' in cert['tbsCertificate'].keys(): 34 | exts = [extension 35 | for extension 36 | in cert['tbsCertificate']['extensions'] 37 | if extension['extnID'] == sctlist_oid] 38 | 39 | if len(exts) != 0: 40 | extension_sctlist = exts[0] 41 | os_inner_der = extension_sctlist['extnValue'] # type: OctetString() 42 | os_inner, _ = der_decoder(os_inner_der, OctetString()) 43 | sctlist_hex = os_inner.prettyPrint().split('0x')[-1] 44 | sctlist_der = binascii.unhexlify(sctlist_hex) 45 | 46 | sctlist = SignedCertificateTimestampList(sctlist_der) 47 | return [SignedCertificateTimestamp(entry.sct_der) 48 | for entry 49 | in sctlist.sct_list] 50 | return [] 51 | 52 | 53 | def sctlist_hex_from_ocsp_pretty_print(ocsp_resp): 54 | sctlist_hex = None 55 | splitted = ocsp_resp.split('=1.3.6.1.4.1.11129.2.4.5', 1) 56 | if len(splitted) > 1: 57 | _, after = splitted 58 | _, sctlist_hex_with_rest = after.split('=0x', 1) 59 | sctlist_hex, _ = sctlist_hex_with_rest.split('\n', 1) 60 | return sctlist_hex 61 | 62 | 63 | def scts_from_ocsp_resp(ocsp_resp_der): 64 | '''Return list of SCTs of the OCSP status response. 65 | 66 | Args: 67 | ocsp_resp_der(bytes): DER encoded OCSP status response 68 | 69 | Return: 70 | [, ...] 71 | ''' 72 | if ocsp_resp_der: 73 | ocsp_resp, _ = der_decoder( 74 | ocsp_resp_der, asn1Spec=pyasn1_modules.rfc2560.OCSPResponse()) 75 | 76 | response_bytes = ocsp_resp.getComponentByName('responseBytes') 77 | if response_bytes is not None: 78 | # os: octet string 79 | response_os = response_bytes.getComponentByName('response') 80 | 81 | der_decoder.defaultErrorState = ber.decoder.stDumpRawValue 82 | response, _ = der_decoder(response_os, Sequence()) 83 | 84 | sctlist_os_hex = sctlist_hex_from_ocsp_pretty_print( 85 | response.prettyPrint()) 86 | 87 | if sctlist_os_hex: 88 | sctlist_os_der = binascii.unhexlify(sctlist_os_hex) 89 | sctlist_os, _ = der_decoder(sctlist_os_der, OctetString()) 90 | sctlist_hex = sctlist_os.prettyPrint().split('0x')[-1] 91 | sctlist_der = binascii.unhexlify(sctlist_hex) 92 | 93 | sctlist = SignedCertificateTimestampList(sctlist_der) 94 | return [SignedCertificateTimestamp(entry.sct_der) 95 | for entry 96 | in sctlist.sct_list] 97 | return [] 98 | 99 | 100 | def scts_from_tls_ext_18(tls_ext_18_tdf): 101 | '''Return list of SCTs of the TLS extension 18 server reply. 102 | 103 | Args: 104 | tls_ext_18_tdf(bytes): TDF encoded TLS extension 18 server reply. 105 | 106 | Return: 107 | [, ...] 108 | ''' 109 | scts = [] 110 | 111 | if tls_ext_18_tdf: 112 | tls_extension_18 = TlsExtension18(tls_ext_18_tdf) 113 | sct_list = tls_extension_18.sct_list 114 | 115 | scts = [SignedCertificateTimestamp(entry.sct_der) 116 | for entry 117 | in sct_list] 118 | 119 | return scts 120 | 121 | 122 | TlsHandshakeResult = namedtuple( 123 | typename='TlsHandshakeResult', 124 | field_names=[ 125 | 'ee_cert_der', # (bytes) 126 | 'issuer_cert_der', # (bytes) 127 | 'more_issuer_cert_der_candidates', # [(bytes), ...] 128 | 'ocsp_resp_der', # (bytes) 129 | 'tls_ext_18_tdf', # (bytes) 130 | 'err', # (str) 131 | ], 132 | lazy_vals={ 133 | 'ee_cert': lambda self: EndEntityCert(self.ee_cert_der), 134 | 'issuer_cert': lambda self: IssuerCert(self.issuer_cert_der), 135 | 'more_issuer_cert_candidates': lambda self: [ 136 | IssuerCert(cert_der) 137 | for cert_der 138 | in self.more_issuer_cert_der_candidates], 139 | 140 | 'scts_by_cert': lambda self: scts_from_cert(self.ee_cert_der), 141 | 'scts_by_ocsp': lambda self: scts_from_ocsp_resp(self.ocsp_resp_der), 142 | 'scts_by_tls': lambda self: scts_from_tls_ext_18(self.tls_ext_18_tdf), 143 | } 144 | ) 145 | 146 | 147 | def create_context(scts_tls, scts_ocsp, timeout): 148 | ''' 149 | Args: 150 | scts_tls: If True, register callback for TSL extension 18 (for SCTs) 151 | scts_ocsp: If True, register callback for OCSP-response (for SCTs) 152 | timeout(int): timeout in seconds 153 | ''' 154 | 155 | def verify_callback(conn, cert, errnum, depth, ok): 156 | '''Dummy callback.''' 157 | return 1 # True 158 | 159 | ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD) 160 | 161 | ctx.set_verify(OpenSSL.SSL.VERIFY_PEER, verify_callback) 162 | ca_filename = certifi.where() 163 | ctx.load_verify_locations(ca_filename) 164 | 165 | ctx.tls_ext_18_tdf = None 166 | if scts_tls: 167 | from ctutlz.tls.handshake_openssl import ffi, lib 168 | 169 | # this annotation makes the callback function available at 170 | # lib.serverinfo_cli_parse_cb() of type cdef so it can be used as 171 | # argument for the call of lib.SSL_CTX_add_client_custom_ext() 172 | @ffi.def_extern() 173 | def serverinfo_cli_parse_cb(ssl, ext_type, _in, inlen, al, arg): 174 | if ext_type == 18: 175 | 176 | def reduce_func(accum_value, current): 177 | fmt = accum_value[0] + current[0] 178 | values = accum_value[1] + (current[1], ) 179 | return fmt, values 180 | 181 | initializer = ('!', ()) 182 | fmt, values = reduce(reduce_func, [ 183 | ('H', ext_type), 184 | ('H', inlen), 185 | (flo('{inlen}s'), bytes(ffi.buffer(_in, inlen))), 186 | ], initializer) 187 | ctx.tls_ext_18_tdf = struct.pack(fmt, *values) 188 | return 1 # True 189 | 190 | # register callback for TLS extension result into the SSL context 191 | # created with PyOpenSSL, using OpenSSL "directly" 192 | if not lib.SSL_CTX_add_client_custom_ext(ffi.cast('struct ssl_ctx_st *', 193 | ctx._context), 194 | 18, 195 | ffi.NULL, ffi.NULL, ffi.NULL, 196 | lib.serverinfo_cli_parse_cb, 197 | ffi.NULL): 198 | import sys 199 | sys.stderr.write('Unable to add custom extension 18\n') 200 | lib.ERR_print_errors_fp(sys.stderr) 201 | sys.exit(1) 202 | 203 | ctx.ocsp_resp_der = None 204 | if scts_ocsp: 205 | 206 | def ocsp_client_callback(connection, ocsp_data, data): 207 | ctx.ocsp_resp_der = ocsp_data 208 | return True 209 | 210 | ctx.set_ocsp_client_callback(ocsp_client_callback, data=None) 211 | 212 | ctx.set_timeout(timeout) 213 | 214 | return ctx 215 | 216 | 217 | def create_socket(ctx): 218 | ''' 219 | Args: 220 | ctx(OpenSSL.SSL.Context): OpenSSL context object 221 | ''' 222 | raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 223 | return OpenSSL.SSL.Connection(context=ctx, socket=raw_sock) 224 | 225 | 226 | def do_handshake(domain, port=443, scts_tls=True, scts_ocsp=True, timeout=5): 227 | ''' 228 | Args: 229 | domain: string with domain name, 230 | for example: 'ritter.vg', or 'www.ritter.vg' 231 | scts_tls: If True, register callback for TSL extension 18 (for SCTs) 232 | scts_ocsp: If True, register callback for OCSP-response (for SCTs) 233 | timeout(int): timeout in seconds 234 | ''' 235 | ctx = create_context(scts_tls, scts_ocsp, timeout) 236 | sock = create_socket(ctx) 237 | sock.set_tlsext_host_name(domain.encode()) 238 | sock.request_ocsp() 239 | sock.set_tlsext_host_name(domain.encode()) 240 | 241 | issuer_cert_x509 = None 242 | more_issuer_cert_x509_candidates = [] 243 | ee_cert_x509 = None 244 | ocsp_resp_der = None 245 | tls_ext_18_tdf = None 246 | err = '' 247 | 248 | try: 249 | sock.connect((domain, port)) 250 | sock.do_handshake() 251 | 252 | # type: OpenSSL.crypto.X509; ee: end entity 253 | ee_cert_x509 = sock.get_peer_certificate() 254 | # type: [OpenSSL.crypto.X509, ...] 255 | chain_x509s = sock.get_peer_cert_chain() 256 | if len(chain_x509s) > 1: 257 | issuer_cert_x509 = chain_x509s[1] 258 | more_issuer_cert_x509_candidates = [ee_cert_x509] + chain_x509s 259 | print("debug: len(chain_x509s) = %d" % len(chain_x509s)) 260 | 261 | ctx = sock.get_context() 262 | if scts_tls: 263 | if ctx.tls_ext_18_tdf: 264 | tls_ext_18_tdf = ctx.tls_ext_18_tdf 265 | if scts_ocsp: 266 | if ctx.ocsp_resp_der: 267 | ocsp_resp_der = ctx.ocsp_resp_der 268 | 269 | except Exception as exc: 270 | exc_str = str(exc) 271 | if exc_str == '': 272 | exc_str = str(type(exc)) 273 | err = domain + ': ' + exc_str 274 | finally: 275 | sock.close() 276 | 277 | ee_cert_der = None 278 | if ee_cert_x509: 279 | ee_cert_der = OpenSSL.crypto.dump_certificate( 280 | type=OpenSSL.crypto.FILETYPE_ASN1, 281 | cert=ee_cert_x509) 282 | 283 | issuer_cert_der = None 284 | if issuer_cert_x509: 285 | # https://tools.ietf.org/html/rfc5246#section-7.4.2 286 | issuer_cert_der = OpenSSL.crypto.dump_certificate( 287 | type=OpenSSL.crypto.FILETYPE_ASN1, 288 | cert=issuer_cert_x509) 289 | 290 | more_issuer_cert_der_candidates = [ 291 | OpenSSL.crypto.dump_certificate(type=OpenSSL.crypto.FILETYPE_ASN1, 292 | cert=cert_x509) 293 | for cert_x509 294 | in more_issuer_cert_x509_candidates 295 | ] 296 | 297 | return TlsHandshakeResult(ee_cert_der, issuer_cert_der, 298 | more_issuer_cert_der_candidates, 299 | ocsp_resp_der, tls_ext_18_tdf, 300 | err) 301 | -------------------------------------------------------------------------------- /ctutlz/tls/handshake_openssl_build.py: -------------------------------------------------------------------------------- 1 | '''Compile cffi loader in order to use OpenSSL c-code functionality to register 2 | a callback for TLS extension 18 results in the SSL context object created 3 | with PyOpenSSL. 4 | 5 | CFFI will be used in API level: 6 | https://cffi.readthedocs.io/en/latest/overview.html#real-example-api-level-out-of-line 7 | 8 | The callback is written in Python: 9 | https://cffi.readthedocs.io/en/latest/using.html#extern-python-new-style-callbacks 10 | 11 | FFI means foreign function interface: 12 | https://enwikipedia.org/wiki/Foreign_function_interface 13 | ''' 14 | 15 | from cffi import FFI 16 | 17 | 18 | def create_ffibuilder(): 19 | module_name = 'ctutlz.tls.handshake_openssl' 20 | 21 | libraries = ['ssl', 'crypto'] 22 | 23 | csource_ffi = r''' 24 | #include "stdio.h" // FILE 25 | 26 | #include "openssl/bio.h" 27 | #include "openssl/err.h" 28 | #include "openssl/ssl.h" 29 | ''' 30 | 31 | cdefinitions_lib = r''' 32 | // for TLS extension 18 33 | 34 | typedef struct ssl_ctx_st SSL_CTX; 35 | typedef struct ssl_st SSL; 36 | 37 | typedef int (*custom_ext_add_cb) (SSL *s, unsigned int ext_type, 38 | const unsigned char **out, 39 | size_t *outlen, int *al, 40 | void *add_arg); 41 | typedef void (*custom_ext_free_cb) (SSL *s, unsigned int ext_type, 42 | const unsigned char *out, 43 | void *add_arg); 44 | typedef int (*custom_ext_parse_cb) (SSL *s, unsigned int ext_type, 45 | const unsigned char *in, 46 | size_t inlen, int *al, 47 | void *parse_arg); 48 | int SSL_CTX_add_client_custom_ext(SSL_CTX *ctx, unsigned int ext_type, 49 | custom_ext_add_cb add_cb, 50 | custom_ext_free_cb free_cb, 51 | void *add_arg, 52 | custom_ext_parse_cb parse_cb, 53 | void *parse_arg); 54 | 55 | extern "Python" static int serverinfo_cli_parse_cb(SSL *s, 56 | unsigned int ext_type, 57 | const unsigned char *in, 58 | size_t inlen, 59 | int *al, void *arg); 60 | ''' 61 | 62 | ffibuilder = FFI() 63 | ffibuilder.set_source(module_name, csource_ffi, libraries=libraries) 64 | ffibuilder.cdef(cdefinitions_lib) 65 | return ffibuilder 66 | 67 | 68 | # hook for the kwarg 'cffi_modules' of the setup() call in setup.py 69 | ffibuilder = create_ffibuilder() 70 | 71 | 72 | # hook for '__main__' clause in verify_scts.py 73 | def compile(): 74 | ffibuilder.compile(verbose=False) 75 | -------------------------------------------------------------------------------- /ctutlz/tls/sctlist.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from utlz import flo 4 | 5 | from utlz import StructContext 6 | 7 | 8 | _SctListEntry = collections.namedtuple( 9 | typename='SctListEntry', 10 | field_names=[ 11 | 'sct_len', 12 | 'sct_der', 13 | ] 14 | ) 15 | 16 | 17 | _TlsExtension18 = collections.namedtuple( 18 | typename='TlsExtension18', 19 | field_names=[ 20 | 'tls_extension_type', 21 | 'tls_extension_len', 22 | 'signed_certificate_timestamp_list_len', 23 | 'sct_list', 24 | ] 25 | ) 26 | 27 | 28 | def TlsExtension18(extension_18_tdf): 29 | with StructContext(extension_18_tdf) as struct: 30 | data_dict = { 31 | 'tls_extension_type': struct.read('!H'), 32 | 'tls_extension_len': struct.read('!H'), 33 | 'signed_certificate_timestamp_list_len': struct.read('!H'), 34 | } 35 | sct_list = [] 36 | while struct.offset < struct.length: 37 | sct_len = struct.read('!H') 38 | sct_der = struct.read(flo('!{sct_len}s')) 39 | sct_list.append(_SctListEntry(sct_len, sct_der)) 40 | return _TlsExtension18(sct_list=sct_list, **data_dict) 41 | 42 | 43 | _SignedCertificateTimestampList = collections.namedtuple( 44 | typename='SignedCertificateTimestampList', 45 | field_names=[ 46 | 'signed_certificate_timestamp_list_len', 47 | 'sct_list', 48 | ] 49 | ) 50 | 51 | 52 | def SignedCertificateTimestampList(sctlist): 53 | with StructContext(sctlist) as struct: 54 | data_dict = { 55 | 'signed_certificate_timestamp_list_len': struct.read('!H'), 56 | } 57 | sct_list = [] 58 | while struct.offset < struct.length: 59 | sct_len = struct.read('!H') 60 | sct_der = struct.read(flo('!{sct_len}s')) 61 | sct_list.append(_SctListEntry(sct_len, sct_der)) 62 | return _SignedCertificateTimestampList(sct_list=sct_list, **data_dict) 63 | -------------------------------------------------------------------------------- /ctutlz/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/ctutlz/utils/__init__.py -------------------------------------------------------------------------------- /ctutlz/utils/encoding.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | 4 | 5 | def encode_to_b64(arg): 6 | '''Return arg as Base64 encoded string (not as bytes-str).''' 7 | res = base64.b64encode(arg) 8 | # assert type(res) == bytes, 'type(result) is ' + str(type(res)) 9 | return res.decode('ascii') 10 | 11 | 12 | def decode_from_b64(arg): 13 | return base64.b64decode(arg) 14 | 15 | 16 | def sha256_digest(arg): 17 | return hashlib.sha256(arg).digest() 18 | 19 | 20 | def digest_from_b64(arg): 21 | return sha256_digest(decode_from_b64(arg)) 22 | 23 | 24 | def digest_from_b64_encoded_to_b64(arg): 25 | return encode_to_b64(digest_from_b64(arg)) 26 | -------------------------------------------------------------------------------- /ctutlz/utils/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | logger = logging.getLogger('ctutlz') 5 | 6 | VERBOSE = 15 # between DEBUG=10 and INFO=20 7 | 8 | 9 | class InfoFilter(logging.Filter): 10 | def filter(self, rec): 11 | return rec.levelno in (logging.DEBUG, VERBOSE, logging.INFO) 12 | 13 | 14 | def init_logger(): 15 | logging.addLevelName(VERBOSE, "VERBOSE") 16 | 17 | def info_verbose(self, message, *args, **kws): 18 | self.log(VERBOSE, message, *args, **kws) 19 | 20 | logging.Logger.verbose = info_verbose 21 | 22 | 23 | def setup_logging(loglevel): 24 | '''Write info, verbose and debug messages to stdout, else to stderr 25 | (warning and error). 26 | ''' 27 | logger.setLevel(loglevel) 28 | try: 29 | # python 2.6 30 | out_handler = logging.StreamHandler(stream=sys.stdout) 31 | err_handler = logging.StreamHandler(stream=sys.stderr) 32 | except TypeError: 33 | # since python 2.7 34 | out_handler = logging.StreamHandler() 35 | err_handler = logging.StreamHandler(stream=sys.stderr) 36 | 37 | out_handler.setLevel(loglevel) 38 | out_handler.addFilter(InfoFilter()) 39 | 40 | err_handler.setLevel(logging.WARNING) 41 | 42 | logger.addHandler(out_handler) 43 | logger.addHandler(err_handler) 44 | 45 | return logger 46 | -------------------------------------------------------------------------------- /ctutlz/utils/string.py: -------------------------------------------------------------------------------- 1 | def to_hex(val): 2 | '''Return val as str of hex values concatenated by colons.''' 3 | if type(val) is int: 4 | return hex(val) 5 | try: 6 | # Python-2.x 7 | if type(val) is long: 8 | return hex(val) 9 | except NameError: 10 | pass 11 | # else: 12 | try: 13 | # Python-2.x 14 | return ":".join("{0:02x}".format(ord(char)) for char in val) 15 | except TypeError: 16 | # Python-3.x 17 | return ":".join("{0:02x}".format(char) for char in val) 18 | 19 | 20 | # http://stackoverflow.com/a/16891418 21 | def string_without_prefix(prefix ,string): 22 | '''Return string without prefix. If string does not start with prefix, 23 | return string. 24 | ''' 25 | if string.startswith(prefix): 26 | return string[len(prefix):] 27 | return string 28 | 29 | 30 | def string_with_prefix(prefix, string): 31 | '''Return string with prefix prepended. If string already starts with 32 | prefix, return string. 33 | ''' 34 | return str(prefix) + string_without_prefix(str(prefix), str(string)) 35 | -------------------------------------------------------------------------------- /ctutlz/utils/tdf_bytes.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from utlz import namedtuple as namedtuple_utlz 4 | 5 | 6 | # tdf := "TLS Data Format" (cf. https://tools.ietf.org/html/rfc5246#section-4) 7 | 8 | 9 | def namedtuple(typename, field_names='arg', lazy_vals=None, **kwargs): 10 | 11 | lazy_vals['_parse'] = lambda self: \ 12 | self.arg if type(self.arg) == dict else \ 13 | self._parse_func(self.arg)[0] if type(self.arg) == bytes else \ 14 | None 15 | 16 | lazy_vals['tdf'] = lambda self: \ 17 | self._parse['tdf'] 18 | 19 | return namedtuple_utlz(typename, field_names, lazy_vals, **kwargs) 20 | 21 | 22 | class TdfBytesParser(object): 23 | '''An instance of this is a file like object which enables access of a 24 | tdf (data) struct (a bytes string). 25 | ''' 26 | 27 | # context methods 28 | 29 | def __init__(self, tdf_bytes): 30 | self._bytes = tdf_bytes 31 | self.offset = 0 32 | '''mutable parse results (read and delegate) dict''' 33 | self.res = {} 34 | 35 | def __enter__(self): 36 | self.offset = 0 37 | return self 38 | 39 | def __exit__(self, exc_type, exc_value, exc_traceback): 40 | self.offset = 0 41 | return 42 | 43 | # methods for parsing 44 | 45 | def read(self, key, fmt): 46 | data = struct.unpack_from(fmt, self._bytes, self.offset) 47 | self.offset += struct.calcsize(fmt) 48 | if len(data) == 1: 49 | self.res[key] = data[0] 50 | else: 51 | self.res[key] = data 52 | return self.res[key] 53 | 54 | def delegate(self, key, read_func): 55 | self.res[key], offset = read_func(self._bytes[self.offset:]) 56 | self.offset += offset 57 | return self.res[key] 58 | 59 | def result(self): 60 | self.res['tdf'] = bytes(bytearray(self._bytes[0:self.offset])) 61 | return self.res, self.offset 62 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import inspect 3 | import sys 4 | import tempfile 5 | from os.path import dirname, join 6 | 7 | from fabric.api import execute, local, task 8 | from fabric.context_managers import warn_only, quiet 9 | try: 10 | from fabsetup.fabutils import extract_minors_from_setup_py, print_msg 11 | from fabsetup.fabutils import determine_latest_pythons, highest_minor 12 | except ImportError: 13 | print('fabsetup not installed, run:\n\n pip2 install fabsetup') 14 | sys.exit(1) 15 | 16 | 17 | # inspired by: http://stackoverflow.com/a/6618825 18 | def flo(string): 19 | '''Return the string given by param formatted with the callers locals.''' 20 | callers_locals = {} 21 | frame = inspect.currentframe() 22 | try: 23 | outerframe = frame.f_back 24 | callers_locals = outerframe.f_locals 25 | finally: 26 | del frame 27 | return string.format(**callers_locals) 28 | 29 | 30 | def _wrap_with(color_code): 31 | '''Color wrapper. 32 | 33 | Example: 34 | >>> blue = _wrap_with('34') 35 | >>> print(blue('text')) 36 | \033[34mtext\033[0m 37 | ''' 38 | def inner(text, bold=False): 39 | '''Inner color function.''' 40 | code = color_code 41 | if bold: 42 | code = flo("1;{code}") 43 | return flo('\033[{code}m{text}\033[0m') 44 | return inner 45 | 46 | 47 | cyan = _wrap_with('36') 48 | 49 | 50 | def query_yes_no(question, default="yes"): 51 | """Ask a yes/no question via raw_input() and return their answer. 52 | 53 | "question" is a string that is presented to the user. 54 | "default" is the presumed answer if the user just hits . 55 | It must be "yes" (the default), "no", or None (which means an answer 56 | of the user is required). 57 | 58 | The "answer" return value is True for "yes" or False for "no". 59 | """ 60 | valid = {"yes": True, "y": True, "ye": True, '1': True, 61 | "no": False, "n": False, '0': False, } 62 | if default is None: 63 | prompt = " [y/n] " 64 | elif default == "yes": 65 | prompt = " [Y/n] " 66 | elif default == "no": 67 | prompt = " [y/N] " 68 | else: 69 | raise ValueError("invalid default answer: '%s'" % default) 70 | 71 | while True: 72 | sys.stdout.write(question + prompt) 73 | choice = raw_input().lower() 74 | if default is not None and choice == '': 75 | return valid[default] 76 | elif choice in valid: 77 | return valid[choice] 78 | else: 79 | sys.stdout.write("Please respond with 'yes' or 'no' " 80 | "(or 'y' or 'n').\n") 81 | 82 | 83 | @task 84 | def clean(deltox=False): 85 | '''Delete temporary files not under version control. 86 | 87 | Args: 88 | deltox: If True, delete virtual environments used by tox 89 | ''' 90 | 91 | basedir = dirname(__file__) 92 | 93 | print(cyan('delete temp files and dirs for packaging')) 94 | local(flo( 95 | 'rm -rf ' 96 | '{basedir}/.eggs/ ' 97 | '{basedir}/ctutlz.egg-info/ ' 98 | '{basedir}/dist ' 99 | '{basedir}/README ' 100 | '{basedir}/build/ ' 101 | )) 102 | 103 | print(cyan('\ndelete temp files and dirs for editing')) 104 | local(flo( 105 | 'rm -rf ' 106 | '{basedir}/.cache ' 107 | '{basedir}/.ropeproject ' 108 | )) 109 | 110 | print(cyan('\ndelete bytecode compiled versions of the python src')) 111 | # cf. http://stackoverflow.com/a/30659970 112 | local(flo('find {basedir}/ctutlz {basedir}/tests ') + 113 | '\( -name \*pyc -o -name \*.pyo -o -name __pycache__ ' 114 | '-o -name \*.so -o -name \*.o -o -name \*.c \) ' 115 | '-prune ' 116 | '-exec rm -rf {} +') 117 | 118 | if deltox: 119 | print(cyan('\ndelete tox virual environments')) 120 | local(flo('cd {basedir} && rm -rf .tox/')) 121 | 122 | 123 | def _pyenv_exists(): 124 | with quiet(): 125 | res = local('pyenv') 126 | if res.return_code == 127: 127 | return False 128 | return True 129 | 130 | 131 | def _determine_latest_pythons(): 132 | filename_setup_py = join(dirname(__file__), 'setup.py') 133 | minors = extract_minors_from_setup_py(filename_setup_py) 134 | return determine_latest_pythons(minors) 135 | 136 | 137 | @task 138 | def pythons(): 139 | '''Install latest pythons with pyenv. 140 | 141 | The python version will be activated in the projects base dir. 142 | 143 | Will skip already installed latest python versions. 144 | ''' 145 | if not _pyenv_exists(): 146 | print('\npyenv is not installed. You can install it with fabsetup ' 147 | '(https://github.com/theno/fabsetup):\n\n ' + 148 | cyan('mkdir ~/repos && cd ~/repos\n ' 149 | 'git clone https://github.com/theno/fabsetup.git\n ' 150 | 'cd fabsetup && fab setup.pyenv -H localhost')) 151 | return 1 152 | 153 | print(cyan('\n## determine latest python versions')) 154 | latest_pythons = _determine_latest_pythons() 155 | 156 | print(cyan('\n## install latest python versions')) 157 | for version in latest_pythons: 158 | local(flo('pyenv install --skip-existing {version}')) 159 | 160 | print(cyan('\n## activate pythons')) 161 | basedir = dirname(__file__) 162 | latest_pythons_str = ' '.join(latest_pythons) 163 | local(flo('cd {basedir} && pyenv local system {latest_pythons_str}')) 164 | 165 | highest_python = latest_pythons[-1] 166 | print(cyan(flo( 167 | '\n## prepare Python-{highest_python} for testing and packaging'))) 168 | packages_for_testing = 'pytest tox' 169 | packages_for_packaging = 'pypandoc twine' 170 | local(flo('~/.pyenv/versions/{highest_python}/bin/pip install --upgrade ' 171 | 'pip {packages_for_testing} {packages_for_packaging}')) 172 | 173 | 174 | def _local_needs_pythons(*args, **kwargs): 175 | with warn_only(): 176 | res = local(*args, **kwargs) 177 | print(res) 178 | if res.return_code == 127: 179 | print(cyan('missing python version(s), ' 180 | 'run fabric task `pythons`:\n\n ' 181 | 'fab pythons\n')) 182 | sys.exit(1) 183 | 184 | 185 | @task 186 | def tox(args=''): 187 | '''Run tox. 188 | 189 | Build package and run unit tests against several pythons. 190 | 191 | Args: 192 | args: Optional arguments passed to tox. 193 | Example: 194 | 195 | fab tox:'-e py36 -r' 196 | ''' 197 | basedir = dirname(__file__) 198 | 199 | latest_pythons = _determine_latest_pythons() 200 | # e.g. highest_minor_python: '3.6' 201 | highest_minor_python = highest_minor(latest_pythons) 202 | 203 | _local_needs_pythons(flo('cd {basedir} && ' 204 | 'python{highest_minor_python} -m tox {args}')) 205 | 206 | 207 | @task 208 | def test(args='', py=None): 209 | '''Run unit tests. 210 | 211 | Keyword-Args: 212 | args: Optional arguments passed to pytest 213 | py: python version to run the tests against 214 | 215 | Example: 216 | 217 | fab test:args=-s,py=py27 218 | ''' 219 | basedir = dirname(__file__) 220 | 221 | if py is None: 222 | # e.g. envlist: 'envlist = py26,py27,py33,py34,py35,py36' 223 | envlist = local(flo('cd {basedir} && grep envlist tox.ini'), 224 | capture=True) 225 | _, py = envlist.rsplit(',', 1) 226 | 227 | with warn_only(): 228 | res = local(flo('cd {basedir} && ' 229 | "PYTHONPATH='.' .tox/{py}/bin/python -m pytest {args}")) 230 | print(res) 231 | if res.return_code == 127: 232 | print(cyan('missing tox virtualenv, ' 233 | 'run fabric task `tox`:\n\n ' 234 | 'fab tox\n')) 235 | sys.exit(1) 236 | 237 | 238 | @task 239 | def pypi(): 240 | '''Build package and upload to pypi.''' 241 | if query_yes_no('version updated in ctutlz/_version.py?'): 242 | 243 | print(cyan('\n## clean-up\n')) 244 | execute(clean) 245 | 246 | basedir = dirname(__file__) 247 | 248 | latest_pythons = _determine_latest_pythons() 249 | # e.g. highest_minor: '3.6' 250 | _highest_minor = highest_minor(latest_pythons) 251 | python = flo('python{_highest_minor}') 252 | 253 | print(cyan('\n## build package')) 254 | _local_needs_pythons(flo('cd {basedir} && {python} setup.py sdist')) 255 | 256 | print(cyan('\n## upload package')) 257 | local(flo('cd {basedir} && {python} -m twine upload dist/*')) 258 | 259 | 260 | @task 261 | def uplogs(): 262 | '''Download latest version of `all_logs_list.json`, `log_list.json` 263 | into dir `tests/data/test_ctlog`. 264 | ''' 265 | basedir = dirname(__file__) 266 | test_data_dir = flo('{basedir}/tests/data/test_ctlog') 267 | 268 | tmp_dir = tempfile.mkdtemp(prefix='ctutlz_') 269 | 270 | file_items = [ 271 | ('known-logs.html', 272 | 'http://www.certificate-transparency.org/known-logs'), 273 | ('all_logs_list.json', 274 | 'https://www.gstatic.com/ct/log_list/all_logs_list.json'), 275 | ('log_list.json', 276 | 'https://www.gstatic.com/ct/log_list/log_list.json'), 277 | ] 278 | for filename, url in file_items: 279 | print_msg(flo('\n## {filename}\n')) 280 | 281 | basename, ending = filename.split('.') 282 | latest = local(flo('cd {test_data_dir} && ' 283 | 'ls {basename}_*.{ending} | sort | tail -n1'), 284 | capture=True) 285 | print(latest) 286 | 287 | tmp_file = flo('{tmp_dir}/{filename}') 288 | 289 | local(flo('wget {url} -O {tmp_file}')) 290 | 291 | with warn_only(): 292 | res = local(flo('diff -u {test_data_dir}/{latest} {tmp_file}'), 293 | capture=True) 294 | print(res) 295 | files_differ = bool(res.return_code != 0) 296 | if files_differ: 297 | today = datetime.date.today().strftime('%F') 298 | local(flo( 299 | 'cp {tmp_file} {test_data_dir}/{basename}_{today}.{ending}')) 300 | else: 301 | print('no changes') 302 | 303 | print('') 304 | local(flo('rm -rf {tmp_dir}')) 305 | -------------------------------------------------------------------------------- /requirements-all.txt: -------------------------------------------------------------------------------- 1 | # all deps and depdeps (gathered with `pip freeze`) 2 | 3 | aiohttp==3.1.3 4 | asn1crypto==0.24.0 5 | async-timeout==2.0.1 6 | attrs==17.4.0 7 | certifi==2018.1.18 8 | cffi==1.11.5 9 | chardet==3.0.4 10 | cryptography==1.9 11 | ctutlz==0.9.7 12 | html2text==2018.1.9 13 | idna==2.6 14 | idna-ssl==1.0.1 15 | multidict==4.2.0 16 | py==1.5.3 17 | pyasn1==0.2.3 18 | pyasn1-modules==0.0.9 19 | pycparser==2.18 20 | pyOpenSSL==17.3.0 21 | pytest==3.2.5 22 | requests==2.20.0 23 | six==1.11.0 24 | urllib3==1.22 25 | utlz==0.10.5 26 | yarl==1.1.1 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Certificate Transparency utils library and scripts. 2 | 3 | * https://github.com/theno/ctutlz 4 | * https://pypi.python.org/pypi/ctutlz 5 | """ 6 | 7 | import os 8 | import shutil 9 | from setuptools import setup, find_packages 10 | from codecs import open 11 | 12 | 13 | def create_readme_with_long_description(): 14 | this_dir = os.path.abspath(os.path.dirname(__file__)) 15 | readme_md = os.path.join(this_dir, 'README.md') 16 | readme = os.path.join(this_dir, 'README') 17 | if os.path.isfile(readme_md): 18 | if os.path.islink(readme): 19 | os.remove(readme) 20 | shutil.copy(readme_md, readme) 21 | try: 22 | import pypandoc 23 | long_description = pypandoc.convert(readme_md, 'rst', format='md') 24 | if os.path.islink(readme): 25 | os.remove(readme) 26 | with open(readme, 'w') as out: 27 | out.write(long_description) 28 | except(IOError, ImportError, RuntimeError): 29 | if os.path.isfile(readme_md): 30 | os.remove(readme) 31 | os.symlink(readme_md, readme) 32 | with open(readme, encoding='utf-8') as in_: 33 | long_description = in_.read() 34 | return long_description 35 | 36 | 37 | this_dir = os.path.abspath(os.path.dirname(__file__)) 38 | filename = os.path.join(this_dir, 'ctutlz', '_version.py') 39 | with open(filename, 'rt') as fh: 40 | version = fh.read().split('"')[1] 41 | 42 | description = __doc__.split('\n')[0] 43 | long_description = create_readme_with_long_description() 44 | 45 | setup( 46 | name='ctutlz', 47 | version=version, 48 | description=description, 49 | long_description=long_description, 50 | url='https://github.com/theno/ctutlz', 51 | author='Theodor Nolte', 52 | author_email='ctutlz@theno.eu', 53 | license='MIT', 54 | entry_points={ 55 | 'console_scripts': [ 56 | 'ctloglist = ctutlz.scripts.ctloglist:main', 57 | 'decompose-cert = ctutlz.scripts.decompose_cert:main', 58 | 'verify-scts = ctutlz.scripts.verify_scts:main', 59 | ], 60 | }, 61 | classifiers=[ 62 | 'Development Status :: 3 - Alpha', 63 | 'Intended Audience :: Developers', 64 | 'Topic :: Software Development :: Libraries :: Python Modules', 65 | 'License :: OSI Approved :: MIT License', 66 | 'Programming Language :: Python :: 3', 67 | 'Programming Language :: Python :: 3.5', 68 | 'Programming Language :: Python :: 3.6', 69 | 'Programming Language :: Python :: 3.7', 70 | 'Programming Language :: Python :: 3.8', 71 | ], 72 | keywords='python development utilities library ' 73 | 'certificate-transparency ct signed-certificate-timestamp sct', 74 | packages=find_packages(exclude=[ 75 | 'contrib', 76 | 'docs', 77 | 'tests', 78 | ]), 79 | package_data={'ctutlz': ['really_all_logs.json', 'log_list_schema.json'], }, 80 | install_requires=[ 81 | 'cffi==1.11.5', 82 | 'cryptography==1.9', 83 | 'html2text==2016.9.19', 84 | 'pyasn1==0.2.3', 85 | 'pyasn1-modules', # ==0.0.9', 86 | 'pyOpenSSL==17.3.0', 87 | 'requests==2.20.0', 88 | 'utlz==0.10.0', 89 | ], 90 | extras_require={ 91 | 'dev': ['pypandoc'], 92 | }, 93 | setup_requires=[ 94 | 'cffi>=1.4.0' 95 | ], 96 | cffi_modules=[ 97 | 'ctutlz/tls/handshake_openssl_build.py:ffibuilder' 98 | ], 99 | ) 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/test_ctlog/all_logs_list_2018-03-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "logs": [ 3 | { 4 | "description": "Google 'Argon2017' log", 5 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVG18id3qnfC6X/RtYHo3TwIlvxz2b4WurxXfaW7t26maKZfymXYe5jNGHif0vnDdWde6z/7Qco6wVw+dN4liow==", 6 | "url": "ct.googleapis.com/logs/argon2017/", 7 | "maximum_merge_delay": 86400, 8 | "operated_by": [ 9 | 0 10 | ] 11 | }, 12 | { 13 | "description": "Google 'Argon2018' log", 14 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0gBVBa3VR7QZu82V+ynXWD14JM3ORp37MtRxTmACJV5ZPtfUA7htQ2hofuigZQs+bnFZkje+qejxoyvk2Q1VaA==", 15 | "url": "ct.googleapis.com/logs/argon2018/", 16 | "maximum_merge_delay": 86400, 17 | "operated_by": [ 18 | 0 19 | ] 20 | }, 21 | { 22 | "description": "Google 'Argon2019' log", 23 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEI3MQm+HzXvaYa2mVlhB4zknbtAT8cSxakmBoJcBKGqGwYS0bhxSpuvABM1kdBTDpQhXnVdcq+LSiukXJRpGHVg==", 24 | "url": "ct.googleapis.com/logs/argon2019/", 25 | "maximum_merge_delay": 86400, 26 | "operated_by": [ 27 | 0 28 | ] 29 | }, 30 | { 31 | "description": "Google 'Argon2020' log", 32 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6Tx2p1yKY4015NyIYvdrk36es0uAc1zA4PQ+TGRY+3ZjUTIYY9Wyu+3q/147JG4vNVKLtDWarZwVqGkg6lAYzA==", 33 | "url": "ct.googleapis.com/logs/argon2020/", 34 | "maximum_merge_delay": 86400, 35 | "operated_by": [ 36 | 0 37 | ] 38 | }, 39 | { 40 | "description": "Google 'Argon2021' log", 41 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETeBmZOrzZKo4xYktx9gI2chEce3cw/tbr5xkoQlmhB18aKfsxD+MnILgGNl0FOm0eYGilFVi85wLRIOhK8lxKw==", 42 | "url": "ct.googleapis.com/logs/argon2021/", 43 | "maximum_merge_delay": 86400, 44 | "operated_by": [ 45 | 0 46 | ] 47 | }, 48 | { 49 | "description": "Google 'Aviator' log", 50 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1/TMabLkDpCjiupacAlP7xNi0I1JYP8bQFAHDG1xhtolSY1l4QgNRzRrvSe8liE+NPWHdjGxfx3JhTsN9x8/6Q==", 51 | "url": "ct.googleapis.com/aviator/", 52 | "maximum_merge_delay": 86400, 53 | "operated_by": [ 54 | 0 55 | ] 56 | }, 57 | { 58 | "description": "Google 'Icarus' log", 59 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETtK8v7MICve56qTHHDhhBOuV4IlUaESxZryCfk9QbG9co/CqPvTsgPDbCpp6oFtyAHwlDhnvr7JijXRD9Cb2FA==", 60 | "url": "ct.googleapis.com/icarus/", 61 | "maximum_merge_delay": 86400, 62 | "operated_by": [ 63 | 0 64 | ] 65 | }, 66 | { 67 | "description": "Google 'Pilot' log", 68 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA==", 69 | "url": "ct.googleapis.com/pilot/", 70 | "maximum_merge_delay": 86400, 71 | "operated_by": [ 72 | 0 73 | ] 74 | }, 75 | { 76 | "description": "Google 'Rocketeer' log", 77 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg==", 78 | "url": "ct.googleapis.com/rocketeer/", 79 | "maximum_merge_delay": 86400, 80 | "operated_by": [ 81 | 0 82 | ] 83 | }, 84 | { 85 | "description": "Google 'Skydiver' log", 86 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmyGDvYXsRJsNyXSrYc9DjHsIa2xzb4UR7ZxVoV6mrc9iZB7xjI6+NrOiwH+P/xxkRmOFG6Jel20q37hTh58rA==", 87 | "url": "ct.googleapis.com/skydiver/", 88 | "maximum_merge_delay": 86400, 89 | "operated_by": [ 90 | 0 91 | ] 92 | }, 93 | { 94 | "description": "Google 'Submariner' log", 95 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOfifIGLUV1Voou9JLfA5LZreRLSUMOCeeic8q3Dw0fpRkGMWV0Gtq20fgHQweQJeLVmEByQj9p81uIW4QkWkTw==", 96 | "url": "ct.googleapis.com/submariner/", 97 | "maximum_merge_delay": 86400, 98 | "operated_by": [ 99 | 0 100 | ] 101 | }, 102 | { 103 | "description": "Google 'Daedalus' log", 104 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbgwcuu4rakGFYB17fqsILPwMCqUIsz7VcCTRbR0ttrfzizbcI02VYxK75IaNzOnR7qFAot8LowYKMMqNrKQpVg==", 105 | "url": "ct.googleapis.com/daedalus/", 106 | "maximum_merge_delay": 604800, 107 | "operated_by": [ 108 | 0 109 | ] 110 | }, 111 | { 112 | "description": "Google 'Testtube' log", 113 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEw8i8S7qiGEs9NXv0ZJFh6uuOmR2Q7dPprzk9XNNGkUXjzqx2SDvRfiwKYwBljfWujozHESVPQyydGaHhkaSz/g==", 114 | "url": "ct.googleapis.com/testtube/", 115 | "maximum_merge_delay": 86400, 116 | "operated_by": [ 117 | 0 118 | ] 119 | }, 120 | { 121 | "description": "Cloudflare 'Nimbus2017' Log", 122 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE15ypB40iQe6ToFJB2vSA8CW86/rzPNJ+kdg/LNpRvcjuKnLj/xhW5DoiDyI8xtUws5toLqtWwkFf1mRXFLFarw==", 123 | "url": "ct.cloudflare.com/logs/nimbus2017/", 124 | "maximum_merge_delay": 86400, 125 | "operated_by": [ 126 | 1 127 | ] 128 | }, 129 | { 130 | "description": "Cloudflare 'Nimbus2018' Log", 131 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAsVpWvrH3Ke0VRaMg9ZQoQjb5g/xh1z3DDa6IuxY5DyPsk6brlvrUNXZzoIg0DcvFiAn2kd6xmu4Obk5XA/nRg==", 132 | "url": "ct.cloudflare.com/logs/nimbus2018/", 133 | "maximum_merge_delay": 86400, 134 | "operated_by": [ 135 | 1 136 | ] 137 | }, 138 | { 139 | "description": "Cloudflare 'Nimbus2019' Log", 140 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkZHz1v5r8a9LmXSMegYZAg4UW+Ug56GtNfJTDNFZuubEJYgWf4FcC5D+ZkYwttXTDSo4OkanG9b3AI4swIQ28g==", 141 | "url": "ct.cloudflare.com/logs/nimbus2019/", 142 | "maximum_merge_delay": 86400, 143 | "operated_by": [ 144 | 1 145 | ] 146 | }, 147 | { 148 | "description": "Cloudflare 'Nimbus2020' Log", 149 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01EAhx4o0zPQrXTcYjgCt4MVFsT0Pwjzb1RwrM0lhWDlxAYPP6/gyMCXNkOn/7KFsjL7rwk78tHMpY8rXn8AYg==", 150 | "url": "ct.cloudflare.com/logs/nimbus2020/", 151 | "maximum_merge_delay": 86400, 152 | "operated_by": [ 153 | 1 154 | ] 155 | }, 156 | { 157 | "description": "Cloudflare 'Nimbus2021' Log", 158 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExpon7ipsqehIeU1bmpog9TFo4Pk8+9oN8OYHl1Q2JGVXnkVFnuuvPgSo2Ep+6vLffNLcmEbxOucz03sFiematg==", 159 | "url": "ct.cloudflare.com/logs/nimbus2021/", 160 | "maximum_merge_delay": 86400, 161 | "operated_by": [ 162 | 1 163 | ] 164 | }, 165 | { 166 | "description": "DigiCert Log Server", 167 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A==", 168 | "url": "ct1.digicert-ct.com/log/", 169 | "maximum_merge_delay": 86400, 170 | "operated_by": [ 171 | 2 172 | ] 173 | }, 174 | { 175 | "description": "DigiCert Log Server 2", 176 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzF05L2a4TH/BLgOhNKPoioYCrkoRxvcmajeb8Dj4XQmNY+gxa4Zmz3mzJTwe33i0qMVp+rfwgnliQ/bM/oFmhA==", 177 | "url": "ct2.digicert-ct.com/log/", 178 | "maximum_merge_delay": 86400, 179 | "operated_by": [ 180 | 2 181 | ] 182 | }, 183 | { 184 | "description": "DigiCert Yeti2018 Log", 185 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESYlKFDLLFmA9JScaiaNnqlU8oWDytxIYMfswHy9Esg0aiX+WnP/yj4O0ViEHtLwbmOQeSWBGkIu9YK9CLeer+g==", 186 | "url": "yeti2018.ct.digicert.com/log/", 187 | "maximum_merge_delay": 86400, 188 | "operated_by": [ 189 | 2 190 | ] 191 | }, 192 | { 193 | "description": "DigiCert Yeti2019 Log", 194 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkZd/ow8X+FSVWAVSf8xzkFohcPph/x6pS1JHh7g1wnCZ5y/8Hk6jzJxs6t3YMAWz2CPd4VkCdxwKexGhcFxD9A==", 195 | "url": "yeti2019.ct.digicert.com/log/", 196 | "maximum_merge_delay": 86400, 197 | "operated_by": [ 198 | 2 199 | ] 200 | }, 201 | { 202 | "description": "DigiCert Yeti2020 Log", 203 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEURAG+Zo0ac3n37ifZKUhBFEV6jfcCzGIRz3tsq8Ca9BP/5XUHy6ZiqsPaAEbVM0uI3Tm9U24RVBHR9JxDElPmg==", 204 | "url": "yeti2020.ct.digicert.com/log/", 205 | "maximum_merge_delay": 86400, 206 | "operated_by": [ 207 | 2 208 | ] 209 | }, 210 | { 211 | "description": "DigiCert Yeti2021 Log", 212 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6J4EbcpIAl1+AkSRsbhoY5oRTj3VoFfaf1DlQkfi7Rbe/HcjfVtrwN8jaC+tQDGjF+dqvKhWJAQ6Q6ev6q9Mew==", 213 | "url": "yeti2021.ct.digicert.com/log/", 214 | "maximum_merge_delay": 86400, 215 | "operated_by": [ 216 | 2 217 | ] 218 | }, 219 | { 220 | "description": "DigiCert Yeti2022 Log", 221 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEn/jYHd77W1G1+131td5mEbCdX/1v/KiYW5hPLcOROvv+xA8Nw2BDjB7y+RGyutD2vKXStp/5XIeiffzUfdYTJg==", 222 | "url": "yeti2022.ct.digicert.com/log/", 223 | "maximum_merge_delay": 86400, 224 | "operated_by": [ 225 | 2 226 | ] 227 | }, 228 | { 229 | "description": "DigiCert Nessie2018 Log", 230 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVqpLa2W+Rz1XDZPBIyKJO+KKFOYZTj9MpJWnZeFUqzc5aivOiWEVhs8Gy2AlH3irWPFjIZPZMs3Dv7M+0LbPyQ==", 231 | "url": "nessie2018.ct.digicert.com/log/", 232 | "maximum_merge_delay": 86400, 233 | "operated_by": [ 234 | 2 235 | ] 236 | }, 237 | { 238 | "description": "DigiCert Nessie2019 Log", 239 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEX+0nudCKImd7QCtelhMrDW0OXni5RE10tiiClZesmrwUk2iHLCoTHHVV+yg5D4n/rxCRVyRhikPpVDOLMLxJaA==", 240 | "url": "nessie2019.ct.digicert.com/log/", 241 | "maximum_merge_delay": 86400, 242 | "operated_by": [ 243 | 2 244 | ] 245 | }, 246 | { 247 | "description": "DigiCert Nessie2020 Log", 248 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4hHIyMVIrR9oShgbQMYEk8WX1lmkfFKB448Gn93KbsZnnwljDHY6MQqEnWfKGgMOq0gh3QK48c5ZB3UKSIFZ4g==", 249 | "url": "nessie2020.ct.digicert.com/log/", 250 | "maximum_merge_delay": 86400, 251 | "operated_by": [ 252 | 2 253 | ] 254 | }, 255 | { 256 | "description": "DigiCert Nessie2021 Log", 257 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9o7AiwrbGBIX6Lnc47I6OfLMdZnRzKoP5u072nBi6vpIOEooktTi1gNwlRPzGC2ySGfuc1xLDeaA/wSFGgpYFg==", 258 | "url": "nessie2021.ct.digicert.com/log/", 259 | "maximum_merge_delay": 86400, 260 | "operated_by": [ 261 | 2 262 | ] 263 | }, 264 | { 265 | "description": "DigiCert Nessie2022 Log", 266 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJyTdaAMoy/5jvg4RR019F2ihEV1McclBKMe2okuX7MCv/C87v+nxsfz1Af+p+0lADGMkmNd5LqZVqxbGvlHYcQ==", 267 | "url": "nessie2022.ct.digicert.com/log/", 268 | "maximum_merge_delay": 86400, 269 | "operated_by": [ 270 | 2 271 | ] 272 | }, 273 | { 274 | "description": "Symantec log", 275 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEluqsHEYMG1XcDfy1lCdGV0JwOmkY4r87xNuroPS2bMBTP01CEDPwWJePa75y9CrsHEKqAy8afig1dpkIPSEUhg==", 276 | "url": "ct.ws.symantec.com/", 277 | "maximum_merge_delay": 86400, 278 | "operated_by": [ 279 | 2 280 | ] 281 | }, 282 | { 283 | "description": "Symantec 'Vega' log", 284 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6pWeAv/u8TNtS4e8zf0ZF2L/lNPQWQc/Ai0ckP7IRzA78d0NuBEMXR2G3avTK0Zm+25ltzv9WWis36b4ztIYTQ==", 285 | "url": "vega.ws.symantec.com/", 286 | "maximum_merge_delay": 86400, 287 | "operated_by": [ 288 | 2 289 | ] 290 | }, 291 | { 292 | "description": "Symantec Deneb", 293 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEloIeo806gIQel7i3BxmudhoO+FV2nRIzTpGI5NBIUFzBn2py1gH1FNbQOG7hMrxnDTfouiIQ0XKGeSiW+RcemA==", 294 | "url": "deneb.ws.symantec.com/", 295 | "maximum_merge_delay": 86400, 296 | "operated_by": [ 297 | 2 298 | ] 299 | }, 300 | { 301 | "description": "Symantec 'Sirius' log", 302 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEowJkhCK7JewN47zCyYl93UXQ7uYVhY/Z5xcbE4Dq7bKFN61qxdglnfr0tPNuFiglN+qjN2Syxwv9UeXBBfQOtQ==", 303 | "url": "sirius.ws.symantec.com/", 304 | "maximum_merge_delay": 86400, 305 | "operated_by": [ 306 | 2 307 | ] 308 | }, 309 | { 310 | "description": "Certly.IO log", 311 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECyPLhWKYYUgEc+tUXfPQB4wtGS2MNvXrjwFCCnyYJifBtd2Sk7Cu+Js9DNhMTh35FftHaHu6ZrclnNBKwmbbSA==", 312 | "url": "log.certly.io/", 313 | "maximum_merge_delay": 86400, 314 | "operated_by": [ 315 | 3 316 | ] 317 | }, 318 | { 319 | "description": "Izenpe log", 320 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ2Q5DC3cUBj4IQCiDu0s6j51up+TZAkAEcQRF6tczw90rLWXkJMAW7jr9yc92bIKgV8vDXU4lDeZHvYHduDuvg==", 321 | "url": "ct.izenpe.com/", 322 | "maximum_merge_delay": 86400, 323 | "operated_by": [ 324 | 4 325 | ] 326 | }, 327 | { 328 | "description": "Izenpe 'Argi' log", 329 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE18gOIz6eAjyauAdKKgX/SkuI1IpNOc73xfK2N+mj7eT1RQkOZxT9UyTVOpTy6rUT2R2LXKfD82vYPy07ZXJY1g==", 330 | "url": "ct.izenpe.eus/", 331 | "maximum_merge_delay": 86400, 332 | "operated_by": [ 333 | 4 334 | ] 335 | }, 336 | { 337 | "description": "WoSign CT log #1", 338 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1+wvK3VPN7yjQ7qLZWY8fWrlDCqmwuUm/gx9TnzwOrzi0yLcAdAfbkOcXG6DrZwV9sSNYLUdu6NiaX7rp6oBmw==", 339 | "url": "ct.wosign.com/", 340 | "maximum_merge_delay": 86400, 341 | "operated_by": [ 342 | 5 343 | ] 344 | }, 345 | { 346 | "description": "WoSign log", 347 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBGIey1my66PTTBmJxklIpMhRrQvAdPG+SvVyLpzmwai8IoCnNBrRhgwhbrpJIsO0VtwKAx+8TpFf1rzgkJgMQ==", 348 | "url": "ctlog.wosign.com/", 349 | "maximum_merge_delay": 86400, 350 | "operated_by": [ 351 | 5 352 | ] 353 | }, 354 | { 355 | "description": "WoSign log 2", 356 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpYzoNS6O5Wp1rVxLMWEpnTBXjgITX+nKu1KoQwVgvw1zV3eyBdhn9vAzyflE3rZTc6oMVcKDCkvOXhrHFx2zzQ==", 357 | "url": "ctlog2.wosign.com/", 358 | "maximum_merge_delay": 86400, 359 | "operated_by": [ 360 | 5 361 | ] 362 | }, 363 | { 364 | "description": "GDCA CT log #1", 365 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErQ8wrZ55pDiJJlSGq0FykG/7yhemrO7Gn30CBexBqMdBnTJJrbA5vTqHPnzuaGxg0Ucqk67hQPQLyDU8HQ9l0w==", 366 | "url": "ct.gdca.com.cn/", 367 | "maximum_merge_delay": 86400, 368 | "operated_by": [ 369 | 6 370 | ] 371 | }, 372 | { 373 | "description": "GDCA CT log #2", 374 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEW0rHAbd0VLpAnEN1lD+s77NxVrjT4nuuobE+U6qXM6GCu19dHAv6hQ289+Wg4CLwoInZCn9fJpTTJOOZLuQVjQ==", 375 | "url": "ctlog.gdca.com.cn/", 376 | "maximum_merge_delay": 86400, 377 | "operated_by": [ 378 | 7 379 | ] 380 | }, 381 | { 382 | "description": "Comodo 'Dodo' CT log", 383 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPXCMfVjQ2oWSgrewu4fIW4Sfh3lco90CwKZ061pvAI1eflh6c8ACE90pKM0muBDHCN+j0HV7scco4KKQPqq4A==", 384 | "url": "dodo.ct.comodo.com/", 385 | "maximum_merge_delay": 86400, 386 | "operated_by": [ 387 | 8 388 | ] 389 | }, 390 | { 391 | "description": "Venafi log", 392 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolpIHxdSlTXLo1s6H1OCdpSj/4DyHDc8wLG9wVmLqy1lk9fz4ATVmm+/1iN2Nk8jmctUKK2MFUtlWXZBSpym97M7frGlSaQXUWyA3CqQUEuIJOmlEjKTBEiQAvpfDjCHjlV2Be4qTM6jamkJbiWtgnYPhJL6ONaGTiSPm7Byy57iaz/hbckldSOIoRhYBiMzeNoA0DiRZ9KmfSeXZ1rB8y8X5urSW+iBzf2SaOfzBvDpcoTuAaWx2DPazoOl28fP1hZ+kHUYvxbcMjttjauCFx+JII0dmuZNIwjfeG/GBb9frpSX219k1O4Wi6OEbHEr8at/XQ0y7gTikOxBn/s5wQIDAQAB", 393 | "url": "ctlog.api.venafi.com/", 394 | "maximum_merge_delay": 86400, 395 | "operated_by": [ 396 | 9 397 | ] 398 | }, 399 | { 400 | "description": "Venafi Gen2 CT log", 401 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjicnerZVCXTrbEuUhGW85BXx6lrYfA43zro/bAna5ymW00VQb94etBzSg4j/KS/Oqf/fNN51D8DMGA2ULvw3AQ==", 402 | "url": "ctlog-gen2.api.venafi.com/", 403 | "maximum_merge_delay": 86400, 404 | "operated_by": [ 405 | 9 406 | ] 407 | }, 408 | { 409 | "description": "CNNIC CT log", 410 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7UIYZopMgTTJWPp2IXhhuAf1l6a9zM7gBvntj5fLaFm9pVKhKYhVnno94XuXeN8EsDgiSIJIj66FpUGvai5samyetZhLocRuXhAiXXbDNyQ4KR51tVebtEq2zT0mT9liTtGwiksFQccyUsaVPhsHq9gJ2IKZdWauVA2Fm5x9h8B9xKn/L/2IaMpkIYtd967TNTP/dLPgixN1PLCLaypvurDGSVDsuWabA3FHKWL9z8wr7kBkbdpEhLlg2H+NAC+9nGKx+tQkuhZ/hWR65aX+CNUPy2OB9/u2rNPyDydb988LENXoUcMkQT0dU3aiYGkFAY0uZjD2vH97TM20xYtNQIDAQAB", 411 | "url": "ctserver.cnnic.cn/", 412 | "maximum_merge_delay": 86400, 413 | "operated_by": [ 414 | 10 415 | ] 416 | }, 417 | { 418 | "description": "StartCom log", 419 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESPNZ8/YFGNPbsu1Gfs/IEbVXsajWTOaft0oaFIZDqUiwy1o/PErK38SCFFWa+PeOQFXc9NKv6nV0+05/YIYuUQ==", 420 | "url": "ct.startssl.com/", 421 | "maximum_merge_delay": 86400, 422 | "operated_by": [ 423 | 11 424 | ] 425 | }, 426 | { 427 | "description": "PuChuangSiDa CT log", 428 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArM8vS3Cs8Q2Wv+gK/kSd1IwXncOaEBGEE+2M+Tdtg+QAb7FLwKaJx2GPmjS7VlLKA1ZQ7yR/S0npNYHd8OcX9XLSI8XjE3/Xjng1j0nemASKY6+tojlwlYRoS5Ez/kzhMhfC8mG4Oo05f9WVgj5WGVBFb8sIMw3VGUIIGkhCEPFow8NBE8sNHtsCtyR6UZZuvAjqaa9t75KYjlXzZeXonL4aR2AwfXqArVaDepPDrpMraiiKpl9jGQy+fHshY0E4t/fodnNrhcy8civBUtBbXTFOnSrzTZtkFJkmxnH4e/hE1eMjIPMK14tRPnKA0nh4NS1K50CZEZU01C9/+V81NwIDAQAB", 429 | "url": "www.certificatetransparency.cn/ct/", 430 | "maximum_merge_delay": 86400, 431 | "operated_by": [ 432 | 12 433 | ] 434 | }, 435 | { 436 | "description": "Comodo 'Sabre' CT log", 437 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8m/SiQ8/xfiHHqtls9m7FyOMBg4JVZY9CgiixXGz0akvKD6DEL8S0ERmFe9U4ZiA0M4kbT5nmuk3I85Sk4bagA==", 438 | "url": "sabre.ct.comodo.com/", 439 | "maximum_merge_delay": 86400, 440 | "operated_by": [ 441 | 8 442 | ] 443 | }, 444 | { 445 | "description": "Comodo 'Mammoth' CT log", 446 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7+R9dC4VFbbpuyOL+yy14ceAmEf7QGlo/EmtYU6DRzwat43f/3swtLr/L8ugFOOt1YU/RFmMjGCL17ixv66MZw==", 447 | "url": "mammoth.ct.comodo.com/", 448 | "maximum_merge_delay": 86400, 449 | "operated_by": [ 450 | 8 451 | ] 452 | }, 453 | { 454 | "description": "Nordu 'flimsy' log", 455 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4qWq6afhBUi0OdcWUYhyJLNXTkGqQ9PMS5lqoCgkV2h1ZvpNjBH2u8UbgcOQwqDo66z6BWQJGolozZYmNHE2kQ==", 456 | "url": "flimsy.ct.nordu.net:8080/", 457 | "maximum_merge_delay": 86400, 458 | "operated_by": [ 459 | 13 460 | ] 461 | }, 462 | { 463 | "description": "Nordu 'plausible' log", 464 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UV9+jO2MCTzkabodO2F7LM03MUBc8MrdAtkcW6v6GA9taTTw9QJqofm0BbdAsbtJL/unyEf0zIkRgXjjzaYqQ==", 465 | "url": "plausible.ct.nordu.net/", 466 | "maximum_merge_delay": 86400, 467 | "operated_by": [ 468 | 13 469 | ] 470 | }, 471 | { 472 | "description": "SHECA CT log 1", 473 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEalgK7RxRWbgLt7VhzvV/vCSN/RoxpLdPxrivAwi1pljKW4yKBTAdiyAqCJRkdbrptjx7PAHfrD8dnB2cnyR6Q==", 474 | "url": "ctlog.sheca.com/", 475 | "maximum_merge_delay": 86400, 476 | "operated_by": [ 477 | 14 478 | ] 479 | }, 480 | { 481 | "description": "SHECA CT log 2", 482 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsY4diqo6rM6Gy1N26KidWb4XiAMH8ifggr6x/Gc7Ru7T8Y3Wd+ijtNsJXKAJQ/xf0Gg0IyQIwk/Y0rad7dWM2w==", 483 | "url": "ct.sheca.com/", 484 | "maximum_merge_delay": 86400, 485 | "operated_by": [ 486 | 14 487 | ] 488 | }, 489 | { 490 | "description": "Akamai CT Log", 491 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQ3nrSVxQKkpqj1mTvMNCdsKZ+CeBPAZs0sgEj3R7tLUh8uOo3DO5/iXpPQT8P7SuQONFfoSSKthS6x8/cxPQyA==", 492 | "url": "ct.akamai.com/", 493 | "maximum_merge_delay": 86400, 494 | "operated_by": [ 495 | 15 496 | ] 497 | }, 498 | { 499 | "description": "Alpha CT Log", 500 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEovftE+HTXAIIxI6Lm4s7OWjHkmo4oU8jxaVvb9dlgfjBm/SfqYtF9LlOG8miaReleIfZzohvQQO7oyrjd5eNeA==", 501 | "url": "alpha.ctlogs.org/", 502 | "maximum_merge_delay": 86400, 503 | "operated_by": [ 504 | 16 505 | ] 506 | }, 507 | { 508 | "description": "Let's Encrypt 'Clicky' log", 509 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHxoVg3cAdWK5n/YGBe2ViYNBgZfn4NQz/na6O8lJws3xz/4ScNe+qCJfsqRnAntxrh2sqOnRCNXO7zN6w18A3A==", 510 | "url": "clicky.ct.letsencrypt.org/", 511 | "maximum_merge_delay": 86400, 512 | "operated_by": [ 513 | 17 514 | ] 515 | }, 516 | { 517 | "description": "Up In The Air 'Behind the Sofa' log", 518 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWTmyppTGMrn+Y2keMDujW9WwQ8lQHpWlLadMSkmOi4+3+MziW5dy1eo/sSFI6ERrf+rvIv/f9F87bXcEsa+Qjw==", 519 | "url": "ct.filippo.io/behindthesofa/", 520 | "maximum_merge_delay": 86400, 521 | "operated_by": [ 522 | 18 523 | ] 524 | } 525 | ], 526 | "operators": [ 527 | { 528 | "name": "Google", 529 | "id": 0 530 | }, 531 | { 532 | "name": "Cloudflare", 533 | "id": 1 534 | }, 535 | { 536 | "name": "DigiCert", 537 | "id": 2 538 | }, 539 | { 540 | "name": "Certly", 541 | "id": 3 542 | }, 543 | { 544 | "name": "Izenpe", 545 | "id": 4 546 | }, 547 | { 548 | "name": "WoSign", 549 | "id": 5 550 | }, 551 | { 552 | "name": "Wang Shengnan", 553 | "id": 6 554 | }, 555 | { 556 | "name": "GDCA", 557 | "id": 7 558 | }, 559 | { 560 | "name": "Comodo CA Limited", 561 | "id": 8 562 | }, 563 | { 564 | "name": "Venafi", 565 | "id": 9 566 | }, 567 | { 568 | "name": "CNNIC", 569 | "id": 10 570 | }, 571 | { 572 | "name": "StartCom", 573 | "id": 11 574 | }, 575 | { 576 | "name": "Beijing PuChuangSiDa Technology Ltd.", 577 | "id": 12 578 | }, 579 | { 580 | "name": "NORDUnet", 581 | "id": 13 582 | }, 583 | { 584 | "name": "SHECA", 585 | "id": 14 586 | }, 587 | { 588 | "name": "Akamai", 589 | "id": 15 590 | }, 591 | { 592 | "name": "Matt Palmer", 593 | "id": 16 594 | }, 595 | { 596 | "name": "Let's Encrypt", 597 | "id": 17 598 | }, 599 | { 600 | "name": "Up In The Air Consulting", 601 | "id": 18 602 | } 603 | ] 604 | } -------------------------------------------------------------------------------- /tests/data/test_ctlog/log_list_2018-03-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "logs": [ 3 | { 4 | "description": "Google 'Argon2018' log", 5 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0gBVBa3VR7QZu82V+ynXWD14JM3ORp37MtRxTmACJV5ZPtfUA7htQ2hofuigZQs+bnFZkje+qejxoyvk2Q1VaA==", 6 | "url": "ct.googleapis.com/logs/argon2018/", 7 | "maximum_merge_delay": 86400, 8 | "operated_by": [ 9 | 0 10 | ], 11 | "dns_api_endpoint": "argon2018.ct.googleapis.com" 12 | }, 13 | { 14 | "description": "Google 'Argon2019' log", 15 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEI3MQm+HzXvaYa2mVlhB4zknbtAT8cSxakmBoJcBKGqGwYS0bhxSpuvABM1kdBTDpQhXnVdcq+LSiukXJRpGHVg==", 16 | "url": "ct.googleapis.com/logs/argon2019/", 17 | "maximum_merge_delay": 86400, 18 | "operated_by": [ 19 | 0 20 | ], 21 | "dns_api_endpoint": "argon2019.ct.googleapis.com" 22 | }, 23 | { 24 | "description": "Google 'Argon2020' log", 25 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6Tx2p1yKY4015NyIYvdrk36es0uAc1zA4PQ+TGRY+3ZjUTIYY9Wyu+3q/147JG4vNVKLtDWarZwVqGkg6lAYzA==", 26 | "url": "ct.googleapis.com/logs/argon2020/", 27 | "maximum_merge_delay": 86400, 28 | "operated_by": [ 29 | 0 30 | ], 31 | "dns_api_endpoint": "argon2020.ct.googleapis.com" 32 | }, 33 | { 34 | "description": "Google 'Argon2021' log", 35 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETeBmZOrzZKo4xYktx9gI2chEce3cw/tbr5xkoQlmhB18aKfsxD+MnILgGNl0FOm0eYGilFVi85wLRIOhK8lxKw==", 36 | "url": "ct.googleapis.com/logs/argon2021/", 37 | "maximum_merge_delay": 86400, 38 | "operated_by": [ 39 | 0 40 | ], 41 | "dns_api_endpoint": "argon2021.ct.googleapis.com" 42 | }, 43 | { 44 | "description": "Google 'Aviator' log", 45 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1/TMabLkDpCjiupacAlP7xNi0I1JYP8bQFAHDG1xhtolSY1l4QgNRzRrvSe8liE+NPWHdjGxfx3JhTsN9x8/6Q==", 46 | "url": "ct.googleapis.com/aviator/", 47 | "maximum_merge_delay": 86400, 48 | "operated_by": [ 49 | 0 50 | ], 51 | "final_sth": { 52 | "tree_size": 46466472, 53 | "timestamp": 1480512258330, 54 | "sha256_root_hash": "LcGcZRsm+LGYmrlyC5LXhV1T6OD8iH5dNlb0sEJl9bA=", 55 | "tree_head_signature": "BAMASDBGAiEA/M0Nvt77aNe+9eYbKsv6rRpTzFTKa5CGqb56ea4hnt8CIQCJDE7pL6xgAewMd5i3G1lrBWgFooT2kd3+zliEz5Rw8w==" 56 | }, 57 | "dns_api_endpoint": "aviator.ct.googleapis.com" 58 | }, 59 | { 60 | "description": "Google 'Icarus' log", 61 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETtK8v7MICve56qTHHDhhBOuV4IlUaESxZryCfk9QbG9co/CqPvTsgPDbCpp6oFtyAHwlDhnvr7JijXRD9Cb2FA==", 62 | "url": "ct.googleapis.com/icarus/", 63 | "maximum_merge_delay": 86400, 64 | "operated_by": [ 65 | 0 66 | ], 67 | "dns_api_endpoint": "icarus.ct.googleapis.com" 68 | }, 69 | { 70 | "description": "Google 'Pilot' log", 71 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA==", 72 | "url": "ct.googleapis.com/pilot/", 73 | "maximum_merge_delay": 86400, 74 | "operated_by": [ 75 | 0 76 | ], 77 | "dns_api_endpoint": "pilot.ct.googleapis.com" 78 | }, 79 | { 80 | "description": "Google 'Rocketeer' log", 81 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg==", 82 | "url": "ct.googleapis.com/rocketeer/", 83 | "maximum_merge_delay": 86400, 84 | "operated_by": [ 85 | 0 86 | ], 87 | "dns_api_endpoint": "rocketeer.ct.googleapis.com" 88 | }, 89 | { 90 | "description": "Google 'Skydiver' log", 91 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmyGDvYXsRJsNyXSrYc9DjHsIa2xzb4UR7ZxVoV6mrc9iZB7xjI6+NrOiwH+P/xxkRmOFG6Jel20q37hTh58rA==", 92 | "url": "ct.googleapis.com/skydiver/", 93 | "maximum_merge_delay": 86400, 94 | "operated_by": [ 95 | 0 96 | ], 97 | "dns_api_endpoint": "skydiver.ct.googleapis.com" 98 | }, 99 | { 100 | "description": "Cloudflare 'Nimbus2018' Log", 101 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAsVpWvrH3Ke0VRaMg9ZQoQjb5g/xh1z3DDa6IuxY5DyPsk6brlvrUNXZzoIg0DcvFiAn2kd6xmu4Obk5XA/nRg==", 102 | "url": "ct.cloudflare.com/logs/nimbus2018/", 103 | "maximum_merge_delay": 86400, 104 | "operated_by": [ 105 | 1 106 | ], 107 | "dns_api_endpoint": "cloudflare-nimbus2018.ct.googleapis.com" 108 | }, 109 | { 110 | "description": "Cloudflare 'Nimbus2019' Log", 111 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkZHz1v5r8a9LmXSMegYZAg4UW+Ug56GtNfJTDNFZuubEJYgWf4FcC5D+ZkYwttXTDSo4OkanG9b3AI4swIQ28g==", 112 | "url": "ct.cloudflare.com/logs/nimbus2019/", 113 | "maximum_merge_delay": 86400, 114 | "operated_by": [ 115 | 1 116 | ], 117 | "dns_api_endpoint": "cloudflare-nimbus2019.ct.googleapis.com" 118 | }, 119 | { 120 | "description": "Cloudflare 'Nimbus2020' Log", 121 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01EAhx4o0zPQrXTcYjgCt4MVFsT0Pwjzb1RwrM0lhWDlxAYPP6/gyMCXNkOn/7KFsjL7rwk78tHMpY8rXn8AYg==", 122 | "url": "ct.cloudflare.com/logs/nimbus2020/", 123 | "maximum_merge_delay": 86400, 124 | "operated_by": [ 125 | 1 126 | ], 127 | "dns_api_endpoint": "cloudflare-nimbus2020.ct.googleapis.com" 128 | }, 129 | { 130 | "description": "Cloudflare 'Nimbus2021' Log", 131 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExpon7ipsqehIeU1bmpog9TFo4Pk8+9oN8OYHl1Q2JGVXnkVFnuuvPgSo2Ep+6vLffNLcmEbxOucz03sFiematg==", 132 | "url": "ct.cloudflare.com/logs/nimbus2021/", 133 | "maximum_merge_delay": 86400, 134 | "operated_by": [ 135 | 1 136 | ], 137 | "dns_api_endpoint": "cloudflare-nimbus2021.ct.googleapis.com" 138 | }, 139 | { 140 | "description": "DigiCert Log Server", 141 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A==", 142 | "url": "ct1.digicert-ct.com/log/", 143 | "maximum_merge_delay": 86400, 144 | "operated_by": [ 145 | 2 146 | ], 147 | "dns_api_endpoint": "digicert.ct.googleapis.com" 148 | }, 149 | { 150 | "description": "DigiCert Log Server 2", 151 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzF05L2a4TH/BLgOhNKPoioYCrkoRxvcmajeb8Dj4XQmNY+gxa4Zmz3mzJTwe33i0qMVp+rfwgnliQ/bM/oFmhA==", 152 | "url": "ct2.digicert-ct.com/log/", 153 | "maximum_merge_delay": 86400, 154 | "operated_by": [ 155 | 2 156 | ], 157 | "dns_api_endpoint": "digicert2.ct.googleapis.com" 158 | }, 159 | { 160 | "description": "Symantec log", 161 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEluqsHEYMG1XcDfy1lCdGV0JwOmkY4r87xNuroPS2bMBTP01CEDPwWJePa75y9CrsHEKqAy8afig1dpkIPSEUhg==", 162 | "url": "ct.ws.symantec.com/", 163 | "maximum_merge_delay": 86400, 164 | "operated_by": [ 165 | 2 166 | ], 167 | "dns_api_endpoint": "symantec.ct.googleapis.com" 168 | }, 169 | { 170 | "description": "Symantec 'Vega' log", 171 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6pWeAv/u8TNtS4e8zf0ZF2L/lNPQWQc/Ai0ckP7IRzA78d0NuBEMXR2G3avTK0Zm+25ltzv9WWis36b4ztIYTQ==", 172 | "url": "vega.ws.symantec.com/", 173 | "maximum_merge_delay": 86400, 174 | "operated_by": [ 175 | 2 176 | ], 177 | "dns_api_endpoint": "symantec-vega.ct.googleapis.com" 178 | }, 179 | { 180 | "description": "Symantec 'Sirius' log", 181 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEowJkhCK7JewN47zCyYl93UXQ7uYVhY/Z5xcbE4Dq7bKFN61qxdglnfr0tPNuFiglN+qjN2Syxwv9UeXBBfQOtQ==", 182 | "url": "sirius.ws.symantec.com/", 183 | "maximum_merge_delay": 86400, 184 | "operated_by": [ 185 | 2 186 | ], 187 | "dns_api_endpoint": "symantec-sirius.ct.googleapis.com" 188 | }, 189 | { 190 | "description": "Certly.IO log", 191 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECyPLhWKYYUgEc+tUXfPQB4wtGS2MNvXrjwFCCnyYJifBtd2Sk7Cu+Js9DNhMTh35FftHaHu6ZrclnNBKwmbbSA==", 192 | "url": "log.certly.io/", 193 | "maximum_merge_delay": 86400, 194 | "operated_by": [ 195 | 3 196 | ], 197 | "disqualified_at": 1460678400, 198 | "dns_api_endpoint": "certly.ct.googleapis.com" 199 | }, 200 | { 201 | "description": "Izenpe log", 202 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ2Q5DC3cUBj4IQCiDu0s6j51up+TZAkAEcQRF6tczw90rLWXkJMAW7jr9yc92bIKgV8vDXU4lDeZHvYHduDuvg==", 203 | "url": "ct.izenpe.com/", 204 | "maximum_merge_delay": 86400, 205 | "operated_by": [ 206 | 4 207 | ], 208 | "disqualified_at": 1464566400, 209 | "dns_api_endpoint": "izenpe1.ct.googleapis.com" 210 | }, 211 | { 212 | "description": "WoSign log", 213 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBGIey1my66PTTBmJxklIpMhRrQvAdPG+SvVyLpzmwai8IoCnNBrRhgwhbrpJIsO0VtwKAx+8TpFf1rzgkJgMQ==", 214 | "url": "ctlog.wosign.com/", 215 | "maximum_merge_delay": 86400, 216 | "operated_by": [ 217 | 5 218 | ], 219 | "disqualified_at": 1518479999, 220 | "dns_api_endpoint": "wosign1.ct.googleapis.com" 221 | }, 222 | { 223 | "description": "Venafi log", 224 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolpIHxdSlTXLo1s6H1OCdpSj/4DyHDc8wLG9wVmLqy1lk9fz4ATVmm+/1iN2Nk8jmctUKK2MFUtlWXZBSpym97M7frGlSaQXUWyA3CqQUEuIJOmlEjKTBEiQAvpfDjCHjlV2Be4qTM6jamkJbiWtgnYPhJL6ONaGTiSPm7Byy57iaz/hbckldSOIoRhYBiMzeNoA0DiRZ9KmfSeXZ1rB8y8X5urSW+iBzf2SaOfzBvDpcoTuAaWx2DPazoOl28fP1hZ+kHUYvxbcMjttjauCFx+JII0dmuZNIwjfeG/GBb9frpSX219k1O4Wi6OEbHEr8at/XQ0y7gTikOxBn/s5wQIDAQAB", 225 | "url": "ctlog.api.venafi.com/", 226 | "maximum_merge_delay": 86400, 227 | "operated_by": [ 228 | 6 229 | ], 230 | "disqualified_at": 1488307346, 231 | "dns_api_endpoint": "venafi.ct.googleapis.com" 232 | }, 233 | { 234 | "description": "Venafi Gen2 CT log", 235 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjicnerZVCXTrbEuUhGW85BXx6lrYfA43zro/bAna5ymW00VQb94etBzSg4j/KS/Oqf/fNN51D8DMGA2ULvw3AQ==", 236 | "url": "ctlog-gen2.api.venafi.com/", 237 | "maximum_merge_delay": 86400, 238 | "operated_by": [ 239 | 6 240 | ], 241 | "dns_api_endpoint": "venafi2.ct.googleapis.com" 242 | }, 243 | { 244 | "description": "CNNIC CT log", 245 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7UIYZopMgTTJWPp2IXhhuAf1l6a9zM7gBvntj5fLaFm9pVKhKYhVnno94XuXeN8EsDgiSIJIj66FpUGvai5samyetZhLocRuXhAiXXbDNyQ4KR51tVebtEq2zT0mT9liTtGwiksFQccyUsaVPhsHq9gJ2IKZdWauVA2Fm5x9h8B9xKn/L/2IaMpkIYtd967TNTP/dLPgixN1PLCLaypvurDGSVDsuWabA3FHKWL9z8wr7kBkbdpEhLlg2H+NAC+9nGKx+tQkuhZ/hWR65aX+CNUPy2OB9/u2rNPyDydb988LENXoUcMkQT0dU3aiYGkFAY0uZjD2vH97TM20xYtNQIDAQAB", 246 | "url": "ctserver.cnnic.cn/", 247 | "maximum_merge_delay": 86400, 248 | "operated_by": [ 249 | 7 250 | ], 251 | "dns_api_endpoint": "cnnic.ct.googleapis.com" 252 | }, 253 | { 254 | "description": "StartCom log", 255 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESPNZ8/YFGNPbsu1Gfs/IEbVXsajWTOaft0oaFIZDqUiwy1o/PErK38SCFFWa+PeOQFXc9NKv6nV0+05/YIYuUQ==", 256 | "url": "ct.startssl.com/", 257 | "maximum_merge_delay": 86400, 258 | "operated_by": [ 259 | 8 260 | ], 261 | "disqualified_at": 1518479999, 262 | "dns_api_endpoint": "startcom1.ct.googleapis.com" 263 | }, 264 | { 265 | "description": "Comodo 'Sabre' CT log", 266 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8m/SiQ8/xfiHHqtls9m7FyOMBg4JVZY9CgiixXGz0akvKD6DEL8S0ERmFe9U4ZiA0M4kbT5nmuk3I85Sk4bagA==", 267 | "url": "sabre.ct.comodo.com/", 268 | "maximum_merge_delay": 86400, 269 | "operated_by": [ 270 | 9 271 | ], 272 | "dns_api_endpoint": "comodo-sabre.ct.googleapis.com" 273 | }, 274 | { 275 | "description": "Comodo 'Mammoth' CT log", 276 | "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7+R9dC4VFbbpuyOL+yy14ceAmEf7QGlo/EmtYU6DRzwat43f/3swtLr/L8ugFOOt1YU/RFmMjGCL17ixv66MZw==", 277 | "url": "mammoth.ct.comodo.com/", 278 | "maximum_merge_delay": 86400, 279 | "operated_by": [ 280 | 9 281 | ], 282 | "dns_api_endpoint": "comodo-mammoth.ct.googleapis.com" 283 | } 284 | ], 285 | "operators": [ 286 | { 287 | "name": "Google", 288 | "id": 0 289 | }, 290 | { 291 | "name": "Cloudflare", 292 | "id": 1 293 | }, 294 | { 295 | "name": "DigiCert", 296 | "id": 2 297 | }, 298 | { 299 | "name": "Certly", 300 | "id": 3 301 | }, 302 | { 303 | "name": "Izenpe", 304 | "id": 4 305 | }, 306 | { 307 | "name": "WoSign", 308 | "id": 5 309 | }, 310 | { 311 | "name": "Venafi", 312 | "id": 6 313 | }, 314 | { 315 | "name": "CNNIC", 316 | "id": 7 317 | }, 318 | { 319 | "name": "StartCom", 320 | "id": 8 321 | }, 322 | { 323 | "name": "Comodo CA Limited", 324 | "id": 9 325 | } 326 | ] 327 | } -------------------------------------------------------------------------------- /tests/data/test_decompose_cert/cert.b64: -------------------------------------------------------------------------------- 1 | MIInnzCCJoegAwIBAgIILuX0ATzKyxAwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRlcm5ldCBBdXRob3JpdHkgRzIwHhcNMTcwNzE5MTEzMDAwWhcNMTcxMDExMTEzMDAwWjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWlzYy5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwot1XdX0HCODHDVDJYGg8PJYjCatVCEk4L+YN5yLGYfNcvEyzlZZUwKUAckXcK1Bxa049DvPxdrZjbUqcjyykgX4at3X5wdplFtVmfOxF7p43H4zoa3xG22xDaeOatcvYu6gjs9kNVMlYfwuPNF5GtQvj/qDyC62rmmVTpWnfyfwibq5dNDR6/F4hXWNlc9er/D3eULnkc8KsFkX2TP4+Y7PPSC32+VqjY0QZZV5uH9mwzznHz4caO6XwJM+y3T/1oOL5zbJlbMUaoBEn00Ey00ll2oYspI3IKOmQmRA7mUXRa7nKq42hu7BTUU+AharoMwSQH531+mlWdk+5WyA+QIDAQABo4IkaTCCJGUwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMIIjNQYDVR0RBIIjLDCCIyiCD21pc2MuZ29vZ2xlLmNvbYIUKi4xaG91cnBlcnNlY29uZC5jb22CGiouYWNjZWxlcmF0ZXdpdGhnb29nbGUuY29tghQqLmFjdGlvbnMuZ29vZ2xlLmNvbYIOKi5hZGdvb2dsZS5uZXSCDCouYWRtZWxkLmNvbYIaKi5hZHZlcnRpc2Vyc2NvbW11bml0eS5jb22CFyouYWR3b3Jkcy1jb21tdW5pdHkuY29tghQqLmFkd29yZHNleHByZXNzLmNvbYIQKi5hZ29vZ2xlZGF5LmNvbYIRKi5hbmd1bGFyZGFydC5vcmeCCCouYXBpLmFpghMqLmFwcGVuZ2luZWRlbW8uY29tggwqLmFwdHVyZS5jb22CDSouYXJlYTEyMC5jb22CECouYXJ0cHJvamVjdC5jb22CFSouYmFzZWxpbmUuZ29vZ2xlLmNvbYITKi5iYXNlbGluZXN0dWR5LmNvbYITKi5iYXNlbGluZXN0dWR5Lm9yZ4ITKi5iZWF0dGhhdHF1b3RlLmNvbYIRKi5iZXJ0YXBwd2FyZC5jb22CCyouYmxpbmsub3JnggwqLmJyb3RsaS5vcmeCDyouYnVtcHNoYXJlLmNvbYIMKi5idW1wdG9wLmNhgg0qLmJ1bXB0b3AuY29tgg0qLmJ1bXB0b3AubmV0gg0qLmJ1bXB0b3Aub3Jngg8qLmJ1bXB0dW5lcy5jb22CFCouY2FtcHVzbG9uZG9uLmNvLnVrghIqLmNhbXB1c2xvbmRvbi5jb22CEyouY2FtcHVzdGVsYXZpdi5jb22CHiouY2VydGlmaWNhdGUtdHJhbnNwYXJlbmN5Lm9yZ4IMKi5jaHJvbWUuY29tghAqLmNocm9tZWJvb2suY29tghEqLmNocm9tZWJvb2tzLmNvbYIQKi5jaHJvbWVjYXN0LmNvbYISKi5jaHJvbWVzaG9ydHMuY29tgg4qLmNocm9taXVtLm9yZ4IYKi5jbG91ZGJ1cnN0cmVzZWFyY2guY29tghQqLmNsb3VkZnVuY3Rpb25zLm5ldIITKi5jbG91ZHJvYm90aWNzLmNvbYIPKi5jb25zY3J5cHQuY29tgg8qLmNvbnNjcnlwdC5vcmeCGyouY29uc3VtZXJiYXJvbWV0ZXIyMDEzLmNvbYILKi5jb292YS5jb22CCyouY29vdmEubmV0ggsqLmNvb3ZhLm9yZ4IJKi5jcnIuY29tggsqLmNzNGhzLmNvbYISKi5jdWx0dXJhbHNwb3QuY29tghAqLmRhcnRzdW1taXQuY29tghUqLmRhdGEtdm9jYWJ1bGFyeS5vcmeCCyouZGVidWcuY29tghIqLmRlYnVncHJvamVjdC5jb22CDyouZGVzaWduLmdvb2dsZYIWKi5kZXZlbG9wZXIuZ29vZ2xlLmNvbYIXKi5kZXZlbG9wZXJzLmdvb2dsZS5jb22CICouZGV2ZWxvcGVyc2d1aWRldG9hcHBnYWxheHkuY29tgiMqLmRldmVsb3BlcnNndWlkZXRvdGhlYXBwZ2FsYXh5LmNvbYIZKi5kZXZndWlkZXRvYXBwZ2FsYXh5LmNvbYIcKi5kZXZndWlkZXRvdGhlYXBwZ2FsYXh5LmNvbYIRKi5kZXZzaXRldGVzdC5ob3eCFCouZGlnaXRhbHdvcmtzaG9wLmRlghMqLmRvb2RsZTRnb29nbGUuY29tghQqLmVudmlyb25tZW50Lmdvb2dsZYIOKi5lcGlzb2RpYy5jb22CECouZmVlZGJ1cm5lci5jb22CDCouZmZsaWNrLmNvbYIZKi5maWJlcmZvcmNvbW11bml0aWVzLmNvbYIYKi5maW5hbmNlbGVhZHNvbmxpbmUuY29tghUqLmZpcmViYXNlLmdvb2dsZS5jb22CECouZmx1dHRlcmFwcC5jb22CFCouZnJlZWFuZG9wZW53ZWIuY29tggsqLmctdHVuLmNvbYIRKi5nYWxheHluZXh1cy5jb22CEyouZ2FtaW5neW91dHViZS5jb22CDCouZ2Jiby5jby51a4IXKi5nYmMuYmVhdHRoYXRxdW90ZS5jb22CFiouZ2Vycml0Y29kZXJldmlldy5jb22CECouZ2V0YnVtcHRvcC5jb22CHCouZ2V0ZXZlcnlidXNpbmVzc29ubGluZS5jb22CEyouZ2V0Z29vZ2xlYmFjay5jb22CFyouZ2V0eW91cmdvb2dsZWJhY2suY29tgg4qLmdpcHNjb3JwLmNvbYIVKi5nbGFzcy1jb21tdW5pdHkuY29tgg8qLmdsb2JhbGVkdS5vcmeCFiouZ29sZGVuaG91cmNhbWVyYS5jb22CEiouZ29uZ2xjaHVhbmdsLm5ldIIPKi5nb29nbGUuYmVybGlughAqLmdvb2dsZS5kb21haW5zggwqLmdvb2dsZS5vcmeCESouZ29vZ2xlLnZlbnR1cmVzghAqLmdvb2dsZWFkYXkuY29tghUqLmdvb2dsZWFuYWx5dGljcy5jb22CECouZ29vZ2xlYXBwcy5jb22CFiouZ29vZ2xlYXJ0cHJvamVjdC5jb22CFiouZ29vZ2xlYXJ0cHJvamVjdC5vcmeCFCouZ29vZ2xlYnJhbmRsYWIuY29tghUqLmdvb2dsZWNvbXBhcmUuY28udWuCEyouZ29vZ2xlZGFubWFyay5jb22CEyouZ29vZ2xlZG9tYWlucy5jb22CFSouZ29vZ2xlZWxlY3Rpb25zLmNvbYIWKi5nb29nbGVlbnRlcnByaXNlLmNvbYITKi5nb29nbGVmaW5sYW5kLmNvbYIXKi5nb29nbGVmb3J2ZXRlcmFucy5jb22CEyouZ29vZ2xlZm9yd29yay5jb22CESouZ29vZ2xlaWRlYXMuY29tghEqLmdvb2dsZWlkZWFzLm9yZ4IYKi5nb29nbGVpbnNpZGVzZWFyY2guY29tghAqLmdvb2dsZW1hcHMuY29tghIqLmdvb2dsZXBob3Rvcy5jb22CECouZ29vZ2xlcGxheS5jb22CECouZ29vZ2xlcGx1cy5jb22CEyouZ29vZ2xlc3ZlcmlnZS5jb22CHCouZ29vZ2xldHJhdmVsYWRzZXJ2aWNlcy5jb22CGSouZ29vZ2xldmVuZG9yY29udGVudC5jb22CFCouZ29vZ2xldmVudHVyZXMuY29tghkqLmdyZWdhbmRnbG9yaWFzcGl6emEuY29tggkqLmdzcmMuaW+CFiouZ3VpZGV0b2FwcGdhbGF4eS5jb22CGSouZ3VpZGV0b3RoZWFwcGdhbGF4eS5jb22CDiouaGluZGl3ZWIuY29tghIqLmhvd3RvZ2V0bW8uY28udWuCECouaHRtbDVyb2Nrcy5jb22CCiouaHdnby5jb22CDyouaW1wZXJtaXVtLmNvbYIMKi5qMm9iamMub3Jngg4qLmphY3F1YXJkLmNvbYIOKi5qYW1ib2FyZC5jb22CFSoua2V5dHJhbnNwYXJlbmN5LmNvbYIVKi5rZXl0cmFuc3BhcmVuY3kuZm9vghUqLmtleXRyYW5zcGFyZW5jeS5vcmeCECoubG9vbmZvcmFsbC5jb22CEioubWFkZXdpdGhjb2RlLmNvbYINKi5tZGlhbG9nLmNvbYIWKi5tZXNzYWdlc2ZvcmphcGFuLm9yZ4ITKi5tZmctaW5zcGVjdG9yLmNvbYIRKi5taXJhaWtpcm9rdS5jb22CESoubWlyYWlrb3Jva3UuY29tghYqLm1vYmlsZXJlc2VhcmNodWIuY29tghEqLm1vYmlsZXZpZXcucGFnZYIMKi5teWdiaXouY29tggkqLm5lYXIuYnmCFSoubmV0ei12ZXJ0ZWlkaWdlci5kZYIUKi5uZXR6dmVydGVpZGlnZXIuZGWCDCoub2F1dGh6LmNvbYIJKi5vbi5oZXJlggkqLm9uMi5jb22CDioub25ldG9kYXkuY29tgg4qLm9uZXRvZGF5Lm9yZ4IZKi5vbmV3b3JsZG1hbnlzdG9yaWVzLmNvbYIMKi5vbmdiaXouY29tghkqLm9wZW5oYW5kc2V0YWxsaWFuY2UuY29tghMqLm9ybmVrLWlzbGV0bWUuY29tghgqLnBhZ2VzcGVlZG1vYmlsaXplci5jb22CFioucGFydHlsaWtlaXRzMTk4Ni5vcmeCECoucGF4bGljZW5zZS5vcmeCGSoucGVyc29uZmluZGVyLmdvb2dsZS5vcmeCESoucGhvdG9zcGhlcmUuY29tggwqLnBpY2FzYS5jb22CDioucGl0dHBhdHQuY29tghQqLnBvbHltZXJwcm9qZWN0Lm9yZ4INKi5wb3N0aW5pLmNvbYIUKi5wcml2YWN5Y2hvaWNlcy5vcmeCECoucHJvamVjdGFyYS5jb22CFSoucHJvamVjdGJhc2VsaW5lLmNvbYIRKi5wcm9qZWN0bG9vbi5jb22CESoucHJvamVjdHNvbGkuY29tghEqLnF1ZXN0dmlzdWFsLmNvbYIRKi5xdWlja29mZmljZS5jb22CDSoucXVpa3NlZS5jb22CHioucXVvdGVwcm94eS5iZWF0dGhhdHF1b3RlLmNvbYIPKi5yZWNhcHRjaGEubmV0ggwqLnJldm9sdi5jb22CFioucmV3b3Jrd2l0aGdvb2dsZS5jb22CESoucmlkZXBlbmd1aW4uY29tghcqLnJpZ2h0bGFuZWJpa2VzaG9wLmNvbYIUKi5zLnN2Yy0xLmdvb2dsZS5jb22CDCouc2F5bm93LmNvbYINKi5zY2hlbWVyLmNvbYIWKi5zY3JlZW53aXNlc2VsZWN0LmNvbYIWKi5zY3JlZW53aXNldHJlbmRzLmNvbYIbKi5zY3JlZW53aXNldHJlbmRzcGFuZWwuY29tgg8qLnNoaWJib2xldGgudHaCDiouc25hcHNlZWQuY29tgg8qLnNvbHZlZm9yeC5jb22CCyouc3BpZGVyLmlvghYqLnN0YWdpbmcud2lkZXZpbmUuY29tgiwqLnN0b3JhZ2UtbmlnaHRseS10ZXN0Lmdvb2dsZXVzZXJjb250ZW50LmNvbYIsKi5zdG9yYWdlLXN0YWdpbmctdGVzdC5nb29nbGV1c2VyY29udGVudC5jb22CKSouc3RvcmFnZS10ZXN0LXRlc3QuZ29vZ2xldXNlcmNvbnRlbnQuY29tghQqLnN1cHBvcnQuZ29vZ2xlLmNvbYIWKi50ZWFjaHBhcmVudHN0ZWNoLmNvbYIWKi50ZWFjaHBhcmVudHN0ZWNoLm9yZ4IQKi50ZW5zb3JmbG93Lm9yZ4IUKi50aGVjbGV2ZXJzZW5zZS5jb22CGCoudGhlZGlnaXRhbGdhcmFnZS5jby51a4IYKi50aGVnbGFzc2NvbGxlY3RpdmUuY29tghYqLnRoaW5rcXVhcnRlcmx5LmNvLnVrghQqLnRoaW5rcXVhcnRlcmx5LmNvbYIYKi50b2FzdGVkYmVhbnNjb2ZmZWUuY29tgg0qLnR4Y2xvdWQubmV0ggsqLnR4dmlhLmNvbYISKi51YXQud2lkZXZpbmUuY29tghkqLnVudGVybmVobWVuLWJlaXNwaWVsLmRlgg8qLnVzZXBsYW5uci5jb22CDyoudjhwcm9qZWN0Lm9yZ4IMKi52ZXJpbHkuY29tghgqLnZlcmlseWxpZmVzY2llbmNlcy5jb22CDCoud2FsbGV0LmNvbYILKi53YXltby5jb22CCioud2F6ZS5jb22CFioud2ViYXBwZmllbGRndWlkZS5jb22CESoud2Vzb2x2ZWZvcnguY29tghEqLndoYXRicm93c2VyLmNvbYIRKi53aGF0YnJvd3Nlci5vcmeCDioud2lkZXZpbmUuY29tgg8qLndvbWVud2lsbC5jb22CCyoueC5jb21wYW55gggqLngudGVhbYIRKi54bi0tOXRyczY1Yi5jb22CEyoueW91dHViZWdhbWluZy5jb22CGioueW91dHViZW1vYmlsZXN1cHBvcnQuY29tggsqLnphZ2F0LmNvbYISMWhvdXJwZXJzZWNvbmQuY29tghhhY2NlbGVyYXRld2l0aGdvb2dsZS5jb22CDGFkZ29vZ2xlLm5ldIIKYWRtZWxkLmNvbYIXYWR2ZXJ0aXNlcmNvbW11bml0eS5jb22CGGFkdmVydGlzZXJzY29tbXVuaXR5LmNvbYIVYWR3b3Jkcy1jb21tdW5pdHkuY29tghJhZHdvcmRzZXhwcmVzcy5jb22CDmFnb29nbGVkYXkuY29tgg9hbmd1bGFyZGFydC5vcmeCBmFwaS5haYIRYXBwZW5naW5lZGVtby5jb22CCmFwdHVyZS5jb22CC2FyZWExMjAuY29tgg5hcnRwcm9qZWN0LmNvbYIRYmFzZWxpbmVzdHVkeS5jb22CEWJhc2VsaW5lc3R1ZHkub3JnghFiZWF0dGhhdHF1b3RlLmNvbYIPYmVydGFwcHdhcmQuY29tgglibGluay5vcmeCCmJyb3RsaS5vcmeCDWJ1bXBzaGFyZS5jb22CCmJ1bXB0b3AuY2GCC2J1bXB0b3AuY29tggtidW1wdG9wLm5ldIILYnVtcHRvcC5vcmeCDWJ1bXB0dW5lcy5jb22CEmNhbXB1c2xvbmRvbi5jby51a4IQY2FtcHVzbG9uZG9uLmNvbYIRY2FtcHVzdGVsYXZpdi5jb22CHGNlcnRpZmljYXRlLXRyYW5zcGFyZW5jeS5vcmeCCmNocm9tZS5jb22CDmNocm9tZWJvb2suY29tgg9jaHJvbWVib29rcy5jb22CDmNocm9tZWNhc3QuY29tghBjaHJvbWVzaG9ydHMuY29tggxjaHJvbWl1bS5vcmeCGWNsaWNrc2VydmUuZGFydHNlYXJjaC5uZXSCHGNsaWNrc2VydmUuZXUuZGFydHNlYXJjaC5uZXSCHGNsaWNrc2VydmUudWsuZGFydHNlYXJjaC5uZXSCHWNsaWNrc2VydmUudXMyLmRhcnRzZWFyY2gubmV0ghljbGlja3NlcnZlci5nb29nbGVhZHMuY29tghZjbG91ZGJ1cnN0cmVzZWFyY2guY29tghJjbG91ZGZ1bmN0aW9ucy5uZXSCEWNsb3Vkcm9ib3RpY3MuY29tgg1jb25zY3J5cHQuY29tgg1jb25zY3J5cHQub3Jnghljb25zdW1lcmJhcm9tZXRlcjIwMTMuY29tghFjb29raWVjaG9pY2VzLm9yZ4IJY29vdmEuY29tggljb292YS5uZXSCCWNvb3ZhLm9yZ4IHY3JyLmNvbYIJY3M0aHMuY29tghBjdWx0dXJhbHNwb3QuY29tgg5kYXJ0c3VtbWl0LmNvbYITZGF0YS12b2NhYnVsYXJ5Lm9yZ4IJZGVidWcuY29tghBkZWJ1Z3Byb2plY3QuY29tgg1kZXNpZ24uZ29vZ2xlghpkZXZlbG9wZXIuZGV2Lm5lc3RsYWJzLmNvbYIVZGV2ZWxvcGVyLmZ0Lm5lc3QuY29tgiJkZXZlbG9wZXIuaW50ZWdyYXRpb24ubmVzdGxhYnMuY29tghJkZXZlbG9wZXIubmVzdC5jb22CGWRldmVsb3Blci5xYS5uZXN0bGFicy5jb22CHWRldmVsb3Blci5zdGFibGUubmVzdGxhYnMuY29tghtkZXZlbG9wZXJzLmRldi5uZXN0bGFicy5jb22CFmRldmVsb3BlcnMuZnQubmVzdC5jb22CI2RldmVsb3BlcnMuaW50ZWdyYXRpb24ubmVzdGxhYnMuY29tghNkZXZlbG9wZXJzLm5lc3QuY29tghpkZXZlbG9wZXJzLnFhLm5lc3RsYWJzLmNvbYIeZGV2ZWxvcGVycy5zdGFibGUubmVzdGxhYnMuY29tgh5kZXZlbG9wZXJzZ3VpZGV0b2FwcGdhbGF4eS5jb22CIWRldmVsb3BlcnNndWlkZXRvdGhlYXBwZ2FsYXh5LmNvbYIXZGV2Z3VpZGV0b2FwcGdhbGF4eS5jb22CGmRldmd1aWRldG90aGVhcHBnYWxheHkuY29tgg9kZXZzaXRldGVzdC5ob3eCEmRpZ2l0YWx3b3Jrc2hvcC5kZYIRZG9vZGxlNGdvb2dsZS5jb22CEmVudmlyb25tZW50Lmdvb2dsZYIMZXBpc29kaWMuY29tgg5mZWVkYnVybmVyLmNvbYIKZmZsaWNrLmNvbYIXZmliZXJmb3Jjb21tdW5pdGllcy5jb22CFmZpbmFuY2VsZWFkc29ubGluZS5jb22CDmZsdXR0ZXJhcHAuY29tghJmcmVlYW5kb3BlbndlYi5jb22CCWctdHVuLmNvbYIPZ2FsYXh5bmV4dXMuY29tghFnYW1pbmd5b3V0dWJlLmNvbYIKZ2Jiby5jby51a4IVZ2JjLmJlYXR0aGF0cXVvdGUuY29tghRnZXJyaXRjb2RlcmV2aWV3LmNvbYIOZ2V0YnVtcHRvcC5jb22CGmdldGV2ZXJ5YnVzaW5lc3NvbmxpbmUuY29tghFnZXRnb29nbGViYWNrLmNvbYIVZ2V0eW91cmdvb2dsZWJhY2suY29tggxnaXBzY29ycC5jb22CE2dsYXNzLWNvbW11bml0eS5jb22CDWdsb2JhbGVkdS5vcmeCFGdvbGRlbmhvdXJjYW1lcmEuY29tghBnb25nbGNodWFuZ2wubmV0gg5nb29nLmRtdHJ5LmNvbYINZ29vZ2xlLmJlcmxpboIOZ29vZ2xlLmRvbWFpbnOCCmdvb2dsZS5vcmeCD2dvb2dsZS52ZW50dXJlc4IOZ29vZ2xlYWRheS5jb22CE2dvb2dsZWFuYWx5dGljcy5jb22CDmdvb2dsZWFwcHMuY29tghRnb29nbGVhcnRwcm9qZWN0LmNvbYIUZ29vZ2xlYXJ0cHJvamVjdC5vcmeCEmdvb2dsZWJyYW5kbGFiLmNvbYITZ29vZ2xlY29tcGFyZS5jby51a4IRZ29vZ2xlZGFubWFyay5jb22CEWdvb2dsZWRvbWFpbnMuY29tghNnb29nbGVlbGVjdGlvbnMuY29tghRnb29nbGVlbnRlcnByaXNlLmNvbYIRZ29vZ2xlZmlubGFuZC5jb22CFWdvb2dsZWZvcnZldGVyYW5zLmNvbYIRZ29vZ2xlZm9yd29yay5jb22CD2dvb2dsZWlkZWFzLmNvbYIPZ29vZ2xlaWRlYXMub3JnghZnb29nbGVpbnNpZGVzZWFyY2guY29tgg5nb29nbGVtYXBzLmNvbYIQZ29vZ2xlcGhvdG9zLmNvbYIOZ29vZ2xlcGxheS5jb22CDmdvb2dsZXBsdXMuY29tghFnb29nbGVzdmVyaWdlLmNvbYIaZ29vZ2xldHJhdmVsYWRzZXJ2aWNlcy5jb22CF2dvb2dsZXZlbmRvcmNvbnRlbnQuY29tghJnb29nbGV2ZW50dXJlcy5jb22CF2dyZWdhbmRnbG9yaWFzcGl6emEuY29tggdnc3JjLmlvghRndWlkZXRvYXBwZ2FsYXh5LmNvbYIXZ3VpZGV0b3RoZWFwcGdhbGF4eS5jb22CDGhpbmRpd2ViLmNvbYIQaG93dG9nZXRtby5jby51a4IOaHRtbDVyb2Nrcy5jb22CCGh3Z28uY29tghBpbWFnZXMuemFnYXQuY29tgg1pbXBlcm1pdW0uY29tggpqMm9iamMub3JnggxqYWNxdWFyZC5jb22CDGphbWJvYXJkLmNvbYIManMuZG10cnkuY29tghNrZXl0cmFuc3BhcmVuY3kuY29tghNrZXl0cmFuc3BhcmVuY3kuZm9vghNrZXl0cmFuc3BhcmVuY3kub3Jngg5sb29uZm9yYWxsLmNvbYIQbWFkZXdpdGhjb2RlLmNvbYILbWRpYWxvZy5jb22CFG1lc3NhZ2VzZm9yamFwYW4ub3JnghFtZmctaW5zcGVjdG9yLmNvbYIPbWlyYWlraXJva3UuY29tgg9taXJhaWtvcm9rdS5jb22CFG1vYmlsZXJlc2VhcmNodWIuY29tgg9tb2JpbGV2aWV3LnBhZ2WCCm15Z2Jpei5jb22CD24zMzkuYXNwLWNjLmNvbYIHbmVhci5ieYITbmV0ei12ZXJ0ZWlkaWdlci5kZYISbmV0enZlcnRlaWRpZ2VyLmRlggpvYXV0aHouY29tggdvbi5oZXJlggdvbjIuY29tggxvbmV0b2RheS5jb22CDG9uZXRvZGF5Lm9yZ4IXb25ld29ybGRtYW55c3Rvcmllcy5jb22CCm9uZ2Jpei5jb22CF29wZW5oYW5kc2V0YWxsaWFuY2UuY29tghFvcm5lay1pc2xldG1lLmNvbYIWcGFnZXNwZWVkbW9iaWxpemVyLmNvbYIUcGFydHlsaWtlaXRzMTk4Ni5vcmeCDnBheGxpY2Vuc2Uub3Jngg9waG90b3NwaGVyZS5jb22CCnBpY2FzYS5jb22CGnBpbmcuZmVlZGJ1cm5lci5nb29nbGUuY29tggxwaXR0cGF0dC5jb22CEnBvbHltZXJwcm9qZWN0Lm9yZ4ILcG9zdGluaS5jb22CEnByaXZhY3ljaG9pY2VzLm9yZ4IOcHJvamVjdGFyYS5jb22CE3Byb2plY3RiYXNlbGluZS5jb22CD3Byb2plY3Rsb29uLmNvbYIPcHJvamVjdHNvbGkuY29tgg9xdWVzdHZpc3VhbC5jb22CC3F1aWtzZWUuY29tghxxdW90ZXByb3h5LmJlYXR0aGF0cXVvdGUuY29tgg1yZWNhcHRjaGEubmV0ggpyZXZvbHYuY29tghRyZXdvcmt3aXRoZ29vZ2xlLmNvbYIPcmlkZXBlbmd1aW4uY29tghVyaWdodGxhbmViaWtlc2hvcC5jb22CFnJvb3RtdXNpYy5iYW5kcGFnZS5jb22CEnMuc3ZjLTEuZ29vZ2xlLmNvbYIKc2F5bm93LmNvbYILc2NoZW1lci5jb22CFHNjcmVlbndpc2VzZWxlY3QuY29tghRzY3JlZW53aXNldHJlbmRzLmNvbYIZc2NyZWVud2lzZXRyZW5kc3BhbmVsLmNvbYIMc25hcHNlZWQuY29tgg1zb2x2ZWZvcnguY29tgglzcGlkZXIuaW+CFHRlYWNocGFyZW50c3RlY2guY29tghR0ZWFjaHBhcmVudHN0ZWNoLm9yZ4IOdGVuc29yZmxvdy5vcmeCEnRoZWNsZXZlcnNlbnNlLmNvbYIWdGhlZGlnaXRhbGdhcmFnZS5jby51a4IWdGhlZ2xhc3Njb2xsZWN0aXZlLmNvbYIUdGhpbmtxdWFydGVybHkuY28udWuCEnRoaW5rcXVhcnRlcmx5LmNvbYIWdG9hc3RlZGJlYW5zY29mZmVlLmNvbYILdHhjbG91ZC5uZXSCCXR4dmlhLmNvbYIXdW50ZXJuZWhtZW4tYmVpc3BpZWwuZGWCDXVzZXBsYW5uci5jb22CDXY4cHJvamVjdC5vcmeCCnZlcmlseS5jb22CFnZlcmlseWxpZmVzY2llbmNlcy5jb22CCndhbGxldC5jb22CCXdheW1vLmNvbYIId2F6ZS5jb22CFHdlYmFwcGZpZWxkZ3VpZGUuY29tghJ3ZWx0d2VpdHdhY2hzZW4uZGWCD3dlc29sdmVmb3J4LmNvbYIPd2hhdGJyb3dzZXIuY29tgg93aGF0YnJvd3Nlci5vcmeCDXdvbWVud2lsbC5jb22CG3d3dy5hZHZlcnRpc2VyY29tbXVuaXR5LmNvbYIQd3d3LmJhbmRwYWdlLmNvbYIVd3d3LmNvb2tpZWNob2ljZXMub3JnghZ3d3cud2VsdHdlaXR3YWNoc2VuLmRlggl4LmNvbXBhbnmCBngudGVhbYIPeG4tLTl0cnM2NWIuY29tghF5b3V0dWJlZ2FtaW5nLmNvbYIYeW91dHViZW1vYmlsZXN1cHBvcnQuY29tggl6YWdhdC5jb20waAYIKwYBBQUHAQEEXDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0GA1UdDgQWBBSJ2VukMD82QpG2M0kGyP9MNY0XejAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMCEGA1UdIAQaMBgwDAYKKwYBBAHWeQIFATAIBgZngQwBAgIwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcyLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAMuMQYyUExA5/pAtNUJH+1RNqYWypGPANfaXqSfEgcoMPXkw//ue69ll7MhbksfoL7mjanYYDqWCE2da8Q+ZhhQScMGQf3e/AccGVd+3FFlqMpwdMP9NABqmKdU4uKxg6oyqcWy3Jbamh8rp7G247Y3iVr+RCVe3V+m9Z6+KAXcU2Lcn73KIg6/wjC4RYm8NGN1uunKfBsf8uk8keFbW89ENEJzcYXZFVmmTGLyZrnA9ith6bKpaUxuyd7uu6UvpXMehW10pFsS3FWTVAkJ+kZtvc8EKo0CkDTWDNArQOcYuxsUY/vgliVdLVwvzMii1fYT4Gew1fwWA6kr27S0g3kQ== 2 | -------------------------------------------------------------------------------- /tests/data/test_decompose_cert/cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_decompose_cert/cert.der -------------------------------------------------------------------------------- /tests/data/test_decompose_cert/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIInnzCCJoegAwIBAgIILuX0ATzKyxAwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UE 3 | BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl 4 | cm5ldCBBdXRob3JpdHkgRzIwHhcNMTcwNzE5MTEzMDAwWhcNMTcxMDExMTEzMDAw 5 | WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN 6 | TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWlz 7 | Yy5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwot1 8 | XdX0HCODHDVDJYGg8PJYjCatVCEk4L+YN5yLGYfNcvEyzlZZUwKUAckXcK1Bxa04 9 | 9DvPxdrZjbUqcjyykgX4at3X5wdplFtVmfOxF7p43H4zoa3xG22xDaeOatcvYu6g 10 | js9kNVMlYfwuPNF5GtQvj/qDyC62rmmVTpWnfyfwibq5dNDR6/F4hXWNlc9er/D3 11 | eULnkc8KsFkX2TP4+Y7PPSC32+VqjY0QZZV5uH9mwzznHz4caO6XwJM+y3T/1oOL 12 | 5zbJlbMUaoBEn00Ey00ll2oYspI3IKOmQmRA7mUXRa7nKq42hu7BTUU+AharoMwS 13 | QH531+mlWdk+5WyA+QIDAQABo4IkaTCCJGUwHQYDVR0lBBYwFAYIKwYBBQUHAwEG 14 | CCsGAQUFBwMCMIIjNQYDVR0RBIIjLDCCIyiCD21pc2MuZ29vZ2xlLmNvbYIUKi4x 15 | aG91cnBlcnNlY29uZC5jb22CGiouYWNjZWxlcmF0ZXdpdGhnb29nbGUuY29tghQq 16 | LmFjdGlvbnMuZ29vZ2xlLmNvbYIOKi5hZGdvb2dsZS5uZXSCDCouYWRtZWxkLmNv 17 | bYIaKi5hZHZlcnRpc2Vyc2NvbW11bml0eS5jb22CFyouYWR3b3Jkcy1jb21tdW5p 18 | dHkuY29tghQqLmFkd29yZHNleHByZXNzLmNvbYIQKi5hZ29vZ2xlZGF5LmNvbYIR 19 | Ki5hbmd1bGFyZGFydC5vcmeCCCouYXBpLmFpghMqLmFwcGVuZ2luZWRlbW8uY29t 20 | ggwqLmFwdHVyZS5jb22CDSouYXJlYTEyMC5jb22CECouYXJ0cHJvamVjdC5jb22C 21 | FSouYmFzZWxpbmUuZ29vZ2xlLmNvbYITKi5iYXNlbGluZXN0dWR5LmNvbYITKi5i 22 | YXNlbGluZXN0dWR5Lm9yZ4ITKi5iZWF0dGhhdHF1b3RlLmNvbYIRKi5iZXJ0YXBw 23 | d2FyZC5jb22CCyouYmxpbmsub3JnggwqLmJyb3RsaS5vcmeCDyouYnVtcHNoYXJl 24 | LmNvbYIMKi5idW1wdG9wLmNhgg0qLmJ1bXB0b3AuY29tgg0qLmJ1bXB0b3AubmV0 25 | gg0qLmJ1bXB0b3Aub3Jngg8qLmJ1bXB0dW5lcy5jb22CFCouY2FtcHVzbG9uZG9u 26 | LmNvLnVrghIqLmNhbXB1c2xvbmRvbi5jb22CEyouY2FtcHVzdGVsYXZpdi5jb22C 27 | HiouY2VydGlmaWNhdGUtdHJhbnNwYXJlbmN5Lm9yZ4IMKi5jaHJvbWUuY29tghAq 28 | LmNocm9tZWJvb2suY29tghEqLmNocm9tZWJvb2tzLmNvbYIQKi5jaHJvbWVjYXN0 29 | LmNvbYISKi5jaHJvbWVzaG9ydHMuY29tgg4qLmNocm9taXVtLm9yZ4IYKi5jbG91 30 | ZGJ1cnN0cmVzZWFyY2guY29tghQqLmNsb3VkZnVuY3Rpb25zLm5ldIITKi5jbG91 31 | ZHJvYm90aWNzLmNvbYIPKi5jb25zY3J5cHQuY29tgg8qLmNvbnNjcnlwdC5vcmeC 32 | GyouY29uc3VtZXJiYXJvbWV0ZXIyMDEzLmNvbYILKi5jb292YS5jb22CCyouY29v 33 | dmEubmV0ggsqLmNvb3ZhLm9yZ4IJKi5jcnIuY29tggsqLmNzNGhzLmNvbYISKi5j 34 | dWx0dXJhbHNwb3QuY29tghAqLmRhcnRzdW1taXQuY29tghUqLmRhdGEtdm9jYWJ1 35 | bGFyeS5vcmeCCyouZGVidWcuY29tghIqLmRlYnVncHJvamVjdC5jb22CDyouZGVz 36 | aWduLmdvb2dsZYIWKi5kZXZlbG9wZXIuZ29vZ2xlLmNvbYIXKi5kZXZlbG9wZXJz 37 | Lmdvb2dsZS5jb22CICouZGV2ZWxvcGVyc2d1aWRldG9hcHBnYWxheHkuY29tgiMq 38 | LmRldmVsb3BlcnNndWlkZXRvdGhlYXBwZ2FsYXh5LmNvbYIZKi5kZXZndWlkZXRv 39 | YXBwZ2FsYXh5LmNvbYIcKi5kZXZndWlkZXRvdGhlYXBwZ2FsYXh5LmNvbYIRKi5k 40 | ZXZzaXRldGVzdC5ob3eCFCouZGlnaXRhbHdvcmtzaG9wLmRlghMqLmRvb2RsZTRn 41 | b29nbGUuY29tghQqLmVudmlyb25tZW50Lmdvb2dsZYIOKi5lcGlzb2RpYy5jb22C 42 | ECouZmVlZGJ1cm5lci5jb22CDCouZmZsaWNrLmNvbYIZKi5maWJlcmZvcmNvbW11 43 | bml0aWVzLmNvbYIYKi5maW5hbmNlbGVhZHNvbmxpbmUuY29tghUqLmZpcmViYXNl 44 | Lmdvb2dsZS5jb22CECouZmx1dHRlcmFwcC5jb22CFCouZnJlZWFuZG9wZW53ZWIu 45 | Y29tggsqLmctdHVuLmNvbYIRKi5nYWxheHluZXh1cy5jb22CEyouZ2FtaW5neW91 46 | dHViZS5jb22CDCouZ2Jiby5jby51a4IXKi5nYmMuYmVhdHRoYXRxdW90ZS5jb22C 47 | FiouZ2Vycml0Y29kZXJldmlldy5jb22CECouZ2V0YnVtcHRvcC5jb22CHCouZ2V0 48 | ZXZlcnlidXNpbmVzc29ubGluZS5jb22CEyouZ2V0Z29vZ2xlYmFjay5jb22CFyou 49 | Z2V0eW91cmdvb2dsZWJhY2suY29tgg4qLmdpcHNjb3JwLmNvbYIVKi5nbGFzcy1j 50 | b21tdW5pdHkuY29tgg8qLmdsb2JhbGVkdS5vcmeCFiouZ29sZGVuaG91cmNhbWVy 51 | YS5jb22CEiouZ29uZ2xjaHVhbmdsLm5ldIIPKi5nb29nbGUuYmVybGlughAqLmdv 52 | b2dsZS5kb21haW5zggwqLmdvb2dsZS5vcmeCESouZ29vZ2xlLnZlbnR1cmVzghAq 53 | Lmdvb2dsZWFkYXkuY29tghUqLmdvb2dsZWFuYWx5dGljcy5jb22CECouZ29vZ2xl 54 | YXBwcy5jb22CFiouZ29vZ2xlYXJ0cHJvamVjdC5jb22CFiouZ29vZ2xlYXJ0cHJv 55 | amVjdC5vcmeCFCouZ29vZ2xlYnJhbmRsYWIuY29tghUqLmdvb2dsZWNvbXBhcmUu 56 | Y28udWuCEyouZ29vZ2xlZGFubWFyay5jb22CEyouZ29vZ2xlZG9tYWlucy5jb22C 57 | FSouZ29vZ2xlZWxlY3Rpb25zLmNvbYIWKi5nb29nbGVlbnRlcnByaXNlLmNvbYIT 58 | Ki5nb29nbGVmaW5sYW5kLmNvbYIXKi5nb29nbGVmb3J2ZXRlcmFucy5jb22CEyou 59 | Z29vZ2xlZm9yd29yay5jb22CESouZ29vZ2xlaWRlYXMuY29tghEqLmdvb2dsZWlk 60 | ZWFzLm9yZ4IYKi5nb29nbGVpbnNpZGVzZWFyY2guY29tghAqLmdvb2dsZW1hcHMu 61 | Y29tghIqLmdvb2dsZXBob3Rvcy5jb22CECouZ29vZ2xlcGxheS5jb22CECouZ29v 62 | Z2xlcGx1cy5jb22CEyouZ29vZ2xlc3ZlcmlnZS5jb22CHCouZ29vZ2xldHJhdmVs 63 | YWRzZXJ2aWNlcy5jb22CGSouZ29vZ2xldmVuZG9yY29udGVudC5jb22CFCouZ29v 64 | Z2xldmVudHVyZXMuY29tghkqLmdyZWdhbmRnbG9yaWFzcGl6emEuY29tggkqLmdz 65 | cmMuaW+CFiouZ3VpZGV0b2FwcGdhbGF4eS5jb22CGSouZ3VpZGV0b3RoZWFwcGdh 66 | bGF4eS5jb22CDiouaGluZGl3ZWIuY29tghIqLmhvd3RvZ2V0bW8uY28udWuCECou 67 | aHRtbDVyb2Nrcy5jb22CCiouaHdnby5jb22CDyouaW1wZXJtaXVtLmNvbYIMKi5q 68 | Mm9iamMub3Jngg4qLmphY3F1YXJkLmNvbYIOKi5qYW1ib2FyZC5jb22CFSoua2V5 69 | dHJhbnNwYXJlbmN5LmNvbYIVKi5rZXl0cmFuc3BhcmVuY3kuZm9vghUqLmtleXRy 70 | YW5zcGFyZW5jeS5vcmeCECoubG9vbmZvcmFsbC5jb22CEioubWFkZXdpdGhjb2Rl 71 | LmNvbYINKi5tZGlhbG9nLmNvbYIWKi5tZXNzYWdlc2ZvcmphcGFuLm9yZ4ITKi5t 72 | ZmctaW5zcGVjdG9yLmNvbYIRKi5taXJhaWtpcm9rdS5jb22CESoubWlyYWlrb3Jv 73 | a3UuY29tghYqLm1vYmlsZXJlc2VhcmNodWIuY29tghEqLm1vYmlsZXZpZXcucGFn 74 | ZYIMKi5teWdiaXouY29tggkqLm5lYXIuYnmCFSoubmV0ei12ZXJ0ZWlkaWdlci5k 75 | ZYIUKi5uZXR6dmVydGVpZGlnZXIuZGWCDCoub2F1dGh6LmNvbYIJKi5vbi5oZXJl 76 | ggkqLm9uMi5jb22CDioub25ldG9kYXkuY29tgg4qLm9uZXRvZGF5Lm9yZ4IZKi5v 77 | bmV3b3JsZG1hbnlzdG9yaWVzLmNvbYIMKi5vbmdiaXouY29tghkqLm9wZW5oYW5k 78 | c2V0YWxsaWFuY2UuY29tghMqLm9ybmVrLWlzbGV0bWUuY29tghgqLnBhZ2VzcGVl 79 | ZG1vYmlsaXplci5jb22CFioucGFydHlsaWtlaXRzMTk4Ni5vcmeCECoucGF4bGlj 80 | ZW5zZS5vcmeCGSoucGVyc29uZmluZGVyLmdvb2dsZS5vcmeCESoucGhvdG9zcGhl 81 | cmUuY29tggwqLnBpY2FzYS5jb22CDioucGl0dHBhdHQuY29tghQqLnBvbHltZXJw 82 | cm9qZWN0Lm9yZ4INKi5wb3N0aW5pLmNvbYIUKi5wcml2YWN5Y2hvaWNlcy5vcmeC 83 | ECoucHJvamVjdGFyYS5jb22CFSoucHJvamVjdGJhc2VsaW5lLmNvbYIRKi5wcm9q 84 | ZWN0bG9vbi5jb22CESoucHJvamVjdHNvbGkuY29tghEqLnF1ZXN0dmlzdWFsLmNv 85 | bYIRKi5xdWlja29mZmljZS5jb22CDSoucXVpa3NlZS5jb22CHioucXVvdGVwcm94 86 | eS5iZWF0dGhhdHF1b3RlLmNvbYIPKi5yZWNhcHRjaGEubmV0ggwqLnJldm9sdi5j 87 | b22CFioucmV3b3Jrd2l0aGdvb2dsZS5jb22CESoucmlkZXBlbmd1aW4uY29tghcq 88 | LnJpZ2h0bGFuZWJpa2VzaG9wLmNvbYIUKi5zLnN2Yy0xLmdvb2dsZS5jb22CDCou 89 | c2F5bm93LmNvbYINKi5zY2hlbWVyLmNvbYIWKi5zY3JlZW53aXNlc2VsZWN0LmNv 90 | bYIWKi5zY3JlZW53aXNldHJlbmRzLmNvbYIbKi5zY3JlZW53aXNldHJlbmRzcGFu 91 | ZWwuY29tgg8qLnNoaWJib2xldGgudHaCDiouc25hcHNlZWQuY29tgg8qLnNvbHZl 92 | Zm9yeC5jb22CCyouc3BpZGVyLmlvghYqLnN0YWdpbmcud2lkZXZpbmUuY29tgiwq 93 | LnN0b3JhZ2UtbmlnaHRseS10ZXN0Lmdvb2dsZXVzZXJjb250ZW50LmNvbYIsKi5z 94 | dG9yYWdlLXN0YWdpbmctdGVzdC5nb29nbGV1c2VyY29udGVudC5jb22CKSouc3Rv 95 | cmFnZS10ZXN0LXRlc3QuZ29vZ2xldXNlcmNvbnRlbnQuY29tghQqLnN1cHBvcnQu 96 | Z29vZ2xlLmNvbYIWKi50ZWFjaHBhcmVudHN0ZWNoLmNvbYIWKi50ZWFjaHBhcmVu 97 | dHN0ZWNoLm9yZ4IQKi50ZW5zb3JmbG93Lm9yZ4IUKi50aGVjbGV2ZXJzZW5zZS5j 98 | b22CGCoudGhlZGlnaXRhbGdhcmFnZS5jby51a4IYKi50aGVnbGFzc2NvbGxlY3Rp 99 | dmUuY29tghYqLnRoaW5rcXVhcnRlcmx5LmNvLnVrghQqLnRoaW5rcXVhcnRlcmx5 100 | LmNvbYIYKi50b2FzdGVkYmVhbnNjb2ZmZWUuY29tgg0qLnR4Y2xvdWQubmV0ggsq 101 | LnR4dmlhLmNvbYISKi51YXQud2lkZXZpbmUuY29tghkqLnVudGVybmVobWVuLWJl 102 | aXNwaWVsLmRlgg8qLnVzZXBsYW5uci5jb22CDyoudjhwcm9qZWN0Lm9yZ4IMKi52 103 | ZXJpbHkuY29tghgqLnZlcmlseWxpZmVzY2llbmNlcy5jb22CDCoud2FsbGV0LmNv 104 | bYILKi53YXltby5jb22CCioud2F6ZS5jb22CFioud2ViYXBwZmllbGRndWlkZS5j 105 | b22CESoud2Vzb2x2ZWZvcnguY29tghEqLndoYXRicm93c2VyLmNvbYIRKi53aGF0 106 | YnJvd3Nlci5vcmeCDioud2lkZXZpbmUuY29tgg8qLndvbWVud2lsbC5jb22CCyou 107 | eC5jb21wYW55gggqLngudGVhbYIRKi54bi0tOXRyczY1Yi5jb22CEyoueW91dHVi 108 | ZWdhbWluZy5jb22CGioueW91dHViZW1vYmlsZXN1cHBvcnQuY29tggsqLnphZ2F0 109 | LmNvbYISMWhvdXJwZXJzZWNvbmQuY29tghhhY2NlbGVyYXRld2l0aGdvb2dsZS5j 110 | b22CDGFkZ29vZ2xlLm5ldIIKYWRtZWxkLmNvbYIXYWR2ZXJ0aXNlcmNvbW11bml0 111 | eS5jb22CGGFkdmVydGlzZXJzY29tbXVuaXR5LmNvbYIVYWR3b3Jkcy1jb21tdW5p 112 | dHkuY29tghJhZHdvcmRzZXhwcmVzcy5jb22CDmFnb29nbGVkYXkuY29tgg9hbmd1 113 | bGFyZGFydC5vcmeCBmFwaS5haYIRYXBwZW5naW5lZGVtby5jb22CCmFwdHVyZS5j 114 | b22CC2FyZWExMjAuY29tgg5hcnRwcm9qZWN0LmNvbYIRYmFzZWxpbmVzdHVkeS5j 115 | b22CEWJhc2VsaW5lc3R1ZHkub3JnghFiZWF0dGhhdHF1b3RlLmNvbYIPYmVydGFw 116 | cHdhcmQuY29tgglibGluay5vcmeCCmJyb3RsaS5vcmeCDWJ1bXBzaGFyZS5jb22C 117 | CmJ1bXB0b3AuY2GCC2J1bXB0b3AuY29tggtidW1wdG9wLm5ldIILYnVtcHRvcC5v 118 | cmeCDWJ1bXB0dW5lcy5jb22CEmNhbXB1c2xvbmRvbi5jby51a4IQY2FtcHVzbG9u 119 | ZG9uLmNvbYIRY2FtcHVzdGVsYXZpdi5jb22CHGNlcnRpZmljYXRlLXRyYW5zcGFy 120 | ZW5jeS5vcmeCCmNocm9tZS5jb22CDmNocm9tZWJvb2suY29tgg9jaHJvbWVib29r 121 | cy5jb22CDmNocm9tZWNhc3QuY29tghBjaHJvbWVzaG9ydHMuY29tggxjaHJvbWl1 122 | bS5vcmeCGWNsaWNrc2VydmUuZGFydHNlYXJjaC5uZXSCHGNsaWNrc2VydmUuZXUu 123 | ZGFydHNlYXJjaC5uZXSCHGNsaWNrc2VydmUudWsuZGFydHNlYXJjaC5uZXSCHWNs 124 | aWNrc2VydmUudXMyLmRhcnRzZWFyY2gubmV0ghljbGlja3NlcnZlci5nb29nbGVh 125 | ZHMuY29tghZjbG91ZGJ1cnN0cmVzZWFyY2guY29tghJjbG91ZGZ1bmN0aW9ucy5u 126 | ZXSCEWNsb3Vkcm9ib3RpY3MuY29tgg1jb25zY3J5cHQuY29tgg1jb25zY3J5cHQu 127 | b3Jnghljb25zdW1lcmJhcm9tZXRlcjIwMTMuY29tghFjb29raWVjaG9pY2VzLm9y 128 | Z4IJY29vdmEuY29tggljb292YS5uZXSCCWNvb3ZhLm9yZ4IHY3JyLmNvbYIJY3M0 129 | aHMuY29tghBjdWx0dXJhbHNwb3QuY29tgg5kYXJ0c3VtbWl0LmNvbYITZGF0YS12 130 | b2NhYnVsYXJ5Lm9yZ4IJZGVidWcuY29tghBkZWJ1Z3Byb2plY3QuY29tgg1kZXNp 131 | Z24uZ29vZ2xlghpkZXZlbG9wZXIuZGV2Lm5lc3RsYWJzLmNvbYIVZGV2ZWxvcGVy 132 | LmZ0Lm5lc3QuY29tgiJkZXZlbG9wZXIuaW50ZWdyYXRpb24ubmVzdGxhYnMuY29t 133 | ghJkZXZlbG9wZXIubmVzdC5jb22CGWRldmVsb3Blci5xYS5uZXN0bGFicy5jb22C 134 | HWRldmVsb3Blci5zdGFibGUubmVzdGxhYnMuY29tghtkZXZlbG9wZXJzLmRldi5u 135 | ZXN0bGFicy5jb22CFmRldmVsb3BlcnMuZnQubmVzdC5jb22CI2RldmVsb3BlcnMu 136 | aW50ZWdyYXRpb24ubmVzdGxhYnMuY29tghNkZXZlbG9wZXJzLm5lc3QuY29tghpk 137 | ZXZlbG9wZXJzLnFhLm5lc3RsYWJzLmNvbYIeZGV2ZWxvcGVycy5zdGFibGUubmVz 138 | dGxhYnMuY29tgh5kZXZlbG9wZXJzZ3VpZGV0b2FwcGdhbGF4eS5jb22CIWRldmVs 139 | b3BlcnNndWlkZXRvdGhlYXBwZ2FsYXh5LmNvbYIXZGV2Z3VpZGV0b2FwcGdhbGF4 140 | eS5jb22CGmRldmd1aWRldG90aGVhcHBnYWxheHkuY29tgg9kZXZzaXRldGVzdC5o 141 | b3eCEmRpZ2l0YWx3b3Jrc2hvcC5kZYIRZG9vZGxlNGdvb2dsZS5jb22CEmVudmly 142 | b25tZW50Lmdvb2dsZYIMZXBpc29kaWMuY29tgg5mZWVkYnVybmVyLmNvbYIKZmZs 143 | aWNrLmNvbYIXZmliZXJmb3Jjb21tdW5pdGllcy5jb22CFmZpbmFuY2VsZWFkc29u 144 | bGluZS5jb22CDmZsdXR0ZXJhcHAuY29tghJmcmVlYW5kb3BlbndlYi5jb22CCWct 145 | dHVuLmNvbYIPZ2FsYXh5bmV4dXMuY29tghFnYW1pbmd5b3V0dWJlLmNvbYIKZ2Ji 146 | by5jby51a4IVZ2JjLmJlYXR0aGF0cXVvdGUuY29tghRnZXJyaXRjb2RlcmV2aWV3 147 | LmNvbYIOZ2V0YnVtcHRvcC5jb22CGmdldGV2ZXJ5YnVzaW5lc3NvbmxpbmUuY29t 148 | ghFnZXRnb29nbGViYWNrLmNvbYIVZ2V0eW91cmdvb2dsZWJhY2suY29tggxnaXBz 149 | Y29ycC5jb22CE2dsYXNzLWNvbW11bml0eS5jb22CDWdsb2JhbGVkdS5vcmeCFGdv 150 | bGRlbmhvdXJjYW1lcmEuY29tghBnb25nbGNodWFuZ2wubmV0gg5nb29nLmRtdHJ5 151 | LmNvbYINZ29vZ2xlLmJlcmxpboIOZ29vZ2xlLmRvbWFpbnOCCmdvb2dsZS5vcmeC 152 | D2dvb2dsZS52ZW50dXJlc4IOZ29vZ2xlYWRheS5jb22CE2dvb2dsZWFuYWx5dGlj 153 | cy5jb22CDmdvb2dsZWFwcHMuY29tghRnb29nbGVhcnRwcm9qZWN0LmNvbYIUZ29v 154 | Z2xlYXJ0cHJvamVjdC5vcmeCEmdvb2dsZWJyYW5kbGFiLmNvbYITZ29vZ2xlY29t 155 | cGFyZS5jby51a4IRZ29vZ2xlZGFubWFyay5jb22CEWdvb2dsZWRvbWFpbnMuY29t 156 | ghNnb29nbGVlbGVjdGlvbnMuY29tghRnb29nbGVlbnRlcnByaXNlLmNvbYIRZ29v 157 | Z2xlZmlubGFuZC5jb22CFWdvb2dsZWZvcnZldGVyYW5zLmNvbYIRZ29vZ2xlZm9y 158 | d29yay5jb22CD2dvb2dsZWlkZWFzLmNvbYIPZ29vZ2xlaWRlYXMub3JnghZnb29n 159 | bGVpbnNpZGVzZWFyY2guY29tgg5nb29nbGVtYXBzLmNvbYIQZ29vZ2xlcGhvdG9z 160 | LmNvbYIOZ29vZ2xlcGxheS5jb22CDmdvb2dsZXBsdXMuY29tghFnb29nbGVzdmVy 161 | aWdlLmNvbYIaZ29vZ2xldHJhdmVsYWRzZXJ2aWNlcy5jb22CF2dvb2dsZXZlbmRv 162 | cmNvbnRlbnQuY29tghJnb29nbGV2ZW50dXJlcy5jb22CF2dyZWdhbmRnbG9yaWFz 163 | cGl6emEuY29tggdnc3JjLmlvghRndWlkZXRvYXBwZ2FsYXh5LmNvbYIXZ3VpZGV0 164 | b3RoZWFwcGdhbGF4eS5jb22CDGhpbmRpd2ViLmNvbYIQaG93dG9nZXRtby5jby51 165 | a4IOaHRtbDVyb2Nrcy5jb22CCGh3Z28uY29tghBpbWFnZXMuemFnYXQuY29tgg1p 166 | bXBlcm1pdW0uY29tggpqMm9iamMub3JnggxqYWNxdWFyZC5jb22CDGphbWJvYXJk 167 | LmNvbYIManMuZG10cnkuY29tghNrZXl0cmFuc3BhcmVuY3kuY29tghNrZXl0cmFu 168 | c3BhcmVuY3kuZm9vghNrZXl0cmFuc3BhcmVuY3kub3Jngg5sb29uZm9yYWxsLmNv 169 | bYIQbWFkZXdpdGhjb2RlLmNvbYILbWRpYWxvZy5jb22CFG1lc3NhZ2VzZm9yamFw 170 | YW4ub3JnghFtZmctaW5zcGVjdG9yLmNvbYIPbWlyYWlraXJva3UuY29tgg9taXJh 171 | aWtvcm9rdS5jb22CFG1vYmlsZXJlc2VhcmNodWIuY29tgg9tb2JpbGV2aWV3LnBh 172 | Z2WCCm15Z2Jpei5jb22CD24zMzkuYXNwLWNjLmNvbYIHbmVhci5ieYITbmV0ei12 173 | ZXJ0ZWlkaWdlci5kZYISbmV0enZlcnRlaWRpZ2VyLmRlggpvYXV0aHouY29tggdv 174 | bi5oZXJlggdvbjIuY29tggxvbmV0b2RheS5jb22CDG9uZXRvZGF5Lm9yZ4IXb25l 175 | d29ybGRtYW55c3Rvcmllcy5jb22CCm9uZ2Jpei5jb22CF29wZW5oYW5kc2V0YWxs 176 | aWFuY2UuY29tghFvcm5lay1pc2xldG1lLmNvbYIWcGFnZXNwZWVkbW9iaWxpemVy 177 | LmNvbYIUcGFydHlsaWtlaXRzMTk4Ni5vcmeCDnBheGxpY2Vuc2Uub3Jngg9waG90 178 | b3NwaGVyZS5jb22CCnBpY2FzYS5jb22CGnBpbmcuZmVlZGJ1cm5lci5nb29nbGUu 179 | Y29tggxwaXR0cGF0dC5jb22CEnBvbHltZXJwcm9qZWN0Lm9yZ4ILcG9zdGluaS5j 180 | b22CEnByaXZhY3ljaG9pY2VzLm9yZ4IOcHJvamVjdGFyYS5jb22CE3Byb2plY3Ri 181 | YXNlbGluZS5jb22CD3Byb2plY3Rsb29uLmNvbYIPcHJvamVjdHNvbGkuY29tgg9x 182 | dWVzdHZpc3VhbC5jb22CC3F1aWtzZWUuY29tghxxdW90ZXByb3h5LmJlYXR0aGF0 183 | cXVvdGUuY29tgg1yZWNhcHRjaGEubmV0ggpyZXZvbHYuY29tghRyZXdvcmt3aXRo 184 | Z29vZ2xlLmNvbYIPcmlkZXBlbmd1aW4uY29tghVyaWdodGxhbmViaWtlc2hvcC5j 185 | b22CFnJvb3RtdXNpYy5iYW5kcGFnZS5jb22CEnMuc3ZjLTEuZ29vZ2xlLmNvbYIK 186 | c2F5bm93LmNvbYILc2NoZW1lci5jb22CFHNjcmVlbndpc2VzZWxlY3QuY29tghRz 187 | Y3JlZW53aXNldHJlbmRzLmNvbYIZc2NyZWVud2lzZXRyZW5kc3BhbmVsLmNvbYIM 188 | c25hcHNlZWQuY29tgg1zb2x2ZWZvcnguY29tgglzcGlkZXIuaW+CFHRlYWNocGFy 189 | ZW50c3RlY2guY29tghR0ZWFjaHBhcmVudHN0ZWNoLm9yZ4IOdGVuc29yZmxvdy5v 190 | cmeCEnRoZWNsZXZlcnNlbnNlLmNvbYIWdGhlZGlnaXRhbGdhcmFnZS5jby51a4IW 191 | dGhlZ2xhc3Njb2xsZWN0aXZlLmNvbYIUdGhpbmtxdWFydGVybHkuY28udWuCEnRo 192 | aW5rcXVhcnRlcmx5LmNvbYIWdG9hc3RlZGJlYW5zY29mZmVlLmNvbYILdHhjbG91 193 | ZC5uZXSCCXR4dmlhLmNvbYIXdW50ZXJuZWhtZW4tYmVpc3BpZWwuZGWCDXVzZXBs 194 | YW5uci5jb22CDXY4cHJvamVjdC5vcmeCCnZlcmlseS5jb22CFnZlcmlseWxpZmVz 195 | Y2llbmNlcy5jb22CCndhbGxldC5jb22CCXdheW1vLmNvbYIId2F6ZS5jb22CFHdl 196 | YmFwcGZpZWxkZ3VpZGUuY29tghJ3ZWx0d2VpdHdhY2hzZW4uZGWCD3dlc29sdmVm 197 | b3J4LmNvbYIPd2hhdGJyb3dzZXIuY29tgg93aGF0YnJvd3Nlci5vcmeCDXdvbWVu 198 | d2lsbC5jb22CG3d3dy5hZHZlcnRpc2VyY29tbXVuaXR5LmNvbYIQd3d3LmJhbmRw 199 | YWdlLmNvbYIVd3d3LmNvb2tpZWNob2ljZXMub3JnghZ3d3cud2VsdHdlaXR3YWNo 200 | c2VuLmRlggl4LmNvbXBhbnmCBngudGVhbYIPeG4tLTl0cnM2NWIuY29tghF5b3V0 201 | dWJlZ2FtaW5nLmNvbYIYeW91dHViZW1vYmlsZXN1cHBvcnQuY29tggl6YWdhdC5j 202 | b20waAYIKwYBBQUHAQEEXDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2ds 203 | ZS5jb20vR0lBRzIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29v 204 | Z2xlLmNvbS9vY3NwMB0GA1UdDgQWBBSJ2VukMD82QpG2M0kGyP9MNY0XejAMBgNV 205 | HRMBAf8EAjAAMB8GA1UdIwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMCEGA1Ud 206 | IAQaMBgwDAYKKwYBBAHWeQIFATAIBgZngQwBAgIwMAYDVR0fBCkwJzAloCOgIYYf 207 | aHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcyLmNybDANBgkqhkiG9w0BAQsFAAOC 208 | AQEAMuMQYyUExA5/pAtNUJH+1RNqYWypGPANfaXqSfEgcoMPXkw//ue69ll7Mhbk 209 | sfoL7mjanYYDqWCE2da8Q+ZhhQScMGQf3e/AccGVd+3FFlqMpwdMP9NABqmKdU4u 210 | Kxg6oyqcWy3Jbamh8rp7G247Y3iVr+RCVe3V+m9Z6+KAXcU2Lcn73KIg6/wjC4RY 211 | m8NGN1uunKfBsf8uk8keFbW89ENEJzcYXZFVmmTGLyZrnA9ith6bKpaUxuyd7uu6 212 | UvpXMehW10pFsS3FWTVAkJ+kZtvc8EKo0CkDTWDNArQOcYuxsUY/vgliVdLVwvzM 213 | ii1fYT4Gew1fwWA6kr27S0g3kQ== 214 | -----END CERTIFICATE----- 215 | -------------------------------------------------------------------------------- /tests/data/test_sct_ee_cert/cert_no_ev.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_ee_cert/cert_no_ev.der -------------------------------------------------------------------------------- /tests/data/test_sct_ee_cert/ev_cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_ee_cert/ev_cert.der -------------------------------------------------------------------------------- /tests/data/test_sct_ee_cert/issued_by_letsencrypt.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_ee_cert/issued_by_letsencrypt.der -------------------------------------------------------------------------------- /tests/data/test_sct_ee_cert/issued_by_letsencrypt_2.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_ee_cert/issued_by_letsencrypt_2.der -------------------------------------------------------------------------------- /tests/data/test_sct_ee_cert/issued_by_letsencrypt_not.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_ee_cert/issued_by_letsencrypt_not.der -------------------------------------------------------------------------------- /tests/data/test_sct_verify_signature/google.com/pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEluqsHEYMG1XcDfy1lCdGV0JwOmkY 3 | 4r87xNuroPS2bMBTP01CEDPwWJePa75y9CrsHEKqAy8afig1dpkIPSEUhg== 4 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /tests/data/test_sct_verify_signature/google.com/signature.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_verify_signature/google.com/signature.der -------------------------------------------------------------------------------- /tests/data/test_sct_verify_signature/google.com/signature_input.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_verify_signature/google.com/signature_input.bin -------------------------------------------------------------------------------- /tests/data/test_sct_verify_signature/pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmyGDvYXsRJsNyXSrYc9DjHsIa2x 3 | zb4UR7ZxVoV6mrc9iZB7xjI6+NrOiwH+P/xxkRmOFG6Jel20q37hTh58rA== 4 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /tests/data/test_sct_verify_signature/pubkey_possl.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxszlc+b71LvlLS0ypt/l 3 | gT/JzSVJtnEqw9WUNGeiChywX2mmQLHEt7KP0JikqUFZOtPclNY823Q4pErMTSWC 4 | 90qlUxI47vNJbXGRfmO2q6Zfw6SE+E9iUb74xezbOJLjBuUIkQzEKEFV+8taiRV+ 5 | ceg1v01yCT2+OjhQW3cxG42zxyRFmqesbQAUWgS3uhPrUQqYQUEiTmVhh4FBUKZ5 6 | XIneGUpX1S7mXRxTLH6YzRoGFqRoc9A0BBNcoXHTWnxV215k4TeHMFYE5RG0KYAS 7 | 8Xk5iKICEXwnZreIt3jyygqoOKsKZMK/Zl2VhMGhJR6HXRpQCyASzEG7bgtROLhL 8 | ywIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /tests/data/test_sct_verify_signature/signature.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_verify_signature/signature.der -------------------------------------------------------------------------------- /tests/data/test_sct_verify_signature/signature_input_valid.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theno/ctutlz/a81b8282096b505f85e81b8fb9c893bd3a9becab/tests/data/test_sct_verify_signature/signature_input_valid.bin -------------------------------------------------------------------------------- /tests/test_ctlog.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import pytest 4 | 5 | from ctutlz import ctlog 6 | 7 | 8 | @pytest.mark.skip(reason="no way of currently testing this") 9 | def test_log_dict_from_log_text(): 10 | test_data = [ 11 | { 12 | 'input': '''\ 13 | ct.googleapis.com/pilot 14 | 15 | Base64 Log ID: pLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BA= 16 | Operator: Google 17 | Contact: google-ct-logs@googlegroups.com 18 | Chrome bug: https://crbug.com/389511 19 | ''', 20 | 'expected': { 21 | 'url': 'ct.googleapis.com/pilot/', 22 | 'id_b64_non_calculated': 'pLkJkLQYWBSHuxOizGdwCj' 23 | 'w1mAT5G9+443fNDsgN3BA=', 24 | 'operated_by': ['Google'], 25 | 'contact': 'google-ct-logs@googlegroups.com', 26 | 'chrome_bug': 'https://crbug.com/389511', 27 | 'maximum_merge_delay': None, 28 | 'description': None, 29 | 'key': None, 30 | }, 31 | }, 32 | { 33 | 'input': '''\ 34 | ct.googleapis.com/submariner 35 | 36 | Base64 Log ID: qJnYeAySkKr0YvMYgMz71SRR6XDQ+/WR73Ww2ZtkVoE= 37 | Operator: Google 38 | Contact: google-ct-logs@googlegroups.com 39 | 40 | Note that this log is not trusted by Chrome. It only logs certificates that 41 | chain to roots that are on track for inclusion in browser roots or were 42 | trusted at some previous point. See the announcement blog post. 43 | 44 | ''', 45 | 'expected': { 46 | 'url': 'ct.googleapis.com/submariner/', 47 | 'id_b64_non_calculated': 'qJnYeAySkKr0YvMYgMz71S' 48 | 'RR6XDQ+/WR73Ww2ZtkVoE=', 49 | 'operated_by': ['Google'], 50 | 'contact': 'google-ct-logs@googlegroups.com', 51 | 'notes': 'Note that this log is not ' 52 | 'trusted by Chrome. It only logs certificates that ' 53 | 'chain to roots that are on track for inclusion in ' 54 | 'browser roots or were trusted at some previous ' 55 | 'point. See the announcement blog post.', 56 | 'maximum_merge_delay': None, 57 | 'description': None, 58 | 'key': None, 59 | }, 60 | }, 61 | { 62 | 'input': '''\ 63 | ct.googleapis.com/testtube 64 | 65 | Base64 Log ID: sMyD5aX5fWuvfAnMKEkEhyrH6IsTLGNQt8b9JuFsbHc= 66 | Operator: Google 67 | Contact: google-ct-logs@googlegroups.com 68 | 69 | Note that this log is intended for testing purposes only and will only log 70 | certificates that chain to a root explicitly added to it. 71 | To add a test root to Testtube, please email google-ct-logs@googlegroups.com 72 | 73 | A test root for Testtube should: 74 | 75 | * have a certificate "Subject" field that: 76 | * includes the word "Test" (to reduce the chances of real certificates being mixed up with test certificates. 77 | * identifies the organization that the test root is for (to allow easy classification of test traffic). 78 | * not allow real certificates to chain to it, either because: 79 | * it is a self-signed root CA certificate identified as a test certificate (as above). 80 | * it is an intermediate CA certificate that chains to a root certificate that is also identified as a test certificate. 81 | * be a CA certificate, by: 82 | * having CA:TRUE in the Basic Constraints extension. 83 | * include the 'Certificate Sign' bit in the Key Usage extension. 84 | 85 | Note that for historical reasons Testtube includes some test roots that do not 86 | comply with all of the above requirements. 87 | 88 | ''', 89 | 'expected': { 90 | 'url': 'ct.googleapis.com/testtube/', 91 | 'id_b64_non_calculated': 'sMyD5aX5fWuvfAnMKEkEhy' 92 | 'rH6IsTLGNQt8b9JuFsbHc=', 93 | 'operated_by': ['Google'], 94 | 'contact': 'google-ct-logs@googlegroups.com', 95 | 'notes': 'Note that this log is intended for testing purposes ' 96 | 'only and will only log certificates that chain to a ' 97 | 'root explicitly added to it. To add a test root to ' 98 | 'Testtube, please email ' 99 | 'google-ct-logs@googlegroups.com ' 100 | 'A test root for Testtube should: ' 101 | '* have a certificate "Subject" field that: ' 102 | '* includes the word "Test" (to reduce the chances of ' 103 | 'real certificates being mixed up with test ' 104 | 'certificates. ' 105 | '* identifies the organization that the test root is ' 106 | 'for (to allow easy classification of test traffic). ' 107 | '* not allow real certificates to chain to it, either ' 108 | 'because: ' 109 | '* it is a self-signed root CA certificate identified ' 110 | 'as a test certificate (as above). ' 111 | '* it is an intermediate CA certificate that chains ' 112 | 'to a root certificate that is also identified as a ' 113 | 'test certificate. ' 114 | '* be a CA certificate, by: ' 115 | '* having CA:TRUE in the Basic Constraints extension. ' 116 | "* include the 'Certificate Sign' bit in the Key " 117 | 'Usage extension. ' 118 | 'Note that for historical reasons Testtube includes ' 119 | 'some test roots that do not comply with all of the ' 120 | 'above requirements.''', 121 | 'maximum_merge_delay': None, 122 | 'description': None, 123 | 'key': None, 124 | }, 125 | }, 126 | ] 127 | for cur_test in test_data: 128 | got = ctlog._log_dict_from_log_text(log_text=cur_test['input']) 129 | assert got == cur_test['expected'] 130 | 131 | 132 | @pytest.mark.skip(reason="no way of currently testing this") 133 | def test_logs_dict_from_html_str(): 134 | thisdir = os.path.abspath(os.path.dirname(__file__)) 135 | test_data = [ 136 | { 137 | 'filename': 'known-logs_2018-02-27.html', 138 | 'expected_logs_dict': { 139 | 'special_purpose_logs': [ 140 | { 141 | 'chrome_state': None, 142 | 'contact': 'google-ct-logs@googlegroups.com', 143 | 'description': None, 144 | 'id_b64_non_calculated': 145 | 'HQJLjrFJizRN/YfqPvwJlvdQbyNdHUlwYaR3PEOcJfs=', 146 | 'key': None, 147 | 'maximum_merge_delay': None, 148 | 'notes': 'This log is not trusted by Chrome. It ' 149 | 'only logs certificates that have expired. ' 150 | 'See the announcement post.', 151 | 'operated_by': ['Google'], 152 | 'url': 'ct.googleapis.com/daedalus/' 153 | }, 154 | { 155 | 'chrome_state': None, 156 | 'contact': 'google-ct-logs@googlegroups.com', 157 | 'description': None, 158 | 'id_b64_non_calculated': 159 | 'qJnYeAySkKr0YvMYgMz71SRR6XDQ+/WR73Ww2ZtkVoE=', 160 | 'key': None, 161 | 'maximum_merge_delay': None, 162 | 'notes': 'This log is not trusted by Chrome. It ' 163 | 'only logs certificates that chain to ' 164 | 'roots that are on track for inclusion in ' 165 | 'browser roots or were trusted at some ' 166 | 'previous point. See the announcement blog ' 167 | 'post.', 168 | 'operated_by': ['Google'], 169 | 'url': 'ct.googleapis.com/submariner/' 170 | }, 171 | { 172 | 'chrome_state': None, 173 | 'contact': 'google-ct-logs@googlegroups.com', 174 | 'description': None, 175 | 'id_b64_non_calculated': 176 | 'sMyD5aX5fWuvfAnMKEkEhyrH6IsTLGNQt8b9JuFsbHc=', 177 | 'key': None, 178 | 'maximum_merge_delay': None, 179 | 'notes': 'This log is intended for testing purposes ' 180 | 'only and will only log certificates that ' 181 | 'chain to a root explicitly added to it. ' 182 | 'To add a test root to Testtube, please ' 183 | 'email google-ct-logs@googlegroups.com A ' 184 | 'test root for Testtube should: * have a ' 185 | 'certificate "Subject" field that: * ' 186 | 'includes the word "Test" (to reduce the ' 187 | 'chances of real certificates being mixed ' 188 | 'up with test certificates. * identifies ' 189 | 'the organization that the test root is ' 190 | 'for (to allow easy classification of test ' 191 | 'traffic). * not allow real certificates ' 192 | 'to chain to it, either because: * it is a ' 193 | 'self-signed root CA certificate ' 194 | 'identified as a test certificate (as ' 195 | 'above). * it is an intermediate CA ' 196 | 'certificate that chains to a root ' 197 | 'certificate that is also identified as a ' 198 | 'test certificate. * be a CA certificate, ' 199 | 'by: * having CA:TRUE in the Basic ' 200 | 'Constraints extension. * include the ' 201 | "'Certificate Sign' bit in the Key Usage " 202 | 'extension. For historical reasons ' 203 | 'Testtube includes some test roots that do ' 204 | 'not comply', 205 | 'operated_by': ['Google'], 206 | 'url': 'ct.googleapis.com/testtube/' 207 | } 208 | ] 209 | } 210 | }, 211 | ] 212 | for item in test_data: 213 | with open(os.path.join(thisdir, 'data', 'test_ctlog', 214 | item['filename'])) as fh: 215 | input = fh.read() 216 | 217 | got = ctlog._logs_dict_from_html(html=input) 218 | 219 | # from pprint import pprint; pprint(got, indent=1, width=80) 220 | assert got == item['expected_logs_dict'] 221 | -------------------------------------------------------------------------------- /tests/test_decompose_cert.py: -------------------------------------------------------------------------------- 1 | from os.path import abspath, dirname, join 2 | 3 | import pyasn1_modules 4 | from pyasn1.codec.der.decoder import decode as der_decoder 5 | from pyasn1.codec.der.encoder import encode as der_encoder 6 | 7 | from ctutlz.scripts.decompose_cert import cert_der_from_data 8 | 9 | 10 | def test_cert_der_from_data(): 11 | thisdir = abspath(dirname(__file__)) 12 | testdata = join(thisdir, 'data', 'test_decompose_cert') 13 | 14 | with open(join(testdata, 'cert.der'), 'rb') as fh: 15 | cert_der = fh.read() 16 | with open(join(testdata, 'cert.b64'), 'rb') as fh: 17 | cert_b64 = fh.read() 18 | with open(join(testdata, 'cert.pem'), 'rb') as fh: 19 | cert_pem = fh.read() 20 | 21 | assert cert_der_from_data(cert_der) == cert_der 22 | assert cert_der_from_data(cert_b64) == cert_der 23 | assert cert_der_from_data(cert_pem) == cert_der 24 | 25 | 26 | def test_parse_cert_with_pyasn1(): 27 | thisdir = abspath(dirname(__file__)) 28 | testdata = join(thisdir, 'data', 'test_decompose_cert') 29 | 30 | with open(join(testdata, 'cert.der'), 'rb') as fh: 31 | cert_der = fh.read() 32 | 33 | cert, _ = der_decoder(cert_der, 34 | asn1Spec=pyasn1_modules.rfc5280.Certificate()) 35 | assert type(cert) is pyasn1_modules.rfc5280.Certificate 36 | 37 | re_encoded = der_encoder(cert) 38 | assert type(re_encoded) is bytes 39 | assert cert_der == re_encoded 40 | -------------------------------------------------------------------------------- /tests/test_rfc6962.py: -------------------------------------------------------------------------------- 1 | from ctutlz import rfc6962 2 | 3 | 4 | def test_parse_log_entry_type_0(): 5 | tdf = b'\x00\x00' 6 | 7 | parse, offset = rfc6962._parse_log_entry_type(tdf) 8 | 9 | assert offset == 2 10 | assert parse == { 11 | 'tdf': b'\x00\x00', 12 | 'val': 0, 13 | } 14 | 15 | 16 | def test_parse_log_entry_type_1(): 17 | tdf = b'\x00\x01' 18 | 19 | parse, offset = rfc6962._parse_log_entry_type(tdf) 20 | 21 | assert offset == 2 22 | assert parse == { 23 | 'tdf': b'\x00\x01', 24 | 'val': 1, 25 | } 26 | 27 | 28 | def test_log_entry_type_0_from_tdf(): 29 | tdf = b'\x00\x00anything' 30 | 31 | log_entry_type = rfc6962.LogEntryType(arg=tdf) 32 | 33 | assert log_entry_type.is_x509_entry is True 34 | assert log_entry_type.is_precert_entry is False 35 | assert log_entry_type.tdf == b'\x00\x00' 36 | assert str(log_entry_type) == 'x509_entry' 37 | assert log_entry_type._parse == { 38 | 'tdf': b'\x00\x00', 39 | 'val': 0, 40 | } 41 | 42 | 43 | def test_log_entry_type_0_from_parse(): 44 | parse = { 45 | 'tdf': b'\x00\x00', 46 | 'val': 0, 47 | } 48 | 49 | log_entry_type = rfc6962.LogEntryType(arg=parse) 50 | 51 | assert log_entry_type.is_x509_entry is True 52 | assert log_entry_type.is_precert_entry is False 53 | assert log_entry_type.tdf == b'\x00\x00' 54 | assert str(log_entry_type) == 'x509_entry' 55 | assert log_entry_type._parse == { 56 | 'tdf': b'\x00\x00', 57 | 'val': 0, 58 | } 59 | 60 | 61 | def test_log_entry_type_1_from_tdf(): 62 | tdf = b'\x00\x01' 63 | 64 | log_entry_type = rfc6962.LogEntryType(arg=tdf) 65 | 66 | assert log_entry_type.is_x509_entry is False 67 | assert log_entry_type.is_precert_entry is True 68 | assert log_entry_type.tdf == b'\x00\x01' 69 | assert str(log_entry_type) == 'precert_entry' 70 | assert log_entry_type._parse == { 71 | 'tdf': b'\x00\x01', 72 | 'val': 1, 73 | } 74 | 75 | 76 | def test_log_entry_type_1_from_parse(): 77 | parse = { 78 | 'tdf': b'\x00\x01', 79 | 'val': 1, 80 | } 81 | 82 | log_entry_type = rfc6962.LogEntryType(arg=parse) 83 | 84 | assert log_entry_type.is_x509_entry is False 85 | assert log_entry_type.is_precert_entry is True 86 | assert log_entry_type.tdf == b'\x00\x01' 87 | assert str(log_entry_type) == 'precert_entry' 88 | assert log_entry_type._parse == { 89 | 'tdf': b'\x00\x01', 90 | 'val': 1, 91 | } 92 | 93 | 94 | def test_signature_type_0_from_tdf(): 95 | tdf = b'\x00\x01\x02\x03\x04\x05\x06\x07\x89' 96 | 97 | signature_type = rfc6962.SignatureType(arg=tdf) 98 | 99 | assert signature_type.is_certificate_timestamp is True 100 | assert signature_type.is_tree_hash is False 101 | assert signature_type._parse == { 102 | 'tdf': b'\x00', 103 | 'val': 0, 104 | } 105 | 106 | 107 | def test_signature_type_0_from_parse(): 108 | parse = { 109 | 'tdf': b'\x00', 110 | 'val': 0, 111 | } 112 | 113 | signature_type = rfc6962.SignatureType(arg=parse) 114 | 115 | assert signature_type.is_certificate_timestamp is True 116 | assert signature_type.is_tree_hash is False 117 | assert signature_type._parse == { 118 | 'tdf': b'\x00', 119 | 'val': 0, 120 | } 121 | 122 | 123 | def test_signature_type_1_from_tdf(): 124 | tdf = b'\x01' 125 | 126 | signature_type = rfc6962.SignatureType(arg=tdf) 127 | 128 | assert signature_type.is_certificate_timestamp is False 129 | assert signature_type.is_tree_hash is True 130 | assert signature_type._parse == { 131 | 'tdf': b'\x01', 132 | 'val': 1, 133 | } 134 | 135 | 136 | def test_signature_type_1_from_parse(): 137 | parse = { 138 | 'tdf': b'\x01', 139 | 'val': 1, 140 | } 141 | 142 | signature_type = rfc6962.SignatureType(arg=parse) 143 | 144 | assert signature_type.is_certificate_timestamp is False 145 | assert signature_type.is_tree_hash is True 146 | assert signature_type._parse == { 147 | 'tdf': b'\x01', 148 | 'val': 1, 149 | } 150 | 151 | 152 | def test_version_from_tdf(): 153 | tdf = b'\x00anything' 154 | 155 | version = rfc6962.Version(tdf) 156 | 157 | assert version.is_v1 is True 158 | assert version._parse == { 159 | 'tdf': b'\x00', 160 | 'val': 0, 161 | } 162 | 163 | # invalid version number 164 | 165 | invalid_tdf = b'\x10' 166 | 167 | version = rfc6962.Version(invalid_tdf) 168 | assert version.is_v1 is False 169 | assert version._parse == { 170 | 'tdf': b'\x10', 171 | 'val': 16, 172 | } 173 | 174 | 175 | def test_version_from_parse(): 176 | parse = { 177 | 'val': 0, 178 | 'tdf': b'\x00', 179 | } 180 | 181 | version = rfc6962.Version(arg=parse) 182 | 183 | assert version.is_v1 is True 184 | assert version._parse == { 185 | 'tdf': b'\x00', 186 | 'val': 0, 187 | } 188 | 189 | 190 | def test_SignedCertificateTimestamp_from_tdf(): 191 | tdf = (b'\x00\xeeK\xbd\xb7u\xce`\xba\xe1Bi\x1f\xab\xe1\x9ef\xa3\x0f~_\xb0r' 192 | b'\xd8\x83\x00\xc4{\x89z\xa8\xfd\xcb\x00\x00\x01]\xe7\x11\xf5\xf7' 193 | b'\x00\x00\x04\x03\x00F0D\x02 ph\xa0\x08\x96H\xbc\x1b\x11\x0e\xd0' 194 | b'\x98\x02\xa8\xac\xb8\x19-|,\xe5\x0e\x9e\xf8/_&\xf7b\x88\xb4U\x02 X' 195 | b'\xbc\r>jFN\x0e\xda\x0b\x1b\xb5\xc0\x1a\xfd\x90\x91\xb0&\x1b\xdf' 196 | b'\xdc\x02Z\xd4zd\xd7\x80c\x0f\xd5') 197 | 198 | sct = rfc6962.SignedCertificateTimestamp(arg=tdf) 199 | 200 | assert sct.log_id.tdf == (b'\xeeK\xbd\xb7u\xce`\xba\xe1Bi\x1f\xab\xe1\x9ef' 201 | b'\xa3\x0f~_\xb0r\xd8\x83\x00\xc4{\x89z\xa8\xfd' 202 | b'\xcb') 203 | assert sct.tdf == tdf 204 | -------------------------------------------------------------------------------- /tests/test_sct_ee_cert.py: -------------------------------------------------------------------------------- 1 | from os.path import join, dirname 2 | 3 | import OpenSSL 4 | from pyasn1.codec.der.decoder import decode as der_decoder 5 | from pyasn1.type.univ import ObjectIdentifier, Sequence 6 | from utlz import flo 7 | 8 | from ctutlz.sct.ee_cert import pyopenssl_certificate_from_der, EndEntityCert 9 | 10 | 11 | def test_pyopenssl_certificate_from_der(): 12 | basedir = join(dirname(__file__), 'data', 'test_sct_ee_cert') 13 | 14 | for filename in ['ev_cert.der', 'cert_no_ev.der']: 15 | cert_der = open(flo('{basedir}/{filename}'), 'rb').read() 16 | got = pyopenssl_certificate_from_der(cert_der) 17 | 18 | assert type(got) is OpenSSL.crypto.X509 19 | 20 | 21 | def test_is_ev_cert(): 22 | basedir = join(dirname(__file__), 'data', 'test_sct_ee_cert') 23 | 24 | test_data = [ 25 | ('ev_cert.der', True), 26 | ('cert_no_ev.der', False), 27 | ] 28 | 29 | for filename, expected in test_data: 30 | cert_der = open(flo('{basedir}/{filename}'), 'rb').read() 31 | ee_cert = EndEntityCert(cert_der) 32 | 33 | assert ee_cert.is_ev_cert is expected 34 | 35 | 36 | def test_is_letsencrypt_cert(): 37 | basedir = join(dirname(__file__), 'data', 'test_sct_ee_cert') 38 | 39 | test_data = [ 40 | ('issued_by_letsencrypt.der', True), 41 | ('issued_by_letsencrypt_2.der', True), 42 | ('issued_by_letsencrypt_not.der', False), 43 | ] 44 | 45 | for filename, expected in test_data: 46 | cert_der = open(flo('{basedir}/{filename}'), 'rb').read() 47 | ee_cert = EndEntityCert(cert_der) 48 | 49 | assert ee_cert.is_letsencrypt_cert is expected 50 | -------------------------------------------------------------------------------- /tests/test_sct_verify_signature.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from os.path import join, dirname 3 | 4 | from utlz import flo 5 | 6 | from ctutlz.sct.verification import verify_signature 7 | 8 | 9 | Item = namedtuple( 10 | typename='TestItem', 11 | field_names=[ 12 | 'domain', 13 | 'pubkey_pem', 14 | 'signature_der', 15 | 'signature_input_bin', 16 | 'expected_verify', 17 | ]) 18 | 19 | 20 | def from_file(filename): 21 | basedir = join(dirname(__file__), 'data', 'test_sct_verify_signature') 22 | with open(flo('{basedir}/{filename}'), 'rb') as fh: 23 | data = fh.read() 24 | return data 25 | 26 | 27 | def test_verify_signature(): 28 | test_data = [ 29 | Item( 30 | domain='', 31 | expected_verify=True, 32 | signature_input_bin=from_file('signature_input_valid.bin'), 33 | signature_der=from_file('signature.der'), 34 | pubkey_pem=from_file('pubkey.pem') 35 | ), 36 | Item( 37 | domain='', 38 | expected_verify=False, 39 | signature_input_bin=b'some invalid signature input', 40 | signature_der=from_file('signature.der'), 41 | pubkey_pem=from_file('pubkey.pem') 42 | ), 43 | Item( 44 | domain='google.com', 45 | expected_verify=True, 46 | signature_input_bin=from_file('google.com/signature_input.bin'), 47 | signature_der=from_file('google.com/signature.der'), 48 | pubkey_pem=from_file('google.com/pubkey.pem') 49 | ), 50 | # Item( 51 | # domain='pirelli.com', 52 | # expected_verify=True, 53 | # signature_input_bin=from_file('pirelli.com/signature_input.bin'), 54 | # signature_der=from_file('pirelli.com/signature.der'), 55 | # pubkey_pem=from_file('pirelli.com/pubkey.pem') 56 | # ), 57 | ] 58 | for item in test_data: 59 | assert verify_signature(item.signature_input_bin, 60 | item.signature_der, 61 | item.pubkey_pem) \ 62 | is item.expected_verify, flo('verify_signature() for {item.domain} ' 63 | 'must return {item.expected_verify}') 64 | -------------------------------------------------------------------------------- /tests/test_utils_encoding.py: -------------------------------------------------------------------------------- 1 | from ctutlz.utils.encoding import decode_from_b64 2 | 3 | 4 | def test_decode_from_b64(): 5 | data = [ 6 | { 7 | 'input': 'ZGF0YSB0byBiZSBlbmNvZGVk', 8 | 'must': b'data to be encoded', 9 | } 10 | ] 11 | for test in data: 12 | got = decode_from_b64(test['input']) 13 | assert got == test['must'] 14 | -------------------------------------------------------------------------------- /tests/test_utils_tdf_bytes.py: -------------------------------------------------------------------------------- 1 | from ctutlz.utils.tdf_bytes import namedtuple, TdfBytesParser 2 | 3 | 4 | def test_tdf_bytes(): 5 | 6 | def _parse_lv2(tdf): 7 | with TdfBytesParser(tdf) as parser: 8 | parser.read('lv2', '!2s') 9 | return parser.result() 10 | 11 | def _parse_nam_tup(tdf): 12 | with TdfBytesParser(tdf) as parser: 13 | parser.read('lv1', '!s') 14 | 15 | parse_lv2 = parser.delegate('_tmp', _parse_lv2) 16 | parser.res.update(parse_lv2) 17 | del parser.res['_tmp'] 18 | 19 | return parser.result() 20 | 21 | try: 22 | str(b'this line raises a TypeError on Python-2.x', 'utf-8') 23 | # Python-3.x 24 | NamTup = namedtuple( 25 | typename='NamTup', 26 | lazy_vals={ 27 | '_parse_func': lambda _: _parse_nam_tup, 28 | 29 | 'lv1': lambda self: str(self._parse['lv1'], 'utf-8'), 30 | 'lv2': lambda self: str(self._parse['lv2'], 'utf-8'), 31 | } 32 | ) 33 | except TypeError: 34 | # Python-2.x 35 | NamTup = namedtuple( 36 | typename='NamTup', 37 | lazy_vals={ 38 | '_parse_func': lambda _: _parse_nam_tup, 39 | 40 | 'lv1': lambda self: str(self._parse['lv1']), 41 | 'lv2': lambda self: str(self._parse['lv2']), 42 | } 43 | ) 44 | 45 | ntup = NamTup(arg=b'ARGUMENT') 46 | 47 | assert ntup.tdf == b'ARG' 48 | assert ntup._parse_func == _parse_nam_tup 49 | assert type(ntup._parse) == dict 50 | assert ntup.lv1 == 'A' 51 | assert ntup.lv2 == 'RG' 52 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35,py36,py37,py38 3 | [testenv] 4 | # pytest needs six to be installed for running ffibuilder 5 | deps = pytest<3.3 6 | six 7 | commands = py.test 8 | --------------------------------------------------------------------------------