commit a9f45b4d5bca38ae99ed38ba51585954951a7aee Author: Ansible Core Team Date: Mon Mar 9 13:11:34 2020 +0000 Initial commit diff --git a/.github/workflows/collection-continuous-integration.yml b/.github/workflows/collection-continuous-integration.yml new file mode 100644 index 00000000..39013462 --- /dev/null +++ b/.github/workflows/collection-continuous-integration.yml @@ -0,0 +1,308 @@ +name: Collection test suite + +on: + push: + pull_request: + schedule: + - cron: 3 0 * * * # Run daily at 0:03 UTC + +jobs: + build-collection-artifact: + name: Build collection + runs-on: ${{ matrix.runner-os }} + strategy: + matrix: + runner-os: + - ubuntu-latest + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + runner-python-version: + - 3.8 + steps: + - name: Check out ${{ github.repository }} on disk + uses: actions/checkout@master + - name: Set up Python ${{ matrix.runner-python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.runner-python-version }} + - name: Set up pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('tests/sanity/requirements.txt') }}-${{ hashFiles('tests/unit/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Build a collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + build + --output-path + "${GITHUB_WORKSPACE}/.cache/collection-tarballs" + - name: Store migrated collection artifacts + uses: actions/upload-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + + sanity-test-collection-via-vms: + name: Sanity in VM ${{ matrix.os.vm || 'ubuntu-latest' }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.os.vm || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + os: + - vm: ubuntu-latest + - vm: ubuntu-16.04 + - vm: macos-latest + python-version: + - 3.8 + - 3.7 + - 3.6 + - 3.5 + - 2.7 + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Set up pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.ref }}-sanity-VMs + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection sanity tests + run: >- + ~/.local/bin/ansible-test + sanity + --color + --requirements + --venv + --python + "${{ matrix.python-version }}" + -vvv + working-directory: >- + /${{ runner.os == 'Linux' && 'home' || 'Users' }}/runner/.ansible/collections/ansible_collections/community/crypto + + sanity-test-collection-via-containers: + name: Sanity in container via Python ${{ matrix.python-version }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.runner-os }} + strategy: + fail-fast: false + matrix: + runner-os: + - ubuntu-latest + runner-python-version: + - 3.8 + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + python-version: + - 3.8 + - 2.7 + - 3.7 + - 3.6 + - 3.5 + - 2.6 + steps: + - name: Set up Python ${{ matrix.runner-python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.runner-python-version }} + - name: Set up pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.ref }}-sanity-containers + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection sanity tests + run: >- + ~/.local/bin/ansible-test + sanity + --color + --requirements + --docker + --python + "${{ matrix.python-version }}" + -vvv + working-directory: >- + /home/runner/.ansible/collections/ansible_collections/community/crypto + + unit-test-collection-via-vms: + name: Units in VM ${{ matrix.os.vm || 'ubuntu-latest' }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.os.vm || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + os: + - vm: ubuntu-latest + - vm: ubuntu-16.04 + - vm: macos-latest + python-version: + - 3.8 + - 3.7 + - 3.6 + - 3.5 + - 2.7 + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Set up pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.ref }}-units-VMs + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection unit tests + run: | + [[ ! -d 'tests/unit' ]] && echo This collection does not have unit tests. Skipping... || \ + ~/.local/bin/ansible-test units --color --coverage --requirements --venv --python "${{ matrix.python-version }}" -vvv + working-directory: >- + /${{ runner.os == 'Linux' && 'home' || 'Users' }}/runner/.ansible/collections/ansible_collections/community/crypto + + unit-test-collection-via-containers: + name: Units in container ${{ matrix.container-image }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.runner-os }} + strategy: + fail-fast: false + matrix: + runner-os: + - ubuntu-latest + runner-python-version: + - 3.8 + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + container-image: + - fedora31 + - ubuntu1804 + - centos8 + - opensuse15 + - fedora30 + - centos7 + - opensuse15py2 + - ubuntu1604 + - centos6 + steps: + - name: Set up Python ${{ matrix.runner-python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.runner-python-version }} + - name: Set up pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.ref }}-units-containers + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection unit tests + run: | + [[ ! -d 'tests/unit' ]] && echo This collection does not have unit tests. Skipping... || \ + ~/.local/bin/ansible-test units --color --coverage --requirements --docker "${{ matrix.container-image }}" -vvv + working-directory: >- + /home/runner/.ansible/collections/ansible_collections/community/crypto \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c6fc14ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,387 @@ + +# Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv +# Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv + +### dotenv ### +.env + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +#!! ERROR: jupyternotebook is undefined. Use list command to see defined gitignore types !!# + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### pydev ### +.pydevproject + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### WebStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..10926e87 --- /dev/null +++ b/COPYING @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/README.md b/README.md new file mode 100644 index 00000000..10aefcd0 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +[![GitHub Actions CI/CD build status — Collection test suite](https://github.com/ansible-collection-migration/community.crypto/workflows/Collection%20test%20suite/badge.svg?branch=master)](https://github.com/ansible-collection-migration/community.crypto/actions?query=workflow%3A%22Collection%20test%20suite%22) + +Ansible Collection: community.crypto +================================================= \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 00000000..6f8d897c --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,15 @@ +namespace: community +name: crypto +version: 0.1.0 +readme: README.md +authors: null +description: null +license: GPL-3.0-or-later +license_file: COPYING +tags: null +dependencies: + ansible.netcommon: '>=0.1.0' +repository: git@github.com:ansible-collection-migration/community.crypto.git +documentation: https://github.com/ansible-collection-migration/community.crypto/tree/master/docs +homepage: https://github.com/ansible-collection-migration/community.crypto +issues: https://github.com/ansible-collection-migration/community.crypto/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc diff --git a/meta/action_groups.yml b/meta/action_groups.yml new file mode 100644 index 00000000..735302fc --- /dev/null +++ b/meta/action_groups.yml @@ -0,0 +1,7 @@ +acme: +- acme_inspect +- acme_certificate_revoke +- acme_certificate +- acme_account +- acme_account_facts +- acme_account_info diff --git a/meta/routing.yml b/meta/routing.yml new file mode 100644 index 00000000..9d04a8ec --- /dev/null +++ b/meta/routing.yml @@ -0,0 +1,6 @@ +plugin_routing: + modules: + acme_account_facts: + deprecation: + removal_date: TBD + warning_text: see plugin documentation for details diff --git a/plugins/doc_fragments/__init__.py b/plugins/doc_fragments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/doc_fragments/acme.py b/plugins/doc_fragments/acme.py new file mode 100644 index 00000000..7e3187f7 --- /dev/null +++ b/plugins/doc_fragments/acme.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016 Michael Gruener +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +notes: + - "If a new enough version of the C(cryptography) library + is available (see Requirements for details), it will be used + instead of the C(openssl) binary. This can be explicitly disabled + or enabled with the C(select_crypto_backend) option. Note that using + the C(openssl) binary will be slower and less secure, as private key + contents always have to be stored on disk (see + C(account_key_content))." + - "Although the defaults are chosen so that the module can be used with + the L(Let's Encrypt,https://letsencrypt.org/) CA, the module can in + principle be used with any CA providing an ACME endpoint, such as + L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme)." +requirements: + - python >= 2.6 + - either openssl or L(cryptography,https://cryptography.io/) >= 1.5 +options: + account_key_src: + description: + - "Path to a file containing the ACME account RSA or Elliptic Curve + key." + - "RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can + be created with C(openssl ecparam -genkey ...). Any other tool creating + private keys in PEM format can be used as well." + - "Mutually exclusive with C(account_key_content)." + - "Required if C(account_key_content) is not used." + type: path + aliases: [ account_key ] + account_key_content: + description: + - "Content of the ACME account RSA or Elliptic Curve key." + - "Mutually exclusive with C(account_key_src)." + - "Required if C(account_key_src) is not used." + - "*Warning:* the content will be written into a temporary file, which will + be deleted by Ansible when the module completes. Since this is an + important private key — it can be used to change the account key, + or to revoke your certificates without knowing their private keys + —, this might not be acceptable." + - "In case C(cryptography) is used, the content is not written into a + temporary file. It can still happen that it is written to disk by + Ansible in the process of moving the module with its argument to + the node where it is executed." + type: str + account_uri: + description: + - "If specified, assumes that the account URI is as given. If the + account key does not match this account, or an account with this + URI does not exist, the module fails." + type: str + acme_version: + description: + - "The ACME version of the endpoint." + - "Must be 1 for the classic Let's Encrypt and Buypass ACME endpoints, + or 2 for standardized ACME v2 endpoints." + - "The default value is 1. Note that in Ansible 2.14, this option *will + be required* and will no longer have a default." + - "Please also note that we will deprecate ACME v1 support eventually." + type: int + choices: [ 1, 2 ] + acme_directory: + description: + - "The ACME directory to use. This is the entry point URL to access + CA server API." + - "For safety reasons the default is set to the Let's Encrypt staging + server (for the ACME v1 protocol). This will create technically correct, + but untrusted certificates." + - "The default value is U(https://acme-staging.api.letsencrypt.org/directory). + Note that in Ansible 2.14, this option *will be required* and will no longer + have a default." + - "For Let's Encrypt, all staging endpoints can be found here: + U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all + endpoints can be found here: + U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)" + - "For Let's Encrypt, the production directory URL for ACME v1 is + U(https://acme-v01.api.letsencrypt.org/directory), and the production + directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory)." + - "For Buypass, the production directory URL for ACME v2 and v1 is + U(https://api.buypass.com/acme/directory)." + - "*Warning:* So far, the module has only been tested against Let's Encrypt + (staging and production), Buypass (staging and production), and + L(Pebble testing server,https://github.com/letsencrypt/Pebble)." + type: str + validate_certs: + description: + - Whether calls to the ACME directory will validate TLS certificates. + - "*Warning:* Should *only ever* be set to C(no) for testing purposes, + for example when testing against a local Pebble server." + type: bool + default: yes + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to + C(openssl). + - If set to C(openssl), will try to use the C(openssl) binary. + - If set to C(cryptography), will try to use the + L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography, openssl ] +''' diff --git a/plugins/doc_fragments/ecs_credential.py b/plugins/doc_fragments/ecs_credential.py new file mode 100644 index 00000000..5f20fc7f --- /dev/null +++ b/plugins/doc_fragments/ecs_credential.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +# Copyright (c), Entrust Datacard Corporation, 2019 +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Plugin options for Entrust Certificate Services (ECS) credentials + DOCUMENTATION = r''' +options: + entrust_api_user: + description: + - The username for authentication to the Entrust Certificate Services (ECS) API. + type: str + required: true + entrust_api_key: + description: + - The key (password) for authentication to the Entrust Certificate Services (ECS) API. + type: str + required: true + entrust_api_client_cert_path: + description: + - The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. + type: path + required: true + entrust_api_client_cert_key_path: + description: + - The path to the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. + type: path + required: true + entrust_api_specification_path: + description: + - The path to the specification file defining the Entrust Certificate Services (ECS) API configuration. + - You can use this to keep a local copy of the specification to avoid downloading it every time the module is used. + type: path + default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml +requirements: + - "PyYAML >= 3.11" +''' diff --git a/plugins/module_utils/__init__.py b/plugins/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/acme.py b/plugins/module_utils/acme.py new file mode 100644 index 00000000..c9732a1f --- /dev/null +++ b/plugins/module_utils/acme.py @@ -0,0 +1,1028 @@ +# -*- coding: utf-8 -*- + +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c), Michael Gruener , 2016 +# +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import binascii +import copy +import datetime +import hashlib +import json +import locale +import os +import re +import shutil +import sys +import tempfile +import traceback + +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils._text import to_native, to_text, to_bytes +from ansible.module_utils.urls import fetch_url +from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress as compat_ipaddress +from ansible.module_utils.six.moves.urllib.parse import unquote + +try: + import cryptography + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization + import cryptography.hazmat.primitives.asymmetric.rsa + import cryptography.hazmat.primitives.asymmetric.ec + import cryptography.hazmat.primitives.asymmetric.padding + import cryptography.hazmat.primitives.hashes + import cryptography.hazmat.primitives.asymmetric.utils + import cryptography.x509 + import cryptography.x509.oid + from distutils.version import LooseVersion + CRYPTOGRAPHY_VERSION = cryptography.__version__ + HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion('1.5')) + if HAS_CURRENT_CRYPTOGRAPHY: + _cryptography_backend = cryptography.hazmat.backends.default_backend() +except Exception as dummy: + HAS_CURRENT_CRYPTOGRAPHY = False + + +class ModuleFailException(Exception): + ''' + If raised, module.fail_json() will be called with the given parameters after cleanup. + ''' + def __init__(self, msg, **args): + super(ModuleFailException, self).__init__(self, msg) + self.msg = msg + self.module_fail_args = args + + def do_fail(self, module, **arguments): + module.fail_json(msg=self.msg, other=self.module_fail_args, **arguments) + + +def nopad_b64(data): + return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "") + + +def read_file(fn, mode='b'): + try: + with open(fn, 'r' + mode) as f: + return f.read() + except Exception as e: + raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e)) + + +# function source: network/basics/uri.py +def write_file(module, dest, content): + ''' + Write content to destination file dest, only if the content + has changed. + ''' + changed = False + # create a tempfile + fd, tmpsrc = tempfile.mkstemp(text=False) + f = os.fdopen(fd, 'wb') + try: + f.write(content) + except Exception as err: + try: + f.close() + except Exception as dummy: + pass + os.remove(tmpsrc) + raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc()) + f.close() + checksum_src = None + checksum_dest = None + # raise an error if there is no tmpsrc file + if not os.path.exists(tmpsrc): + try: + os.remove(tmpsrc) + except Exception as dummy: + pass + raise ModuleFailException("Source %s does not exist" % (tmpsrc)) + if not os.access(tmpsrc, os.R_OK): + os.remove(tmpsrc) + raise ModuleFailException("Source %s not readable" % (tmpsrc)) + checksum_src = module.sha1(tmpsrc) + # check if there is no dest file + if os.path.exists(dest): + # raise an error if copy has no permission on dest + if not os.access(dest, os.W_OK): + os.remove(tmpsrc) + raise ModuleFailException("Destination %s not writable" % (dest)) + if not os.access(dest, os.R_OK): + os.remove(tmpsrc) + raise ModuleFailException("Destination %s not readable" % (dest)) + checksum_dest = module.sha1(dest) + else: + dirname = os.path.dirname(dest) or '.' + if not os.access(dirname, os.W_OK): + os.remove(tmpsrc) + raise ModuleFailException("Destination dir %s not writable" % (dirname)) + if checksum_src != checksum_dest: + try: + shutil.copyfile(tmpsrc, dest) + changed = True + except Exception as err: + os.remove(tmpsrc) + raise ModuleFailException("failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)), exception=traceback.format_exc()) + os.remove(tmpsrc) + return changed + + +def pem_to_der(pem_filename): + ''' + Load PEM file, and convert to DER. + + If PEM contains multiple entities, the first entity will be used. + ''' + certificate_lines = [] + try: + with open(pem_filename, "rt") as f: + header_line_count = 0 + for line in f: + if line.startswith('-----'): + header_line_count += 1 + if header_line_count == 2: + # If certificate file contains other certs appended + # (like intermediate certificates), ignore these. + break + continue + certificate_lines.append(line.strip()) + except Exception as err: + raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc()) + return base64.b64decode(''.join(certificate_lines)) + + +def _parse_key_openssl(openssl_binary, module, key_file=None, key_content=None): + ''' + Parses an RSA or Elliptic Curve key file in PEM format and returns a pair + (error, key_data). + ''' + # If key_file isn't given, but key_content, write that to a temporary file + if key_file is None: + fd, tmpsrc = tempfile.mkstemp() + module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit + f = os.fdopen(fd, 'wb') + try: + f.write(key_content.encode('utf-8')) + key_file = tmpsrc + except Exception as err: + try: + f.close() + except Exception as dummy: + pass + raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc()) + f.close() + # Parse key + account_key_type = None + with open(key_file, "rt") as f: + for line in f: + m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line) + if m is not None: + account_key_type = m.group(1).lower() + break + if account_key_type is None: + # This happens for example if openssl_privatekey created this key + # (as opposed to the OpenSSL binary). For now, we assume this is + # an RSA key. + # FIXME: add some kind of auto-detection + account_key_type = "rsa" + if account_key_type not in ("rsa", "ec"): + return 'unknown key type "%s"' % account_key_type, {} + + openssl_keydump_cmd = [openssl_binary, account_key_type, "-in", key_file, "-noout", "-text"] + dummy, out, dummy = module.run_command(openssl_keydump_cmd, check_rc=True) + + if account_key_type == 'rsa': + pub_hex, pub_exp = re.search( + r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", + to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + if len(pub_exp) % 2: + pub_exp = "0{0}".format(pub_exp) + + return None, { + 'key_file': key_file, + 'type': 'rsa', + 'alg': 'RS256', + 'jwk': { + "kty": "RSA", + "e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + }, + 'hash': 'sha256', + } + elif account_key_type == 'ec': + pub_data = re.search( + r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?", + to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL) + if pub_data is None: + return 'cannot parse elliptic curve key', {} + pub_hex = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_data.group(1)).encode("utf-8")) + asn1_oid_curve = pub_data.group(2).lower() + nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None + if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256': + bits = 256 + alg = 'ES256' + hashalg = 'sha256' + point_size = 32 + curve = 'P-256' + elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384': + bits = 384 + alg = 'ES384' + hashalg = 'sha384' + point_size = 48 + curve = 'P-384' + elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521': + # Not yet supported on Let's Encrypt side, see + # https://github.com/letsencrypt/boulder/issues/2217 + bits = 521 + alg = 'ES512' + hashalg = 'sha512' + point_size = 66 + curve = 'P-521' + else: + return 'unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve), {} + num_bytes = (bits + 7) // 8 + if len(pub_hex) != 2 * num_bytes: + return 'bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve), {} + return None, { + 'key_file': key_file, + 'type': 'ec', + 'alg': alg, + 'jwk': { + "kty": "EC", + "crv": curve, + "x": nopad_b64(pub_hex[:num_bytes]), + "y": nopad_b64(pub_hex[num_bytes:]), + }, + 'hash': hashalg, + 'point_size': point_size, + } + + +def _sign_request_openssl(openssl_binary, module, payload64, protected64, key_data): + openssl_sign_cmd = [openssl_binary, "dgst", "-{0}".format(key_data['hash']), "-sign", key_data['key_file']] + sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') + dummy, out, dummy = module.run_command(openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True) + + if key_data['type'] == 'ec': + dummy, der_out, dummy = module.run_command( + [openssl_binary, "asn1parse", "-inform", "DER"], + data=out, binary_data=True) + expected_len = 2 * key_data['point_size'] + sig = re.findall( + r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len, + to_text(der_out, errors='surrogate_or_strict')) + if len(sig) != 2: + raise ModuleFailException( + "failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format( + to_text(der_out, errors='surrogate_or_strict'))) + sig[0] = (expected_len - len(sig[0])) * '0' + sig[0] + sig[1] = (expected_len - len(sig[1])) * '0' + sig[1] + out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1]) + + return { + "protected": protected64, + "payload": payload64, + "signature": nopad_b64(to_bytes(out)), + } + + +if sys.version_info[0] >= 3: + # Python 3 (and newer) + def _count_bytes(n): + return (n.bit_length() + 7) // 8 if n > 0 else 0 + + def _convert_int_to_bytes(count, no): + return no.to_bytes(count, byteorder='big') + + def _pad_hex(n, digits): + res = hex(n)[2:] + if len(res) < digits: + res = '0' * (digits - len(res)) + res + return res +else: + # Python 2 + def _count_bytes(n): + if n <= 0: + return 0 + h = '%x' % n + return (len(h) + 1) // 2 + + def _convert_int_to_bytes(count, n): + h = '%x' % n + if len(h) > 2 * count: + raise Exception('Number {1} needs more than {0} bytes!'.format(count, n)) + return ('0' * (2 * count - len(h)) + h).decode('hex') + + def _pad_hex(n, digits): + h = '%x' % n + if len(h) < digits: + h = '0' * (digits - len(h)) + h + return h + + +def _parse_key_cryptography(module, key_file=None, key_content=None): + ''' + Parses an RSA or Elliptic Curve key file in PEM format and returns a pair + (error, key_data). + ''' + # If key_content isn't given, read key_file + if key_content is None: + key_content = read_file(key_file) + else: + key_content = to_bytes(key_content) + # Parse key + try: + key = cryptography.hazmat.primitives.serialization.load_pem_private_key(key_content, password=None, backend=_cryptography_backend) + except Exception as e: + return 'error while loading key: {0}'.format(e), None + if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + pk = key.public_key().public_numbers() + return None, { + 'key_obj': key, + 'type': 'rsa', + 'alg': 'RS256', + 'jwk': { + "kty": "RSA", + "e": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.e), pk.e)), + "n": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.n), pk.n)), + }, + 'hash': 'sha256', + } + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + pk = key.public_key().public_numbers() + if pk.curve.name == 'secp256r1': + bits = 256 + alg = 'ES256' + hashalg = 'sha256' + point_size = 32 + curve = 'P-256' + elif pk.curve.name == 'secp384r1': + bits = 384 + alg = 'ES384' + hashalg = 'sha384' + point_size = 48 + curve = 'P-384' + elif pk.curve.name == 'secp521r1': + # Not yet supported on Let's Encrypt side, see + # https://github.com/letsencrypt/boulder/issues/2217 + bits = 521 + alg = 'ES512' + hashalg = 'sha512' + point_size = 66 + curve = 'P-521' + else: + return 'unknown elliptic curve: {0}'.format(pk.curve.name), {} + num_bytes = (bits + 7) // 8 + return None, { + 'key_obj': key, + 'type': 'ec', + 'alg': alg, + 'jwk': { + "kty": "EC", + "crv": curve, + "x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)), + "y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)), + }, + 'hash': hashalg, + 'point_size': point_size, + } + else: + return 'unknown key type "{0}"'.format(type(key)), {} + + +def _sign_request_cryptography(module, payload64, protected64, key_data): + sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') + if isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15() + hashalg = cryptography.hazmat.primitives.hashes.SHA256 + signature = key_data['key_obj'].sign(sign_payload, padding, hashalg()) + elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + if key_data['hash'] == 'sha256': + hashalg = cryptography.hazmat.primitives.hashes.SHA256 + elif key_data['hash'] == 'sha384': + hashalg = cryptography.hazmat.primitives.hashes.SHA384 + elif key_data['hash'] == 'sha512': + hashalg = cryptography.hazmat.primitives.hashes.SHA512 + ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg()) + r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa)) + rr = _pad_hex(r, 2 * key_data['point_size']) + ss = _pad_hex(s, 2 * key_data['point_size']) + signature = binascii.unhexlify(rr) + binascii.unhexlify(ss) + + return { + "protected": protected64, + "payload": payload64, + "signature": nopad_b64(signature), + } + + +def _assert_fetch_url_success(response, info, allow_redirect=False, allow_client_error=True, allow_server_error=True): + if info['status'] < 0: + raise ModuleFailException(msg="Failure downloading %s, %s" % (info['url'], info['msg'])) + + if (300 <= info['status'] < 400 and not allow_redirect) or \ + (400 <= info['status'] < 500 and not allow_client_error) or \ + (info['status'] >= 500 and not allow_server_error): + raise ModuleFailException("ACME request failed: CODE: {0} MGS: {1} RESULT: {2}".format(info['status'], info['msg'], response)) + + +class ACMEDirectory(object): + ''' + The ACME server directory. Gives access to the available resources, + and allows to obtain a Replay-Nonce. The acme_directory URL + needs to support unauthenticated GET requests; ACME endpoints + requiring authentication are not supported. + https://tools.ietf.org/html/rfc8555#section-7.1.1 + ''' + + def __init__(self, module, account): + self.module = module + self.directory_root = module.params['acme_directory'] + self.version = module.params['acme_version'] + + self.directory, dummy = account.get_request(self.directory_root, get_only=True) + + # Check whether self.version matches what we expect + if self.version == 1: + for key in ('new-reg', 'new-authz', 'new-cert'): + if key not in self.directory: + raise ModuleFailException("ACME directory does not seem to follow protocol ACME v1") + if self.version == 2: + for key in ('newNonce', 'newAccount', 'newOrder'): + if key not in self.directory: + raise ModuleFailException("ACME directory does not seem to follow protocol ACME v2") + + def __getitem__(self, key): + return self.directory[key] + + def get_nonce(self, resource=None): + url = self.directory_root if self.version == 1 else self.directory['newNonce'] + if resource is not None: + url = resource + dummy, info = fetch_url(self.module, url, method='HEAD') + if info['status'] not in (200, 204): + raise ModuleFailException("Failed to get replay-nonce, got status {0}".format(info['status'])) + return info['replay-nonce'] + + +class ACMEAccount(object): + ''' + ACME account object. Handles the authorized communication with the + ACME server. Provides access to account bound information like + the currently active authorizations and valid certificates + ''' + + def __init__(self, module): + # Set to true to enable logging of all signed requests + self._debug = False + + self.module = module + self.version = module.params['acme_version'] + # account_key path and content are mutually exclusive + self.key = module.params['account_key_src'] + self.key_content = module.params['account_key_content'] + + # Grab account URI from module parameters. + # Make sure empty string is treated as None. + self.uri = module.params.get('account_uri') or None + + self._openssl_bin = module.get_bin_path('openssl', True) + + if self.key is not None or self.key_content is not None: + error, self.key_data = self.parse_key(self.key, self.key_content) + if error: + raise ModuleFailException("error while parsing account key: %s" % error) + self.jwk = self.key_data['jwk'] + self.jws_header = { + "alg": self.key_data['alg'], + "jwk": self.jwk, + } + if self.uri: + # Make sure self.jws_header is updated + self.set_account_uri(self.uri) + + self.directory = ACMEDirectory(module, self) + + def get_keyauthorization(self, token): + ''' + Returns the key authorization for the given token + https://tools.ietf.org/html/rfc8555#section-8.1 + ''' + accountkey_json = json.dumps(self.jwk, sort_keys=True, separators=(',', ':')) + thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + return "{0}.{1}".format(token, thumbprint) + + def parse_key(self, key_file=None, key_content=None): + ''' + Parses an RSA or Elliptic Curve key file in PEM format and returns a pair + (error, key_data). + ''' + if key_file is None and key_content is None: + raise AssertionError('One of key_file and key_content must be specified!') + if HAS_CURRENT_CRYPTOGRAPHY: + return _parse_key_cryptography(self.module, key_file, key_content) + else: + return _parse_key_openssl(self._openssl_bin, self.module, key_file, key_content) + + def sign_request(self, protected, payload, key_data, encode_payload=True): + try: + if payload is None: + # POST-as-GET + payload64 = '' + else: + # POST + if encode_payload: + payload = self.module.jsonify(payload).encode('utf8') + payload64 = nopad_b64(to_bytes(payload)) + protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8')) + except Exception as e: + raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e)) + + if HAS_CURRENT_CRYPTOGRAPHY: + return _sign_request_cryptography(self.module, payload64, protected64, key_data) + else: + return _sign_request_openssl(self._openssl_bin, self.module, payload64, protected64, key_data) + + def _log(self, msg, data=None): + ''' + Write arguments to acme.log when logging is enabled. + ''' + if self._debug: + with open('acme.log', 'ab') as f: + f.write('[{0}] {1}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%s'), msg).encode('utf-8')) + if data is not None: + f.write('{0}\n\n'.format(json.dumps(data, indent=2, sort_keys=True)).encode('utf-8')) + + def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True, encode_payload=True): + ''' + Sends a JWS signed HTTP POST request to the ACME server and returns + the response as dictionary + https://tools.ietf.org/html/rfc8555#section-6.2 + + If payload is None, a POST-as-GET is performed. + (https://tools.ietf.org/html/rfc8555#section-6.3) + ''' + key_data = key_data or self.key_data + jws_header = jws_header or self.jws_header + failed_tries = 0 + while True: + protected = copy.deepcopy(jws_header) + protected["nonce"] = self.directory.get_nonce() + if self.version != 1: + protected["url"] = url + + self._log('URL', url) + self._log('protected', protected) + self._log('payload', payload) + data = self.sign_request(protected, payload, key_data, encode_payload=encode_payload) + if self.version == 1: + data["header"] = jws_header.copy() + for k, v in protected.items(): + hv = data["header"].pop(k, None) + self._log('signed request', data) + data = self.module.jsonify(data) + + headers = { + 'Content-Type': 'application/jose+json', + } + resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST') + _assert_fetch_url_success(resp, info) + result = {} + try: + content = resp.read() + except AttributeError: + content = info.pop('body', None) + + if content or not parse_json_result: + if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600: + try: + decoded_result = self.module.from_json(content.decode('utf8')) + self._log('parsed result', decoded_result) + # In case of badNonce error, try again (up to 5 times) + # (https://tools.ietf.org/html/rfc8555#section-6.7) + if (400 <= info['status'] < 600 and + decoded_result.get('type') == 'urn:ietf:params:acme:error:badNonce' and + failed_tries <= 5): + failed_tries += 1 + continue + if parse_json_result: + result = decoded_result + else: + result = content + except ValueError: + raise ModuleFailException("Failed to parse the ACME response: {0} {1}".format(url, content)) + else: + result = content + + return result, info + + def get_request(self, uri, parse_json_result=True, headers=None, get_only=False, fail_on_error=True): + ''' + Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback + to GET if server replies with a status code of 405. + ''' + if not get_only and self.version != 1: + # Try POST-as-GET + content, info = self.send_signed_request(uri, None, parse_json_result=False) + if info['status'] == 405: + # Instead, do unauthenticated GET + get_only = True + else: + # Do unauthenticated GET + get_only = True + + if get_only: + # Perform unauthenticated GET + resp, info = fetch_url(self.module, uri, method='GET', headers=headers) + + _assert_fetch_url_success(resp, info) + + try: + content = resp.read() + except AttributeError: + content = info.pop('body', None) + + # Process result + if parse_json_result: + result = {} + if content: + if info['content-type'].startswith('application/json'): + try: + result = self.module.from_json(content.decode('utf8')) + except ValueError: + raise ModuleFailException("Failed to parse the ACME response: {0} {1}".format(uri, content)) + else: + result = content + else: + result = content + + if fail_on_error and (info['status'] < 200 or info['status'] >= 400): + raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], result)) + return result, info + + def set_account_uri(self, uri): + ''' + Set account URI. For ACME v2, it needs to be used to sending signed + requests. + ''' + self.uri = uri + if self.version != 1: + self.jws_header.pop('jwk') + self.jws_header['kid'] = self.uri + + def _new_reg(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True): + ''' + Registers a new ACME account. Returns a pair ``(created, data)``. + Here, ``created`` is ``True`` if the account was created and + ``False`` if it already existed (e.g. it was not newly created), + or does not exist. In case the account was created or exists, + ``data`` contains the account data; otherwise, it is ``None``. + https://tools.ietf.org/html/rfc8555#section-7.3 + ''' + contact = contact or [] + + if self.version == 1: + new_reg = { + 'resource': 'new-reg', + 'contact': contact + } + if agreement: + new_reg['agreement'] = agreement + else: + new_reg['agreement'] = self.directory['meta']['terms-of-service'] + url = self.directory['new-reg'] + else: + new_reg = { + 'contact': contact + } + if not allow_creation: + # https://tools.ietf.org/html/rfc8555#section-7.3.1 + new_reg['onlyReturnExisting'] = True + if terms_agreed: + new_reg['termsOfServiceAgreed'] = True + url = self.directory['newAccount'] + + result, info = self.send_signed_request(url, new_reg) + + if info['status'] in ([200, 201] if self.version == 1 else [201]): + # Account did not exist + if 'location' in info: + self.set_account_uri(info['location']) + return True, result + elif info['status'] == (409 if self.version == 1 else 200): + # Account did exist + if result.get('status') == 'deactivated': + # A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and + # Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should + # not return a valid account object according to + # https://tools.ietf.org/html/rfc8555#section-7.3.6: + # "Once an account is deactivated, the server MUST NOT accept further + # requests authorized by that account's key." + if not allow_creation: + return False, None + else: + raise ModuleFailException("Account is deactivated") + if 'location' in info: + self.set_account_uri(info['location']) + return False, result + elif info['status'] == 400 and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation: + # Account does not exist (and we didn't try to create it) + return False, None + elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''): + # Account has been deactivated; currently works for Pebble; hasn't been + # implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971), + # might need adjustment in error detection. + if not allow_creation: + return False, None + else: + raise ModuleFailException("Account is deactivated") + else: + raise ModuleFailException("Error registering: {0} {1}".format(info['status'], result)) + + def get_account_data(self): + ''' + Retrieve account information. Can only be called when the account + URI is already known (such as after calling setup_account). + Return None if the account was deactivated, or a dict otherwise. + ''' + if self.uri is None: + raise ModuleFailException("Account URI unknown") + if self.version == 1: + data = {} + data['resource'] = 'reg' + result, info = self.send_signed_request(self.uri, data) + else: + # try POST-as-GET first (draft-15 or newer) + data = None + result, info = self.send_signed_request(self.uri, data) + # check whether that failed with a malformed request error + if info['status'] >= 400 and result.get('type') == 'urn:ietf:params:acme:error:malformed': + # retry as a regular POST (with no changed data) for pre-draft-15 ACME servers + data = {} + result, info = self.send_signed_request(self.uri, data) + if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized': + # Returned when account is deactivated + return None + if info['status'] in (400, 404) and result.get('type') == 'urn:ietf:params:acme:error:accountDoesNotExist': + # Returned when account does not exist + return None + if info['status'] < 200 or info['status'] >= 300: + raise ModuleFailException("Error getting account data from {2}: {0} {1}".format(info['status'], result, self.uri)) + return result + + def setup_account(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True, remove_account_uri_if_not_exists=False): + ''' + Detect or create an account on the ACME server. For ACME v1, + as the only way (without knowing an account URI) to test if an + account exists is to try and create one with the provided account + key, this method will always result in an account being present + (except on error situations). For ACME v2, a new account will + only be created if ``allow_creation`` is set to True. + + For ACME v2, ``check_mode`` is fully respected. For ACME v1, the + account might be created if it does not yet exist. + + Return a pair ``(created, account_data)``. Here, ``created`` will + be ``True`` in case the account was created or would be created + (check mode). ``account_data`` will be the current account data, + or ``None`` if the account does not exist. + + The account URI will be stored in ``self.uri``; if it is ``None``, + the account does not exist. + + https://tools.ietf.org/html/rfc8555#section-7.3 + ''' + + if self.uri is not None: + created = False + # Verify that the account key belongs to the URI. + # (If update_contact is True, this will be done below.) + account_data = self.get_account_data() + if account_data is None: + if remove_account_uri_if_not_exists and not allow_creation: + self.uri = None + else: + raise ModuleFailException("Account is deactivated or does not exist!") + else: + created, account_data = self._new_reg( + contact, + agreement=agreement, + terms_agreed=terms_agreed, + allow_creation=allow_creation and not self.module.check_mode + ) + if self.module.check_mode and self.uri is None and allow_creation: + created = True + account_data = { + 'contact': contact or [] + } + return created, account_data + + def update_account(self, account_data, contact=None): + ''' + Update an account on the ACME server. Check mode is fully respected. + + The current account data must be provided as ``account_data``. + + Return a pair ``(updated, account_data)``, where ``updated`` is + ``True`` in case something changed (contact info updated) or + would be changed (check mode), and ``account_data`` the updated + account data. + + https://tools.ietf.org/html/rfc8555#section-7.3.2 + ''' + # Create request + update_request = {} + if contact is not None and account_data.get('contact', []) != contact: + update_request['contact'] = list(contact) + + # No change? + if not update_request: + return False, dict(account_data) + + # Apply change + if self.module.check_mode: + account_data = dict(account_data) + account_data.update(update_request) + else: + if self.version == 1: + update_request['resource'] = 'reg' + account_data, dummy = self.send_signed_request(self.uri, update_request) + return True, account_data + + +def _normalize_ip(ip): + try: + return to_native(compat_ipaddress.ip_address(to_text(ip)).compressed) + except ValueError: + # We don't want to error out on something IPAddress() can't parse + return ip + + +def openssl_get_csr_identifiers(openssl_binary, module, csr_filename): + ''' + Return a set of requested identifiers (CN and SANs) for the CSR. + Each identifier is a pair (type, identifier), where type is either + 'dns' or 'ip'. + ''' + openssl_csr_cmd = [openssl_binary, "req", "-in", csr_filename, "-noout", "-text"] + dummy, out, dummy = module.run_command(openssl_csr_cmd, check_rc=True) + + identifiers = set([]) + common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict')) + if common_name is not None: + identifiers.add(('dns', common_name.group(1))) + subject_alt_names = re.search( + r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", + to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL) + if subject_alt_names is not None: + for san in subject_alt_names.group(1).split(", "): + if san.lower().startswith("dns:"): + identifiers.add(('dns', san[4:])) + elif san.lower().startswith("ip:"): + identifiers.add(('ip', _normalize_ip(san[3:]))) + elif san.lower().startswith("ip address:"): + identifiers.add(('ip', _normalize_ip(san[11:]))) + else: + raise ModuleFailException('Found unsupported SAN identifier "{0}"'.format(san)) + return identifiers + + +def cryptography_get_csr_identifiers(module, csr_filename): + ''' + Return a set of requested identifiers (CN and SANs) for the CSR. + Each identifier is a pair (type, identifier), where type is either + 'dns' or 'ip'. + ''' + identifiers = set([]) + csr = cryptography.x509.load_pem_x509_csr(read_file(csr_filename), _cryptography_backend) + for sub in csr.subject: + if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME: + identifiers.add(('dns', sub.value)) + for extension in csr.extensions: + if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: + for name in extension.value: + if isinstance(name, cryptography.x509.DNSName): + identifiers.add(('dns', name.value)) + elif isinstance(name, cryptography.x509.IPAddress): + identifiers.add(('ip', name.value.compressed)) + else: + raise ModuleFailException('Found unsupported SAN identifier {0}'.format(name)) + return identifiers + + +def cryptography_get_cert_days(module, cert_file, now=None): + ''' + Return the days the certificate in cert_file remains valid and -1 + if the file was not found. If cert_file contains more than one + certificate, only the first one will be considered. + ''' + if not os.path.exists(cert_file): + return -1 + + try: + cert = cryptography.x509.load_pem_x509_certificate(read_file(cert_file), _cryptography_backend) + except Exception as e: + raise ModuleFailException('Cannot parse certificate {0}: {1}'.format(cert_file, e)) + if now is None: + now = datetime.datetime.now() + return (cert.not_valid_after - now).days + + +def set_crypto_backend(module): + ''' + Sets which crypto backend to use (default: auto detection). + + Does not care whether a new enough cryptoraphy is available or not. Must + be called before any real stuff is done which might evaluate + ``HAS_CURRENT_CRYPTOGRAPHY``. + ''' + global HAS_CURRENT_CRYPTOGRAPHY + # Choose backend + backend = module.params['select_crypto_backend'] + if backend == 'auto': + pass + elif backend == 'openssl': + HAS_CURRENT_CRYPTOGRAPHY = False + elif backend == 'cryptography': + try: + cryptography.__version__ + except Exception as dummy: + module.fail_json(msg=missing_required_lib('cryptography')) + HAS_CURRENT_CRYPTOGRAPHY = True + else: + module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend)) + # Inform about choices + if HAS_CURRENT_CRYPTOGRAPHY: + module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION)) + return 'cryptography' + else: + module.debug('Using OpenSSL binary backend') + return 'openssl' + + +def process_links(info, callback): + ''' + Process link header, calls callback for every link header with the URL and relation as options. + ''' + if 'link' in info: + link = info['link'] + for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link): + callback(unquote(url), relation) + + +def get_default_argspec(): + ''' + Provides default argument spec for the options documented in the acme doc fragment. + ''' + return dict( + account_key_src=dict(type='path', aliases=['account_key']), + account_key_content=dict(type='str', no_log=True), + account_uri=dict(type='str'), + acme_directory=dict(type='str'), + acme_version=dict(type='int', choices=[1, 2]), + validate_certs=dict(type='bool', default=True), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']), + ) + + +def handle_standard_module_arguments(module, needs_acme_v2=False): + ''' + Do standard module setup, argument handling and warning emitting. + ''' + backend = set_crypto_backend(module) + + if not module.params['validate_certs']: + module.warn( + 'Disabling certificate validation for communications with ACME endpoint. ' + 'This should only be done for testing against a local ACME server for ' + 'development purposes, but *never* for production purposes.' + ) + + if module.params['acme_version'] is None: + module.params['acme_version'] = 1 + module.deprecate("The option 'acme_version' will be required from Ansible 2.14 on", version='2.14') + + if module.params['acme_directory'] is None: + module.params['acme_directory'] = 'https://acme-staging.api.letsencrypt.org/directory' + module.deprecate("The option 'acme_directory' will be required from Ansible 2.14 on", version='2.14') + + if needs_acme_v2 and module.params['acme_version'] < 2: + module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name)) + + # AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates. + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + locale.setlocale(locale.LC_ALL, 'C') + + return backend diff --git a/plugins/module_utils/crypto.py b/plugins/module_utils/crypto.py new file mode 100644 index 00000000..e67eeff1 --- /dev/null +++ b/plugins/module_utils/crypto.py @@ -0,0 +1,2125 @@ +# -*- coding: utf-8 -*- +# +# (c) 2016, Yanis Guenane +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +# ---------------------------------------------------------------------- +# A clearly marked portion of this file is licensed under the BSD license +# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk) +# Copyright (c) 2017 Fraser Tweedale (@frasertweedale) +# For more details, search for the function _obj2txt(). +# --------------------------------------------------------------------- +# A clearly marked portion of this file is extracted from a project that +# is licensed under the Apache License 2.0 +# Copyright (c) the OpenSSL contributors +# For more details, search for the function _OID_MAP. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import sys +from distutils.version import LooseVersion + +try: + import OpenSSL + from OpenSSL import crypto +except ImportError: + # An error will be raised in the calling class to let the end + # user know that OpenSSL couldn't be found. + pass + +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend as cryptography_backend + from cryptography.hazmat.primitives.serialization import load_pem_private_key + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives import serialization + import ipaddress + + # Older versions of cryptography (< 2.1) do not have __hash__ functions for + # general name objects (DNSName, IPAddress, ...), while providing overloaded + # equality and string representation operations. This makes it impossible to + # use them in hash-based data structures such as set or dict. Since we are + # actually doing that in openssl_certificate, and potentially in other code, + # we need to monkey-patch __hash__ for these classes to make sure our code + # works fine. + if LooseVersion(cryptography.__version__) < LooseVersion('2.1'): + # A very simply hash function which relies on the representation + # of an object to be implemented. This is the case since at least + # cryptography 1.0, see + # https://github.com/pyca/cryptography/commit/7a9abce4bff36c05d26d8d2680303a6f64a0e84f + def simple_hash(self): + return hash(repr(self)) + + # The hash functions for the following types were added for cryptography 2.1: + # https://github.com/pyca/cryptography/commit/fbfc36da2a4769045f2373b004ddf0aff906cf38 + x509.DNSName.__hash__ = simple_hash + x509.DirectoryName.__hash__ = simple_hash + x509.GeneralName.__hash__ = simple_hash + x509.IPAddress.__hash__ = simple_hash + x509.OtherName.__hash__ = simple_hash + x509.RegisteredID.__hash__ = simple_hash + + if LooseVersion(cryptography.__version__) < LooseVersion('1.2'): + # The hash functions for the following types were added for cryptography 1.2: + # https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0 + # https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486 + x509.RFC822Name.__hash__ = simple_hash + x509.UniformResourceIdentifier.__hash__ = simple_hash + + # Test whether we have support for X25519, X448, Ed25519 and/or Ed448 + try: + import cryptography.hazmat.primitives.asymmetric.x25519 + CRYPTOGRAPHY_HAS_X25519 = True + try: + cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes + CRYPTOGRAPHY_HAS_X25519_FULL = True + except AttributeError: + CRYPTOGRAPHY_HAS_X25519_FULL = False + except ImportError: + CRYPTOGRAPHY_HAS_X25519 = False + CRYPTOGRAPHY_HAS_X25519_FULL = False + try: + import cryptography.hazmat.primitives.asymmetric.x448 + CRYPTOGRAPHY_HAS_X448 = True + except ImportError: + CRYPTOGRAPHY_HAS_X448 = False + try: + import cryptography.hazmat.primitives.asymmetric.ed25519 + CRYPTOGRAPHY_HAS_ED25519 = True + except ImportError: + CRYPTOGRAPHY_HAS_ED25519 = False + try: + import cryptography.hazmat.primitives.asymmetric.ed448 + CRYPTOGRAPHY_HAS_ED448 = True + except ImportError: + CRYPTOGRAPHY_HAS_ED448 = False + + HAS_CRYPTOGRAPHY = True +except ImportError: + # Error handled in the calling module. + CRYPTOGRAPHY_HAS_X25519 = False + CRYPTOGRAPHY_HAS_X25519_FULL = False + CRYPTOGRAPHY_HAS_X448 = False + CRYPTOGRAPHY_HAS_ED25519 = False + CRYPTOGRAPHY_HAS_ED448 = False + HAS_CRYPTOGRAPHY = False + + +import abc +import base64 +import binascii +import datetime +import errno +import hashlib +import os +import re +import tempfile + +from ansible.module_utils import six +from ansible.module_utils._text import to_native, to_bytes, to_text + + +class OpenSSLObjectError(Exception): + pass + + +class OpenSSLBadPassphraseError(OpenSSLObjectError): + pass + + +def get_fingerprint_of_bytes(source): + """Generate the fingerprint of the given bytes.""" + + fingerprint = {} + + try: + algorithms = hashlib.algorithms + except AttributeError: + try: + algorithms = hashlib.algorithms_guaranteed + except AttributeError: + return None + + for algo in algorithms: + f = getattr(hashlib, algo) + try: + h = f(source) + except ValueError: + # This can happen for hash algorithms not supported in FIPS mode + # (https://github.com/ansible/ansible/issues/67213) + continue + try: + # Certain hash functions have a hexdigest() which expects a length parameter + pubkey_digest = h.hexdigest() + except TypeError: + pubkey_digest = h.hexdigest(32) + fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2)) + + return fingerprint + + +def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'): + """Generate the fingerprint of the public key. """ + + privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend) + + if backend == 'pyopenssl': + try: + publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey) + except AttributeError: + # If PyOpenSSL < 16.0 crypto.dump_publickey() will fail. + try: + bio = crypto._new_mem_buf() + rc = crypto._lib.i2d_PUBKEY_bio(bio, privatekey._pkey) + if rc != 1: + crypto._raise_current_error() + publickey = crypto._bio_to_string(bio) + except AttributeError: + # By doing this we prevent the code from raising an error + # yet we return no value in the fingerprint hash. + return None + elif backend == 'cryptography': + publickey = privatekey.public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + + return get_fingerprint_of_bytes(publickey) + + +def load_file_if_exists(path, module=None, ignore_errors=False): + try: + with open(path, 'rb') as f: + return f.read() + except EnvironmentError as exc: + if exc.errno == errno.ENOENT: + return None + if ignore_errors: + return None + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + except Exception as exc: + if ignore_errors: + return None + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + + +def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'): + """Load the specified OpenSSL private key. + + The content can also be specified via content; in that case, + this function will not load the key from disk. + """ + + try: + if content is None: + with open(path, 'rb') as b_priv_key_fh: + priv_key_detail = b_priv_key_fh.read() + else: + priv_key_detail = content + + if backend == 'pyopenssl': + + # First try: try to load with real passphrase (resp. empty string) + # Will work if this is the correct passphrase, or the key is not + # password-protected. + try: + result = crypto.load_privatekey(crypto.FILETYPE_PEM, + priv_key_detail, + to_bytes(passphrase or '')) + except crypto.Error as e: + if len(e.args) > 0 and len(e.args[0]) > 0: + if e.args[0][0][2] in ('bad decrypt', 'bad password read'): + # This happens in case we have the wrong passphrase. + if passphrase is not None: + raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!') + else: + raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') + raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e)) + if check_passphrase: + # Next we want to make sure that the key is actually protected by + # a passphrase (in case we did try the empty string before, make + # sure that the key is not protected by the empty string) + try: + crypto.load_privatekey(crypto.FILETYPE_PEM, + priv_key_detail, + to_bytes('y' if passphrase == 'x' else 'x')) + if passphrase is not None: + # Since we can load the key without an exception, the + # key isn't password-protected + raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!') + except crypto.Error as e: + if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0: + if e.args[0][0][2] in ('bad decrypt', 'bad password read'): + # The key is obviously protected by the empty string. + # Don't do this at home (if it's possible at all)... + raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') + elif backend == 'cryptography': + try: + result = load_pem_private_key(priv_key_detail, + None if passphrase is None else to_bytes(passphrase), + cryptography_backend()) + except TypeError as dummy: + raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key') + except ValueError as dummy: + raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key') + + return result + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + + +def load_certificate(path, content=None, backend='pyopenssl'): + """Load the specified certificate.""" + + try: + if content is None: + with open(path, 'rb') as cert_fh: + cert_content = cert_fh.read() + else: + cert_content = content + if backend == 'pyopenssl': + return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content) + elif backend == 'cryptography': + return x509.load_pem_x509_certificate(cert_content, cryptography_backend()) + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + + +def load_certificate_request(path, content=None, backend='pyopenssl'): + """Load the specified certificate signing request.""" + try: + if content is None: + with open(path, 'rb') as csr_fh: + csr_content = csr_fh.read() + else: + csr_content = content + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + if backend == 'pyopenssl': + return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content) + elif backend == 'cryptography': + return x509.load_pem_x509_csr(csr_content, cryptography_backend()) + + +def parse_name_field(input_dict): + """Take a dict with key: value or key: list_of_values mappings and return a list of tuples""" + + result = [] + for key in input_dict: + if isinstance(input_dict[key], list): + for entry in input_dict[key]: + result.append((key, entry)) + else: + result.append((key, input_dict[key])) + return result + + +def convert_relative_to_datetime(relative_time_string): + """Get a datetime.datetime or None from a string in the time format described in sshd_config(5)""" + + parsed_result = re.match( + r"^(?P[+-])((?P\d+)[wW])?((?P\d+)[dD])?((?P\d+)[hH])?((?P\d+)[mM])?((?P\d+)[sS]?)?$", + relative_time_string) + + if parsed_result is None or len(relative_time_string) == 1: + # not matched or only a single "+" or "-" + return None + + offset = datetime.timedelta(0) + if parsed_result.group("weeks") is not None: + offset += datetime.timedelta(weeks=int(parsed_result.group("weeks"))) + if parsed_result.group("days") is not None: + offset += datetime.timedelta(days=int(parsed_result.group("days"))) + if parsed_result.group("hours") is not None: + offset += datetime.timedelta(hours=int(parsed_result.group("hours"))) + if parsed_result.group("minutes") is not None: + offset += datetime.timedelta( + minutes=int(parsed_result.group("minutes"))) + if parsed_result.group("seconds") is not None: + offset += datetime.timedelta( + seconds=int(parsed_result.group("seconds"))) + + if parsed_result.group("prefix") == "+": + return datetime.datetime.utcnow() + offset + else: + return datetime.datetime.utcnow() - offset + + +def get_relative_time_option(input_string, input_name, backend='cryptography'): + """Return an absolute timespec if a relative timespec or an ASN1 formatted + string is provided. + + The return value will be a datetime object for the cryptography backend, + and a ASN1 formatted string for the pyopenssl backend.""" + result = to_native(input_string) + if result is None: + raise OpenSSLObjectError( + 'The timespec "%s" for %s is not valid' % + input_string, input_name) + # Relative time + if result.startswith("+") or result.startswith("-"): + result_datetime = convert_relative_to_datetime(result) + if backend == 'pyopenssl': + return result_datetime.strftime("%Y%m%d%H%M%SZ") + elif backend == 'cryptography': + return result_datetime + # Absolute time + if backend == 'pyopenssl': + return input_string + elif backend == 'cryptography': + for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']: + try: + return datetime.datetime.strptime(result, date_fmt) + except ValueError: + pass + + raise OpenSSLObjectError( + 'The time spec "%s" for %s is invalid' % + (input_string, input_name) + ) + + +def select_message_digest(digest_string): + digest = None + if digest_string == 'sha256': + digest = hashes.SHA256() + elif digest_string == 'sha384': + digest = hashes.SHA384() + elif digest_string == 'sha512': + digest = hashes.SHA512() + elif digest_string == 'sha1': + digest = hashes.SHA1() + elif digest_string == 'md5': + digest = hashes.MD5() + return digest + + +def write_file(module, content, default_mode=None, path=None): + ''' + Writes content into destination file as securely as possible. + Uses file arguments from module. + ''' + # Find out parameters for file + file_args = module.load_file_common_arguments(module.params, path=path) + if file_args['mode'] is None: + file_args['mode'] = default_mode + # Create tempfile name + tmp_fd, tmp_name = tempfile.mkstemp(prefix=b'.ansible_tmp') + try: + os.close(tmp_fd) + except Exception as dummy: + pass + module.add_cleanup_file(tmp_name) # if we fail, let Ansible try to remove the file + try: + try: + # Create tempfile + file = os.open(tmp_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + os.write(file, content) + os.close(file) + except Exception as e: + try: + os.remove(tmp_name) + except Exception as dummy: + pass + module.fail_json(msg='Error while writing result into temporary file: {0}'.format(e)) + # Update destination to wanted permissions + if os.path.exists(file_args['path']): + module.set_fs_attributes_if_different(file_args, False) + # Move tempfile to final destination + module.atomic_move(tmp_name, file_args['path']) + # Try to update permissions again + module.set_fs_attributes_if_different(file_args, False) + except Exception as e: + try: + os.remove(tmp_name) + except Exception as dummy: + pass + module.fail_json(msg='Error while writing result: {0}'.format(e)) + + +@six.add_metaclass(abc.ABCMeta) +class OpenSSLObject(object): + + def __init__(self, path, state, force, check_mode): + self.path = path + self.state = state + self.force = force + self.name = os.path.basename(path) + self.changed = False + self.check_mode = check_mode + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + def _check_state(): + return os.path.exists(self.path) + + def _check_perms(module): + file_args = module.load_file_common_arguments(module.params) + return not module.set_fs_attributes_if_different(file_args, False) + + if not perms_required: + return _check_state() + + return _check_state() and _check_perms(module) + + @abc.abstractmethod + def dump(self): + """Serialize the object into a dictionary.""" + + pass + + @abc.abstractmethod + def generate(self): + """Generate the resource.""" + + pass + + def remove(self, module): + """Remove the resource from the filesystem.""" + + try: + os.remove(self.path) + self.changed = True + except OSError as exc: + if exc.errno != errno.ENOENT: + raise OpenSSLObjectError(exc) + else: + pass + + +# ##################################################################################### +# ##################################################################################### +# This has been extracted from the OpenSSL project's objects.txt: +# https://github.com/openssl/openssl/blob/9537fe5757bb07761fa275d779bbd40bcf5530e4/crypto/objects/objects.txt +# Extracted with https://gist.github.com/felixfontein/376748017ad65ead093d56a45a5bf376 +# +# In case the following data structure has any copyrightable content, note that it is licensed as follows: +# Copyright (c) the OpenSSL contributors +# Licensed under the Apache License 2.0 +# https://github.com/openssl/openssl/blob/master/LICENSE +_OID_MAP = { + '0': ('itu-t', 'ITU-T', 'ccitt'), + '0.3.4401.5': ('ntt-ds', ), + '0.3.4401.5.3.1.9': ('camellia', ), + '0.3.4401.5.3.1.9.1': ('camellia-128-ecb', 'CAMELLIA-128-ECB'), + '0.3.4401.5.3.1.9.3': ('camellia-128-ofb', 'CAMELLIA-128-OFB'), + '0.3.4401.5.3.1.9.4': ('camellia-128-cfb', 'CAMELLIA-128-CFB'), + '0.3.4401.5.3.1.9.6': ('camellia-128-gcm', 'CAMELLIA-128-GCM'), + '0.3.4401.5.3.1.9.7': ('camellia-128-ccm', 'CAMELLIA-128-CCM'), + '0.3.4401.5.3.1.9.9': ('camellia-128-ctr', 'CAMELLIA-128-CTR'), + '0.3.4401.5.3.1.9.10': ('camellia-128-cmac', 'CAMELLIA-128-CMAC'), + '0.3.4401.5.3.1.9.21': ('camellia-192-ecb', 'CAMELLIA-192-ECB'), + '0.3.4401.5.3.1.9.23': ('camellia-192-ofb', 'CAMELLIA-192-OFB'), + '0.3.4401.5.3.1.9.24': ('camellia-192-cfb', 'CAMELLIA-192-CFB'), + '0.3.4401.5.3.1.9.26': ('camellia-192-gcm', 'CAMELLIA-192-GCM'), + '0.3.4401.5.3.1.9.27': ('camellia-192-ccm', 'CAMELLIA-192-CCM'), + '0.3.4401.5.3.1.9.29': ('camellia-192-ctr', 'CAMELLIA-192-CTR'), + '0.3.4401.5.3.1.9.30': ('camellia-192-cmac', 'CAMELLIA-192-CMAC'), + '0.3.4401.5.3.1.9.41': ('camellia-256-ecb', 'CAMELLIA-256-ECB'), + '0.3.4401.5.3.1.9.43': ('camellia-256-ofb', 'CAMELLIA-256-OFB'), + '0.3.4401.5.3.1.9.44': ('camellia-256-cfb', 'CAMELLIA-256-CFB'), + '0.3.4401.5.3.1.9.46': ('camellia-256-gcm', 'CAMELLIA-256-GCM'), + '0.3.4401.5.3.1.9.47': ('camellia-256-ccm', 'CAMELLIA-256-CCM'), + '0.3.4401.5.3.1.9.49': ('camellia-256-ctr', 'CAMELLIA-256-CTR'), + '0.3.4401.5.3.1.9.50': ('camellia-256-cmac', 'CAMELLIA-256-CMAC'), + '0.9': ('data', ), + '0.9.2342': ('pss', ), + '0.9.2342.19200300': ('ucl', ), + '0.9.2342.19200300.100': ('pilot', ), + '0.9.2342.19200300.100.1': ('pilotAttributeType', ), + '0.9.2342.19200300.100.1.1': ('userId', 'UID'), + '0.9.2342.19200300.100.1.2': ('textEncodedORAddress', ), + '0.9.2342.19200300.100.1.3': ('rfc822Mailbox', 'mail'), + '0.9.2342.19200300.100.1.4': ('info', ), + '0.9.2342.19200300.100.1.5': ('favouriteDrink', ), + '0.9.2342.19200300.100.1.6': ('roomNumber', ), + '0.9.2342.19200300.100.1.7': ('photo', ), + '0.9.2342.19200300.100.1.8': ('userClass', ), + '0.9.2342.19200300.100.1.9': ('host', ), + '0.9.2342.19200300.100.1.10': ('manager', ), + '0.9.2342.19200300.100.1.11': ('documentIdentifier', ), + '0.9.2342.19200300.100.1.12': ('documentTitle', ), + '0.9.2342.19200300.100.1.13': ('documentVersion', ), + '0.9.2342.19200300.100.1.14': ('documentAuthor', ), + '0.9.2342.19200300.100.1.15': ('documentLocation', ), + '0.9.2342.19200300.100.1.20': ('homeTelephoneNumber', ), + '0.9.2342.19200300.100.1.21': ('secretary', ), + '0.9.2342.19200300.100.1.22': ('otherMailbox', ), + '0.9.2342.19200300.100.1.23': ('lastModifiedTime', ), + '0.9.2342.19200300.100.1.24': ('lastModifiedBy', ), + '0.9.2342.19200300.100.1.25': ('domainComponent', 'DC'), + '0.9.2342.19200300.100.1.26': ('aRecord', ), + '0.9.2342.19200300.100.1.27': ('pilotAttributeType27', ), + '0.9.2342.19200300.100.1.28': ('mXRecord', ), + '0.9.2342.19200300.100.1.29': ('nSRecord', ), + '0.9.2342.19200300.100.1.30': ('sOARecord', ), + '0.9.2342.19200300.100.1.31': ('cNAMERecord', ), + '0.9.2342.19200300.100.1.37': ('associatedDomain', ), + '0.9.2342.19200300.100.1.38': ('associatedName', ), + '0.9.2342.19200300.100.1.39': ('homePostalAddress', ), + '0.9.2342.19200300.100.1.40': ('personalTitle', ), + '0.9.2342.19200300.100.1.41': ('mobileTelephoneNumber', ), + '0.9.2342.19200300.100.1.42': ('pagerTelephoneNumber', ), + '0.9.2342.19200300.100.1.43': ('friendlyCountryName', ), + '0.9.2342.19200300.100.1.44': ('uniqueIdentifier', 'uid'), + '0.9.2342.19200300.100.1.45': ('organizationalStatus', ), + '0.9.2342.19200300.100.1.46': ('janetMailbox', ), + '0.9.2342.19200300.100.1.47': ('mailPreferenceOption', ), + '0.9.2342.19200300.100.1.48': ('buildingName', ), + '0.9.2342.19200300.100.1.49': ('dSAQuality', ), + '0.9.2342.19200300.100.1.50': ('singleLevelQuality', ), + '0.9.2342.19200300.100.1.51': ('subtreeMinimumQuality', ), + '0.9.2342.19200300.100.1.52': ('subtreeMaximumQuality', ), + '0.9.2342.19200300.100.1.53': ('personalSignature', ), + '0.9.2342.19200300.100.1.54': ('dITRedirect', ), + '0.9.2342.19200300.100.1.55': ('audio', ), + '0.9.2342.19200300.100.1.56': ('documentPublisher', ), + '0.9.2342.19200300.100.3': ('pilotAttributeSyntax', ), + '0.9.2342.19200300.100.3.4': ('iA5StringSyntax', ), + '0.9.2342.19200300.100.3.5': ('caseIgnoreIA5StringSyntax', ), + '0.9.2342.19200300.100.4': ('pilotObjectClass', ), + '0.9.2342.19200300.100.4.3': ('pilotObject', ), + '0.9.2342.19200300.100.4.4': ('pilotPerson', ), + '0.9.2342.19200300.100.4.5': ('account', ), + '0.9.2342.19200300.100.4.6': ('document', ), + '0.9.2342.19200300.100.4.7': ('room', ), + '0.9.2342.19200300.100.4.9': ('documentSeries', ), + '0.9.2342.19200300.100.4.13': ('Domain', 'domain'), + '0.9.2342.19200300.100.4.14': ('rFC822localPart', ), + '0.9.2342.19200300.100.4.15': ('dNSDomain', ), + '0.9.2342.19200300.100.4.17': ('domainRelatedObject', ), + '0.9.2342.19200300.100.4.18': ('friendlyCountry', ), + '0.9.2342.19200300.100.4.19': ('simpleSecurityObject', ), + '0.9.2342.19200300.100.4.20': ('pilotOrganization', ), + '0.9.2342.19200300.100.4.21': ('pilotDSA', ), + '0.9.2342.19200300.100.4.22': ('qualityLabelledData', ), + '0.9.2342.19200300.100.10': ('pilotGroups', ), + '1': ('iso', 'ISO'), + '1.0.9797.3.4': ('gmac', 'GMAC'), + '1.0.10118.3.0.55': ('whirlpool', ), + '1.2': ('ISO Member Body', 'member-body'), + '1.2.156': ('ISO CN Member Body', 'ISO-CN'), + '1.2.156.10197': ('oscca', ), + '1.2.156.10197.1': ('sm-scheme', ), + '1.2.156.10197.1.104.1': ('sm4-ecb', 'SM4-ECB'), + '1.2.156.10197.1.104.2': ('sm4-cbc', 'SM4-CBC'), + '1.2.156.10197.1.104.3': ('sm4-ofb', 'SM4-OFB'), + '1.2.156.10197.1.104.4': ('sm4-cfb', 'SM4-CFB'), + '1.2.156.10197.1.104.5': ('sm4-cfb1', 'SM4-CFB1'), + '1.2.156.10197.1.104.6': ('sm4-cfb8', 'SM4-CFB8'), + '1.2.156.10197.1.104.7': ('sm4-ctr', 'SM4-CTR'), + '1.2.156.10197.1.301': ('sm2', 'SM2'), + '1.2.156.10197.1.401': ('sm3', 'SM3'), + '1.2.156.10197.1.501': ('SM2-with-SM3', 'SM2-SM3'), + '1.2.156.10197.1.504': ('sm3WithRSAEncryption', 'RSA-SM3'), + '1.2.392.200011.61.1.1.1.2': ('camellia-128-cbc', 'CAMELLIA-128-CBC'), + '1.2.392.200011.61.1.1.1.3': ('camellia-192-cbc', 'CAMELLIA-192-CBC'), + '1.2.392.200011.61.1.1.1.4': ('camellia-256-cbc', 'CAMELLIA-256-CBC'), + '1.2.392.200011.61.1.1.3.2': ('id-camellia128-wrap', ), + '1.2.392.200011.61.1.1.3.3': ('id-camellia192-wrap', ), + '1.2.392.200011.61.1.1.3.4': ('id-camellia256-wrap', ), + '1.2.410.200004': ('kisa', 'KISA'), + '1.2.410.200004.1.3': ('seed-ecb', 'SEED-ECB'), + '1.2.410.200004.1.4': ('seed-cbc', 'SEED-CBC'), + '1.2.410.200004.1.5': ('seed-cfb', 'SEED-CFB'), + '1.2.410.200004.1.6': ('seed-ofb', 'SEED-OFB'), + '1.2.410.200046.1.1': ('aria', ), + '1.2.410.200046.1.1.1': ('aria-128-ecb', 'ARIA-128-ECB'), + '1.2.410.200046.1.1.2': ('aria-128-cbc', 'ARIA-128-CBC'), + '1.2.410.200046.1.1.3': ('aria-128-cfb', 'ARIA-128-CFB'), + '1.2.410.200046.1.1.4': ('aria-128-ofb', 'ARIA-128-OFB'), + '1.2.410.200046.1.1.5': ('aria-128-ctr', 'ARIA-128-CTR'), + '1.2.410.200046.1.1.6': ('aria-192-ecb', 'ARIA-192-ECB'), + '1.2.410.200046.1.1.7': ('aria-192-cbc', 'ARIA-192-CBC'), + '1.2.410.200046.1.1.8': ('aria-192-cfb', 'ARIA-192-CFB'), + '1.2.410.200046.1.1.9': ('aria-192-ofb', 'ARIA-192-OFB'), + '1.2.410.200046.1.1.10': ('aria-192-ctr', 'ARIA-192-CTR'), + '1.2.410.200046.1.1.11': ('aria-256-ecb', 'ARIA-256-ECB'), + '1.2.410.200046.1.1.12': ('aria-256-cbc', 'ARIA-256-CBC'), + '1.2.410.200046.1.1.13': ('aria-256-cfb', 'ARIA-256-CFB'), + '1.2.410.200046.1.1.14': ('aria-256-ofb', 'ARIA-256-OFB'), + '1.2.410.200046.1.1.15': ('aria-256-ctr', 'ARIA-256-CTR'), + '1.2.410.200046.1.1.34': ('aria-128-gcm', 'ARIA-128-GCM'), + '1.2.410.200046.1.1.35': ('aria-192-gcm', 'ARIA-192-GCM'), + '1.2.410.200046.1.1.36': ('aria-256-gcm', 'ARIA-256-GCM'), + '1.2.410.200046.1.1.37': ('aria-128-ccm', 'ARIA-128-CCM'), + '1.2.410.200046.1.1.38': ('aria-192-ccm', 'ARIA-192-CCM'), + '1.2.410.200046.1.1.39': ('aria-256-ccm', 'ARIA-256-CCM'), + '1.2.643.2.2': ('cryptopro', ), + '1.2.643.2.2.3': ('GOST R 34.11-94 with GOST R 34.10-2001', 'id-GostR3411-94-with-GostR3410-2001'), + '1.2.643.2.2.4': ('GOST R 34.11-94 with GOST R 34.10-94', 'id-GostR3411-94-with-GostR3410-94'), + '1.2.643.2.2.9': ('GOST R 34.11-94', 'md_gost94'), + '1.2.643.2.2.10': ('HMAC GOST 34.11-94', 'id-HMACGostR3411-94'), + '1.2.643.2.2.14.0': ('id-Gost28147-89-None-KeyMeshing', ), + '1.2.643.2.2.14.1': ('id-Gost28147-89-CryptoPro-KeyMeshing', ), + '1.2.643.2.2.19': ('GOST R 34.10-2001', 'gost2001'), + '1.2.643.2.2.20': ('GOST R 34.10-94', 'gost94'), + '1.2.643.2.2.20.1': ('id-GostR3410-94-a', ), + '1.2.643.2.2.20.2': ('id-GostR3410-94-aBis', ), + '1.2.643.2.2.20.3': ('id-GostR3410-94-b', ), + '1.2.643.2.2.20.4': ('id-GostR3410-94-bBis', ), + '1.2.643.2.2.21': ('GOST 28147-89', 'gost89'), + '1.2.643.2.2.22': ('GOST 28147-89 MAC', 'gost-mac'), + '1.2.643.2.2.23': ('GOST R 34.11-94 PRF', 'prf-gostr3411-94'), + '1.2.643.2.2.30.0': ('id-GostR3411-94-TestParamSet', ), + '1.2.643.2.2.30.1': ('id-GostR3411-94-CryptoProParamSet', ), + '1.2.643.2.2.31.0': ('id-Gost28147-89-TestParamSet', ), + '1.2.643.2.2.31.1': ('id-Gost28147-89-CryptoPro-A-ParamSet', ), + '1.2.643.2.2.31.2': ('id-Gost28147-89-CryptoPro-B-ParamSet', ), + '1.2.643.2.2.31.3': ('id-Gost28147-89-CryptoPro-C-ParamSet', ), + '1.2.643.2.2.31.4': ('id-Gost28147-89-CryptoPro-D-ParamSet', ), + '1.2.643.2.2.31.5': ('id-Gost28147-89-CryptoPro-Oscar-1-1-ParamSet', ), + '1.2.643.2.2.31.6': ('id-Gost28147-89-CryptoPro-Oscar-1-0-ParamSet', ), + '1.2.643.2.2.31.7': ('id-Gost28147-89-CryptoPro-RIC-1-ParamSet', ), + '1.2.643.2.2.32.0': ('id-GostR3410-94-TestParamSet', ), + '1.2.643.2.2.32.2': ('id-GostR3410-94-CryptoPro-A-ParamSet', ), + '1.2.643.2.2.32.3': ('id-GostR3410-94-CryptoPro-B-ParamSet', ), + '1.2.643.2.2.32.4': ('id-GostR3410-94-CryptoPro-C-ParamSet', ), + '1.2.643.2.2.32.5': ('id-GostR3410-94-CryptoPro-D-ParamSet', ), + '1.2.643.2.2.33.1': ('id-GostR3410-94-CryptoPro-XchA-ParamSet', ), + '1.2.643.2.2.33.2': ('id-GostR3410-94-CryptoPro-XchB-ParamSet', ), + '1.2.643.2.2.33.3': ('id-GostR3410-94-CryptoPro-XchC-ParamSet', ), + '1.2.643.2.2.35.0': ('id-GostR3410-2001-TestParamSet', ), + '1.2.643.2.2.35.1': ('id-GostR3410-2001-CryptoPro-A-ParamSet', ), + '1.2.643.2.2.35.2': ('id-GostR3410-2001-CryptoPro-B-ParamSet', ), + '1.2.643.2.2.35.3': ('id-GostR3410-2001-CryptoPro-C-ParamSet', ), + '1.2.643.2.2.36.0': ('id-GostR3410-2001-CryptoPro-XchA-ParamSet', ), + '1.2.643.2.2.36.1': ('id-GostR3410-2001-CryptoPro-XchB-ParamSet', ), + '1.2.643.2.2.98': ('GOST R 34.10-2001 DH', 'id-GostR3410-2001DH'), + '1.2.643.2.2.99': ('GOST R 34.10-94 DH', 'id-GostR3410-94DH'), + '1.2.643.2.9': ('cryptocom', ), + '1.2.643.2.9.1.3.3': ('GOST R 34.11-94 with GOST R 34.10-94 Cryptocom', 'id-GostR3411-94-with-GostR3410-94-cc'), + '1.2.643.2.9.1.3.4': ('GOST R 34.11-94 with GOST R 34.10-2001 Cryptocom', 'id-GostR3411-94-with-GostR3410-2001-cc'), + '1.2.643.2.9.1.5.3': ('GOST 34.10-94 Cryptocom', 'gost94cc'), + '1.2.643.2.9.1.5.4': ('GOST 34.10-2001 Cryptocom', 'gost2001cc'), + '1.2.643.2.9.1.6.1': ('GOST 28147-89 Cryptocom ParamSet', 'id-Gost28147-89-cc'), + '1.2.643.2.9.1.8.1': ('GOST R 3410-2001 Parameter Set Cryptocom', 'id-GostR3410-2001-ParamSet-cc'), + '1.2.643.3.131.1.1': ('INN', 'INN'), + '1.2.643.7.1': ('id-tc26', ), + '1.2.643.7.1.1': ('id-tc26-algorithms', ), + '1.2.643.7.1.1.1': ('id-tc26-sign', ), + '1.2.643.7.1.1.1.1': ('GOST R 34.10-2012 with 256 bit modulus', 'gost2012_256'), + '1.2.643.7.1.1.1.2': ('GOST R 34.10-2012 with 512 bit modulus', 'gost2012_512'), + '1.2.643.7.1.1.2': ('id-tc26-digest', ), + '1.2.643.7.1.1.2.2': ('GOST R 34.11-2012 with 256 bit hash', 'md_gost12_256'), + '1.2.643.7.1.1.2.3': ('GOST R 34.11-2012 with 512 bit hash', 'md_gost12_512'), + '1.2.643.7.1.1.3': ('id-tc26-signwithdigest', ), + '1.2.643.7.1.1.3.2': ('GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit)', 'id-tc26-signwithdigest-gost3410-2012-256'), + '1.2.643.7.1.1.3.3': ('GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit)', 'id-tc26-signwithdigest-gost3410-2012-512'), + '1.2.643.7.1.1.4': ('id-tc26-mac', ), + '1.2.643.7.1.1.4.1': ('HMAC GOST 34.11-2012 256 bit', 'id-tc26-hmac-gost-3411-2012-256'), + '1.2.643.7.1.1.4.2': ('HMAC GOST 34.11-2012 512 bit', 'id-tc26-hmac-gost-3411-2012-512'), + '1.2.643.7.1.1.5': ('id-tc26-cipher', ), + '1.2.643.7.1.1.5.1': ('id-tc26-cipher-gostr3412-2015-magma', ), + '1.2.643.7.1.1.5.1.1': ('id-tc26-cipher-gostr3412-2015-magma-ctracpkm', ), + '1.2.643.7.1.1.5.1.2': ('id-tc26-cipher-gostr3412-2015-magma-ctracpkm-omac', ), + '1.2.643.7.1.1.5.2': ('id-tc26-cipher-gostr3412-2015-kuznyechik', ), + '1.2.643.7.1.1.5.2.1': ('id-tc26-cipher-gostr3412-2015-kuznyechik-ctracpkm', ), + '1.2.643.7.1.1.5.2.2': ('id-tc26-cipher-gostr3412-2015-kuznyechik-ctracpkm-omac', ), + '1.2.643.7.1.1.6': ('id-tc26-agreement', ), + '1.2.643.7.1.1.6.1': ('id-tc26-agreement-gost-3410-2012-256', ), + '1.2.643.7.1.1.6.2': ('id-tc26-agreement-gost-3410-2012-512', ), + '1.2.643.7.1.1.7': ('id-tc26-wrap', ), + '1.2.643.7.1.1.7.1': ('id-tc26-wrap-gostr3412-2015-magma', ), + '1.2.643.7.1.1.7.1.1': ('id-tc26-wrap-gostr3412-2015-magma-kexp15', 'id-tc26-wrap-gostr3412-2015-kuznyechik-kexp15'), + '1.2.643.7.1.1.7.2': ('id-tc26-wrap-gostr3412-2015-kuznyechik', ), + '1.2.643.7.1.2': ('id-tc26-constants', ), + '1.2.643.7.1.2.1': ('id-tc26-sign-constants', ), + '1.2.643.7.1.2.1.1': ('id-tc26-gost-3410-2012-256-constants', ), + '1.2.643.7.1.2.1.1.1': ('GOST R 34.10-2012 (256 bit) ParamSet A', 'id-tc26-gost-3410-2012-256-paramSetA'), + '1.2.643.7.1.2.1.1.2': ('GOST R 34.10-2012 (256 bit) ParamSet B', 'id-tc26-gost-3410-2012-256-paramSetB'), + '1.2.643.7.1.2.1.1.3': ('GOST R 34.10-2012 (256 bit) ParamSet C', 'id-tc26-gost-3410-2012-256-paramSetC'), + '1.2.643.7.1.2.1.1.4': ('GOST R 34.10-2012 (256 bit) ParamSet D', 'id-tc26-gost-3410-2012-256-paramSetD'), + '1.2.643.7.1.2.1.2': ('id-tc26-gost-3410-2012-512-constants', ), + '1.2.643.7.1.2.1.2.0': ('GOST R 34.10-2012 (512 bit) testing parameter set', 'id-tc26-gost-3410-2012-512-paramSetTest'), + '1.2.643.7.1.2.1.2.1': ('GOST R 34.10-2012 (512 bit) ParamSet A', 'id-tc26-gost-3410-2012-512-paramSetA'), + '1.2.643.7.1.2.1.2.2': ('GOST R 34.10-2012 (512 bit) ParamSet B', 'id-tc26-gost-3410-2012-512-paramSetB'), + '1.2.643.7.1.2.1.2.3': ('GOST R 34.10-2012 (512 bit) ParamSet C', 'id-tc26-gost-3410-2012-512-paramSetC'), + '1.2.643.7.1.2.2': ('id-tc26-digest-constants', ), + '1.2.643.7.1.2.5': ('id-tc26-cipher-constants', ), + '1.2.643.7.1.2.5.1': ('id-tc26-gost-28147-constants', ), + '1.2.643.7.1.2.5.1.1': ('GOST 28147-89 TC26 parameter set', 'id-tc26-gost-28147-param-Z'), + '1.2.643.100.1': ('OGRN', 'OGRN'), + '1.2.643.100.3': ('SNILS', 'SNILS'), + '1.2.643.100.111': ('Signing Tool of Subject', 'subjectSignTool'), + '1.2.643.100.112': ('Signing Tool of Issuer', 'issuerSignTool'), + '1.2.804': ('ISO-UA', ), + '1.2.804.2.1.1.1': ('ua-pki', ), + '1.2.804.2.1.1.1.1.1.1': ('DSTU Gost 28147-2009', 'dstu28147'), + '1.2.804.2.1.1.1.1.1.1.2': ('DSTU Gost 28147-2009 OFB mode', 'dstu28147-ofb'), + '1.2.804.2.1.1.1.1.1.1.3': ('DSTU Gost 28147-2009 CFB mode', 'dstu28147-cfb'), + '1.2.804.2.1.1.1.1.1.1.5': ('DSTU Gost 28147-2009 key wrap', 'dstu28147-wrap'), + '1.2.804.2.1.1.1.1.1.2': ('HMAC DSTU Gost 34311-95', 'hmacWithDstu34311'), + '1.2.804.2.1.1.1.1.2.1': ('DSTU Gost 34311-95', 'dstu34311'), + '1.2.804.2.1.1.1.1.3.1.1': ('DSTU 4145-2002 little endian', 'dstu4145le'), + '1.2.804.2.1.1.1.1.3.1.1.1.1': ('DSTU 4145-2002 big endian', 'dstu4145be'), + '1.2.804.2.1.1.1.1.3.1.1.2.0': ('DSTU curve 0', 'uacurve0'), + '1.2.804.2.1.1.1.1.3.1.1.2.1': ('DSTU curve 1', 'uacurve1'), + '1.2.804.2.1.1.1.1.3.1.1.2.2': ('DSTU curve 2', 'uacurve2'), + '1.2.804.2.1.1.1.1.3.1.1.2.3': ('DSTU curve 3', 'uacurve3'), + '1.2.804.2.1.1.1.1.3.1.1.2.4': ('DSTU curve 4', 'uacurve4'), + '1.2.804.2.1.1.1.1.3.1.1.2.5': ('DSTU curve 5', 'uacurve5'), + '1.2.804.2.1.1.1.1.3.1.1.2.6': ('DSTU curve 6', 'uacurve6'), + '1.2.804.2.1.1.1.1.3.1.1.2.7': ('DSTU curve 7', 'uacurve7'), + '1.2.804.2.1.1.1.1.3.1.1.2.8': ('DSTU curve 8', 'uacurve8'), + '1.2.804.2.1.1.1.1.3.1.1.2.9': ('DSTU curve 9', 'uacurve9'), + '1.2.840': ('ISO US Member Body', 'ISO-US'), + '1.2.840.10040': ('X9.57', 'X9-57'), + '1.2.840.10040.2': ('holdInstruction', ), + '1.2.840.10040.2.1': ('Hold Instruction None', 'holdInstructionNone'), + '1.2.840.10040.2.2': ('Hold Instruction Call Issuer', 'holdInstructionCallIssuer'), + '1.2.840.10040.2.3': ('Hold Instruction Reject', 'holdInstructionReject'), + '1.2.840.10040.4': ('X9.57 CM ?', 'X9cm'), + '1.2.840.10040.4.1': ('dsaEncryption', 'DSA'), + '1.2.840.10040.4.3': ('dsaWithSHA1', 'DSA-SHA1'), + '1.2.840.10045': ('ANSI X9.62', 'ansi-X9-62'), + '1.2.840.10045.1': ('id-fieldType', ), + '1.2.840.10045.1.1': ('prime-field', ), + '1.2.840.10045.1.2': ('characteristic-two-field', ), + '1.2.840.10045.1.2.3': ('id-characteristic-two-basis', ), + '1.2.840.10045.1.2.3.1': ('onBasis', ), + '1.2.840.10045.1.2.3.2': ('tpBasis', ), + '1.2.840.10045.1.2.3.3': ('ppBasis', ), + '1.2.840.10045.2': ('id-publicKeyType', ), + '1.2.840.10045.2.1': ('id-ecPublicKey', ), + '1.2.840.10045.3': ('ellipticCurve', ), + '1.2.840.10045.3.0': ('c-TwoCurve', ), + '1.2.840.10045.3.0.1': ('c2pnb163v1', ), + '1.2.840.10045.3.0.2': ('c2pnb163v2', ), + '1.2.840.10045.3.0.3': ('c2pnb163v3', ), + '1.2.840.10045.3.0.4': ('c2pnb176v1', ), + '1.2.840.10045.3.0.5': ('c2tnb191v1', ), + '1.2.840.10045.3.0.6': ('c2tnb191v2', ), + '1.2.840.10045.3.0.7': ('c2tnb191v3', ), + '1.2.840.10045.3.0.8': ('c2onb191v4', ), + '1.2.840.10045.3.0.9': ('c2onb191v5', ), + '1.2.840.10045.3.0.10': ('c2pnb208w1', ), + '1.2.840.10045.3.0.11': ('c2tnb239v1', ), + '1.2.840.10045.3.0.12': ('c2tnb239v2', ), + '1.2.840.10045.3.0.13': ('c2tnb239v3', ), + '1.2.840.10045.3.0.14': ('c2onb239v4', ), + '1.2.840.10045.3.0.15': ('c2onb239v5', ), + '1.2.840.10045.3.0.16': ('c2pnb272w1', ), + '1.2.840.10045.3.0.17': ('c2pnb304w1', ), + '1.2.840.10045.3.0.18': ('c2tnb359v1', ), + '1.2.840.10045.3.0.19': ('c2pnb368w1', ), + '1.2.840.10045.3.0.20': ('c2tnb431r1', ), + '1.2.840.10045.3.1': ('primeCurve', ), + '1.2.840.10045.3.1.1': ('prime192v1', ), + '1.2.840.10045.3.1.2': ('prime192v2', ), + '1.2.840.10045.3.1.3': ('prime192v3', ), + '1.2.840.10045.3.1.4': ('prime239v1', ), + '1.2.840.10045.3.1.5': ('prime239v2', ), + '1.2.840.10045.3.1.6': ('prime239v3', ), + '1.2.840.10045.3.1.7': ('prime256v1', ), + '1.2.840.10045.4': ('id-ecSigType', ), + '1.2.840.10045.4.1': ('ecdsa-with-SHA1', ), + '1.2.840.10045.4.2': ('ecdsa-with-Recommended', ), + '1.2.840.10045.4.3': ('ecdsa-with-Specified', ), + '1.2.840.10045.4.3.1': ('ecdsa-with-SHA224', ), + '1.2.840.10045.4.3.2': ('ecdsa-with-SHA256', ), + '1.2.840.10045.4.3.3': ('ecdsa-with-SHA384', ), + '1.2.840.10045.4.3.4': ('ecdsa-with-SHA512', ), + '1.2.840.10046.2.1': ('X9.42 DH', 'dhpublicnumber'), + '1.2.840.113533.7.66.10': ('cast5-cbc', 'CAST5-CBC'), + '1.2.840.113533.7.66.12': ('pbeWithMD5AndCast5CBC', ), + '1.2.840.113533.7.66.13': ('password based MAC', 'id-PasswordBasedMAC'), + '1.2.840.113533.7.66.30': ('Diffie-Hellman based MAC', 'id-DHBasedMac'), + '1.2.840.113549': ('RSA Data Security, Inc.', 'rsadsi'), + '1.2.840.113549.1': ('RSA Data Security, Inc. PKCS', 'pkcs'), + '1.2.840.113549.1.1': ('pkcs1', ), + '1.2.840.113549.1.1.1': ('rsaEncryption', ), + '1.2.840.113549.1.1.2': ('md2WithRSAEncryption', 'RSA-MD2'), + '1.2.840.113549.1.1.3': ('md4WithRSAEncryption', 'RSA-MD4'), + '1.2.840.113549.1.1.4': ('md5WithRSAEncryption', 'RSA-MD5'), + '1.2.840.113549.1.1.5': ('sha1WithRSAEncryption', 'RSA-SHA1'), + '1.2.840.113549.1.1.6': ('rsaOAEPEncryptionSET', ), + '1.2.840.113549.1.1.7': ('rsaesOaep', 'RSAES-OAEP'), + '1.2.840.113549.1.1.8': ('mgf1', 'MGF1'), + '1.2.840.113549.1.1.9': ('pSpecified', 'PSPECIFIED'), + '1.2.840.113549.1.1.10': ('rsassaPss', 'RSASSA-PSS'), + '1.2.840.113549.1.1.11': ('sha256WithRSAEncryption', 'RSA-SHA256'), + '1.2.840.113549.1.1.12': ('sha384WithRSAEncryption', 'RSA-SHA384'), + '1.2.840.113549.1.1.13': ('sha512WithRSAEncryption', 'RSA-SHA512'), + '1.2.840.113549.1.1.14': ('sha224WithRSAEncryption', 'RSA-SHA224'), + '1.2.840.113549.1.1.15': ('sha512-224WithRSAEncryption', 'RSA-SHA512/224'), + '1.2.840.113549.1.1.16': ('sha512-256WithRSAEncryption', 'RSA-SHA512/256'), + '1.2.840.113549.1.3': ('pkcs3', ), + '1.2.840.113549.1.3.1': ('dhKeyAgreement', ), + '1.2.840.113549.1.5': ('pkcs5', ), + '1.2.840.113549.1.5.1': ('pbeWithMD2AndDES-CBC', 'PBE-MD2-DES'), + '1.2.840.113549.1.5.3': ('pbeWithMD5AndDES-CBC', 'PBE-MD5-DES'), + '1.2.840.113549.1.5.4': ('pbeWithMD2AndRC2-CBC', 'PBE-MD2-RC2-64'), + '1.2.840.113549.1.5.6': ('pbeWithMD5AndRC2-CBC', 'PBE-MD5-RC2-64'), + '1.2.840.113549.1.5.10': ('pbeWithSHA1AndDES-CBC', 'PBE-SHA1-DES'), + '1.2.840.113549.1.5.11': ('pbeWithSHA1AndRC2-CBC', 'PBE-SHA1-RC2-64'), + '1.2.840.113549.1.5.12': ('PBKDF2', ), + '1.2.840.113549.1.5.13': ('PBES2', ), + '1.2.840.113549.1.5.14': ('PBMAC1', ), + '1.2.840.113549.1.7': ('pkcs7', ), + '1.2.840.113549.1.7.1': ('pkcs7-data', ), + '1.2.840.113549.1.7.2': ('pkcs7-signedData', ), + '1.2.840.113549.1.7.3': ('pkcs7-envelopedData', ), + '1.2.840.113549.1.7.4': ('pkcs7-signedAndEnvelopedData', ), + '1.2.840.113549.1.7.5': ('pkcs7-digestData', ), + '1.2.840.113549.1.7.6': ('pkcs7-encryptedData', ), + '1.2.840.113549.1.9': ('pkcs9', ), + '1.2.840.113549.1.9.1': ('emailAddress', ), + '1.2.840.113549.1.9.2': ('unstructuredName', ), + '1.2.840.113549.1.9.3': ('contentType', ), + '1.2.840.113549.1.9.4': ('messageDigest', ), + '1.2.840.113549.1.9.5': ('signingTime', ), + '1.2.840.113549.1.9.6': ('countersignature', ), + '1.2.840.113549.1.9.7': ('challengePassword', ), + '1.2.840.113549.1.9.8': ('unstructuredAddress', ), + '1.2.840.113549.1.9.9': ('extendedCertificateAttributes', ), + '1.2.840.113549.1.9.14': ('Extension Request', 'extReq'), + '1.2.840.113549.1.9.15': ('S/MIME Capabilities', 'SMIME-CAPS'), + '1.2.840.113549.1.9.16': ('S/MIME', 'SMIME'), + '1.2.840.113549.1.9.16.0': ('id-smime-mod', ), + '1.2.840.113549.1.9.16.0.1': ('id-smime-mod-cms', ), + '1.2.840.113549.1.9.16.0.2': ('id-smime-mod-ess', ), + '1.2.840.113549.1.9.16.0.3': ('id-smime-mod-oid', ), + '1.2.840.113549.1.9.16.0.4': ('id-smime-mod-msg-v3', ), + '1.2.840.113549.1.9.16.0.5': ('id-smime-mod-ets-eSignature-88', ), + '1.2.840.113549.1.9.16.0.6': ('id-smime-mod-ets-eSignature-97', ), + '1.2.840.113549.1.9.16.0.7': ('id-smime-mod-ets-eSigPolicy-88', ), + '1.2.840.113549.1.9.16.0.8': ('id-smime-mod-ets-eSigPolicy-97', ), + '1.2.840.113549.1.9.16.1': ('id-smime-ct', ), + '1.2.840.113549.1.9.16.1.1': ('id-smime-ct-receipt', ), + '1.2.840.113549.1.9.16.1.2': ('id-smime-ct-authData', ), + '1.2.840.113549.1.9.16.1.3': ('id-smime-ct-publishCert', ), + '1.2.840.113549.1.9.16.1.4': ('id-smime-ct-TSTInfo', ), + '1.2.840.113549.1.9.16.1.5': ('id-smime-ct-TDTInfo', ), + '1.2.840.113549.1.9.16.1.6': ('id-smime-ct-contentInfo', ), + '1.2.840.113549.1.9.16.1.7': ('id-smime-ct-DVCSRequestData', ), + '1.2.840.113549.1.9.16.1.8': ('id-smime-ct-DVCSResponseData', ), + '1.2.840.113549.1.9.16.1.9': ('id-smime-ct-compressedData', ), + '1.2.840.113549.1.9.16.1.19': ('id-smime-ct-contentCollection', ), + '1.2.840.113549.1.9.16.1.23': ('id-smime-ct-authEnvelopedData', ), + '1.2.840.113549.1.9.16.1.27': ('id-ct-asciiTextWithCRLF', ), + '1.2.840.113549.1.9.16.1.28': ('id-ct-xml', ), + '1.2.840.113549.1.9.16.2': ('id-smime-aa', ), + '1.2.840.113549.1.9.16.2.1': ('id-smime-aa-receiptRequest', ), + '1.2.840.113549.1.9.16.2.2': ('id-smime-aa-securityLabel', ), + '1.2.840.113549.1.9.16.2.3': ('id-smime-aa-mlExpandHistory', ), + '1.2.840.113549.1.9.16.2.4': ('id-smime-aa-contentHint', ), + '1.2.840.113549.1.9.16.2.5': ('id-smime-aa-msgSigDigest', ), + '1.2.840.113549.1.9.16.2.6': ('id-smime-aa-encapContentType', ), + '1.2.840.113549.1.9.16.2.7': ('id-smime-aa-contentIdentifier', ), + '1.2.840.113549.1.9.16.2.8': ('id-smime-aa-macValue', ), + '1.2.840.113549.1.9.16.2.9': ('id-smime-aa-equivalentLabels', ), + '1.2.840.113549.1.9.16.2.10': ('id-smime-aa-contentReference', ), + '1.2.840.113549.1.9.16.2.11': ('id-smime-aa-encrypKeyPref', ), + '1.2.840.113549.1.9.16.2.12': ('id-smime-aa-signingCertificate', ), + '1.2.840.113549.1.9.16.2.13': ('id-smime-aa-smimeEncryptCerts', ), + '1.2.840.113549.1.9.16.2.14': ('id-smime-aa-timeStampToken', ), + '1.2.840.113549.1.9.16.2.15': ('id-smime-aa-ets-sigPolicyId', ), + '1.2.840.113549.1.9.16.2.16': ('id-smime-aa-ets-commitmentType', ), + '1.2.840.113549.1.9.16.2.17': ('id-smime-aa-ets-signerLocation', ), + '1.2.840.113549.1.9.16.2.18': ('id-smime-aa-ets-signerAttr', ), + '1.2.840.113549.1.9.16.2.19': ('id-smime-aa-ets-otherSigCert', ), + '1.2.840.113549.1.9.16.2.20': ('id-smime-aa-ets-contentTimestamp', ), + '1.2.840.113549.1.9.16.2.21': ('id-smime-aa-ets-CertificateRefs', ), + '1.2.840.113549.1.9.16.2.22': ('id-smime-aa-ets-RevocationRefs', ), + '1.2.840.113549.1.9.16.2.23': ('id-smime-aa-ets-certValues', ), + '1.2.840.113549.1.9.16.2.24': ('id-smime-aa-ets-revocationValues', ), + '1.2.840.113549.1.9.16.2.25': ('id-smime-aa-ets-escTimeStamp', ), + '1.2.840.113549.1.9.16.2.26': ('id-smime-aa-ets-certCRLTimestamp', ), + '1.2.840.113549.1.9.16.2.27': ('id-smime-aa-ets-archiveTimeStamp', ), + '1.2.840.113549.1.9.16.2.28': ('id-smime-aa-signatureType', ), + '1.2.840.113549.1.9.16.2.29': ('id-smime-aa-dvcs-dvc', ), + '1.2.840.113549.1.9.16.2.47': ('id-smime-aa-signingCertificateV2', ), + '1.2.840.113549.1.9.16.3': ('id-smime-alg', ), + '1.2.840.113549.1.9.16.3.1': ('id-smime-alg-ESDHwith3DES', ), + '1.2.840.113549.1.9.16.3.2': ('id-smime-alg-ESDHwithRC2', ), + '1.2.840.113549.1.9.16.3.3': ('id-smime-alg-3DESwrap', ), + '1.2.840.113549.1.9.16.3.4': ('id-smime-alg-RC2wrap', ), + '1.2.840.113549.1.9.16.3.5': ('id-smime-alg-ESDH', ), + '1.2.840.113549.1.9.16.3.6': ('id-smime-alg-CMS3DESwrap', ), + '1.2.840.113549.1.9.16.3.7': ('id-smime-alg-CMSRC2wrap', ), + '1.2.840.113549.1.9.16.3.8': ('zlib compression', 'ZLIB'), + '1.2.840.113549.1.9.16.3.9': ('id-alg-PWRI-KEK', ), + '1.2.840.113549.1.9.16.4': ('id-smime-cd', ), + '1.2.840.113549.1.9.16.4.1': ('id-smime-cd-ldap', ), + '1.2.840.113549.1.9.16.5': ('id-smime-spq', ), + '1.2.840.113549.1.9.16.5.1': ('id-smime-spq-ets-sqt-uri', ), + '1.2.840.113549.1.9.16.5.2': ('id-smime-spq-ets-sqt-unotice', ), + '1.2.840.113549.1.9.16.6': ('id-smime-cti', ), + '1.2.840.113549.1.9.16.6.1': ('id-smime-cti-ets-proofOfOrigin', ), + '1.2.840.113549.1.9.16.6.2': ('id-smime-cti-ets-proofOfReceipt', ), + '1.2.840.113549.1.9.16.6.3': ('id-smime-cti-ets-proofOfDelivery', ), + '1.2.840.113549.1.9.16.6.4': ('id-smime-cti-ets-proofOfSender', ), + '1.2.840.113549.1.9.16.6.5': ('id-smime-cti-ets-proofOfApproval', ), + '1.2.840.113549.1.9.16.6.6': ('id-smime-cti-ets-proofOfCreation', ), + '1.2.840.113549.1.9.20': ('friendlyName', ), + '1.2.840.113549.1.9.21': ('localKeyID', ), + '1.2.840.113549.1.9.22': ('certTypes', ), + '1.2.840.113549.1.9.22.1': ('x509Certificate', ), + '1.2.840.113549.1.9.22.2': ('sdsiCertificate', ), + '1.2.840.113549.1.9.23': ('crlTypes', ), + '1.2.840.113549.1.9.23.1': ('x509Crl', ), + '1.2.840.113549.1.12': ('pkcs12', ), + '1.2.840.113549.1.12.1': ('pkcs12-pbeids', ), + '1.2.840.113549.1.12.1.1': ('pbeWithSHA1And128BitRC4', 'PBE-SHA1-RC4-128'), + '1.2.840.113549.1.12.1.2': ('pbeWithSHA1And40BitRC4', 'PBE-SHA1-RC4-40'), + '1.2.840.113549.1.12.1.3': ('pbeWithSHA1And3-KeyTripleDES-CBC', 'PBE-SHA1-3DES'), + '1.2.840.113549.1.12.1.4': ('pbeWithSHA1And2-KeyTripleDES-CBC', 'PBE-SHA1-2DES'), + '1.2.840.113549.1.12.1.5': ('pbeWithSHA1And128BitRC2-CBC', 'PBE-SHA1-RC2-128'), + '1.2.840.113549.1.12.1.6': ('pbeWithSHA1And40BitRC2-CBC', 'PBE-SHA1-RC2-40'), + '1.2.840.113549.1.12.10': ('pkcs12-Version1', ), + '1.2.840.113549.1.12.10.1': ('pkcs12-BagIds', ), + '1.2.840.113549.1.12.10.1.1': ('keyBag', ), + '1.2.840.113549.1.12.10.1.2': ('pkcs8ShroudedKeyBag', ), + '1.2.840.113549.1.12.10.1.3': ('certBag', ), + '1.2.840.113549.1.12.10.1.4': ('crlBag', ), + '1.2.840.113549.1.12.10.1.5': ('secretBag', ), + '1.2.840.113549.1.12.10.1.6': ('safeContentsBag', ), + '1.2.840.113549.2.2': ('md2', 'MD2'), + '1.2.840.113549.2.4': ('md4', 'MD4'), + '1.2.840.113549.2.5': ('md5', 'MD5'), + '1.2.840.113549.2.6': ('hmacWithMD5', ), + '1.2.840.113549.2.7': ('hmacWithSHA1', ), + '1.2.840.113549.2.8': ('hmacWithSHA224', ), + '1.2.840.113549.2.9': ('hmacWithSHA256', ), + '1.2.840.113549.2.10': ('hmacWithSHA384', ), + '1.2.840.113549.2.11': ('hmacWithSHA512', ), + '1.2.840.113549.2.12': ('hmacWithSHA512-224', ), + '1.2.840.113549.2.13': ('hmacWithSHA512-256', ), + '1.2.840.113549.3.2': ('rc2-cbc', 'RC2-CBC'), + '1.2.840.113549.3.4': ('rc4', 'RC4'), + '1.2.840.113549.3.7': ('des-ede3-cbc', 'DES-EDE3-CBC'), + '1.2.840.113549.3.8': ('rc5-cbc', 'RC5-CBC'), + '1.2.840.113549.3.10': ('des-cdmf', 'DES-CDMF'), + '1.3': ('identified-organization', 'org', 'ORG'), + '1.3.6': ('dod', 'DOD'), + '1.3.6.1': ('iana', 'IANA', 'internet'), + '1.3.6.1.1': ('Directory', 'directory'), + '1.3.6.1.2': ('Management', 'mgmt'), + '1.3.6.1.3': ('Experimental', 'experimental'), + '1.3.6.1.4': ('Private', 'private'), + '1.3.6.1.4.1': ('Enterprises', 'enterprises'), + '1.3.6.1.4.1.188.7.1.1.2': ('idea-cbc', 'IDEA-CBC'), + '1.3.6.1.4.1.311.2.1.14': ('Microsoft Extension Request', 'msExtReq'), + '1.3.6.1.4.1.311.2.1.21': ('Microsoft Individual Code Signing', 'msCodeInd'), + '1.3.6.1.4.1.311.2.1.22': ('Microsoft Commercial Code Signing', 'msCodeCom'), + '1.3.6.1.4.1.311.10.3.1': ('Microsoft Trust List Signing', 'msCTLSign'), + '1.3.6.1.4.1.311.10.3.3': ('Microsoft Server Gated Crypto', 'msSGC'), + '1.3.6.1.4.1.311.10.3.4': ('Microsoft Encrypted File System', 'msEFS'), + '1.3.6.1.4.1.311.17.1': ('Microsoft CSP Name', 'CSPName'), + '1.3.6.1.4.1.311.17.2': ('Microsoft Local Key set', 'LocalKeySet'), + '1.3.6.1.4.1.311.20.2.2': ('Microsoft Smartcardlogin', 'msSmartcardLogin'), + '1.3.6.1.4.1.311.20.2.3': ('Microsoft Universal Principal Name', 'msUPN'), + '1.3.6.1.4.1.311.60.2.1.1': ('jurisdictionLocalityName', 'jurisdictionL'), + '1.3.6.1.4.1.311.60.2.1.2': ('jurisdictionStateOrProvinceName', 'jurisdictionST'), + '1.3.6.1.4.1.311.60.2.1.3': ('jurisdictionCountryName', 'jurisdictionC'), + '1.3.6.1.4.1.1466.344': ('dcObject', 'dcobject'), + '1.3.6.1.4.1.1722.12.2.1.16': ('blake2b512', 'BLAKE2b512'), + '1.3.6.1.4.1.1722.12.2.2.8': ('blake2s256', 'BLAKE2s256'), + '1.3.6.1.4.1.3029.1.2': ('bf-cbc', 'BF-CBC'), + '1.3.6.1.4.1.11129.2.4.2': ('CT Precertificate SCTs', 'ct_precert_scts'), + '1.3.6.1.4.1.11129.2.4.3': ('CT Precertificate Poison', 'ct_precert_poison'), + '1.3.6.1.4.1.11129.2.4.4': ('CT Precertificate Signer', 'ct_precert_signer'), + '1.3.6.1.4.1.11129.2.4.5': ('CT Certificate SCTs', 'ct_cert_scts'), + '1.3.6.1.4.1.11591.4.11': ('scrypt', 'id-scrypt'), + '1.3.6.1.5': ('Security', 'security'), + '1.3.6.1.5.2.3': ('id-pkinit', ), + '1.3.6.1.5.2.3.4': ('PKINIT Client Auth', 'pkInitClientAuth'), + '1.3.6.1.5.2.3.5': ('Signing KDC Response', 'pkInitKDC'), + '1.3.6.1.5.5.7': ('PKIX', ), + '1.3.6.1.5.5.7.0': ('id-pkix-mod', ), + '1.3.6.1.5.5.7.0.1': ('id-pkix1-explicit-88', ), + '1.3.6.1.5.5.7.0.2': ('id-pkix1-implicit-88', ), + '1.3.6.1.5.5.7.0.3': ('id-pkix1-explicit-93', ), + '1.3.6.1.5.5.7.0.4': ('id-pkix1-implicit-93', ), + '1.3.6.1.5.5.7.0.5': ('id-mod-crmf', ), + '1.3.6.1.5.5.7.0.6': ('id-mod-cmc', ), + '1.3.6.1.5.5.7.0.7': ('id-mod-kea-profile-88', ), + '1.3.6.1.5.5.7.0.8': ('id-mod-kea-profile-93', ), + '1.3.6.1.5.5.7.0.9': ('id-mod-cmp', ), + '1.3.6.1.5.5.7.0.10': ('id-mod-qualified-cert-88', ), + '1.3.6.1.5.5.7.0.11': ('id-mod-qualified-cert-93', ), + '1.3.6.1.5.5.7.0.12': ('id-mod-attribute-cert', ), + '1.3.6.1.5.5.7.0.13': ('id-mod-timestamp-protocol', ), + '1.3.6.1.5.5.7.0.14': ('id-mod-ocsp', ), + '1.3.6.1.5.5.7.0.15': ('id-mod-dvcs', ), + '1.3.6.1.5.5.7.0.16': ('id-mod-cmp2000', ), + '1.3.6.1.5.5.7.1': ('id-pe', ), + '1.3.6.1.5.5.7.1.1': ('Authority Information Access', 'authorityInfoAccess'), + '1.3.6.1.5.5.7.1.2': ('Biometric Info', 'biometricInfo'), + '1.3.6.1.5.5.7.1.3': ('qcStatements', ), + '1.3.6.1.5.5.7.1.4': ('ac-auditEntity', ), + '1.3.6.1.5.5.7.1.5': ('ac-targeting', ), + '1.3.6.1.5.5.7.1.6': ('aaControls', ), + '1.3.6.1.5.5.7.1.7': ('sbgp-ipAddrBlock', ), + '1.3.6.1.5.5.7.1.8': ('sbgp-autonomousSysNum', ), + '1.3.6.1.5.5.7.1.9': ('sbgp-routerIdentifier', ), + '1.3.6.1.5.5.7.1.10': ('ac-proxying', ), + '1.3.6.1.5.5.7.1.11': ('Subject Information Access', 'subjectInfoAccess'), + '1.3.6.1.5.5.7.1.14': ('Proxy Certificate Information', 'proxyCertInfo'), + '1.3.6.1.5.5.7.1.24': ('TLS Feature', 'tlsfeature'), + '1.3.6.1.5.5.7.2': ('id-qt', ), + '1.3.6.1.5.5.7.2.1': ('Policy Qualifier CPS', 'id-qt-cps'), + '1.3.6.1.5.5.7.2.2': ('Policy Qualifier User Notice', 'id-qt-unotice'), + '1.3.6.1.5.5.7.2.3': ('textNotice', ), + '1.3.6.1.5.5.7.3': ('id-kp', ), + '1.3.6.1.5.5.7.3.1': ('TLS Web Server Authentication', 'serverAuth'), + '1.3.6.1.5.5.7.3.2': ('TLS Web Client Authentication', 'clientAuth'), + '1.3.6.1.5.5.7.3.3': ('Code Signing', 'codeSigning'), + '1.3.6.1.5.5.7.3.4': ('E-mail Protection', 'emailProtection'), + '1.3.6.1.5.5.7.3.5': ('IPSec End System', 'ipsecEndSystem'), + '1.3.6.1.5.5.7.3.6': ('IPSec Tunnel', 'ipsecTunnel'), + '1.3.6.1.5.5.7.3.7': ('IPSec User', 'ipsecUser'), + '1.3.6.1.5.5.7.3.8': ('Time Stamping', 'timeStamping'), + '1.3.6.1.5.5.7.3.9': ('OCSP Signing', 'OCSPSigning'), + '1.3.6.1.5.5.7.3.10': ('dvcs', 'DVCS'), + '1.3.6.1.5.5.7.3.17': ('ipsec Internet Key Exchange', 'ipsecIKE'), + '1.3.6.1.5.5.7.3.18': ('Ctrl/provision WAP Access', 'capwapAC'), + '1.3.6.1.5.5.7.3.19': ('Ctrl/Provision WAP Termination', 'capwapWTP'), + '1.3.6.1.5.5.7.3.21': ('SSH Client', 'secureShellClient'), + '1.3.6.1.5.5.7.3.22': ('SSH Server', 'secureShellServer'), + '1.3.6.1.5.5.7.3.23': ('Send Router', 'sendRouter'), + '1.3.6.1.5.5.7.3.24': ('Send Proxied Router', 'sendProxiedRouter'), + '1.3.6.1.5.5.7.3.25': ('Send Owner', 'sendOwner'), + '1.3.6.1.5.5.7.3.26': ('Send Proxied Owner', 'sendProxiedOwner'), + '1.3.6.1.5.5.7.3.27': ('CMC Certificate Authority', 'cmcCA'), + '1.3.6.1.5.5.7.3.28': ('CMC Registration Authority', 'cmcRA'), + '1.3.6.1.5.5.7.4': ('id-it', ), + '1.3.6.1.5.5.7.4.1': ('id-it-caProtEncCert', ), + '1.3.6.1.5.5.7.4.2': ('id-it-signKeyPairTypes', ), + '1.3.6.1.5.5.7.4.3': ('id-it-encKeyPairTypes', ), + '1.3.6.1.5.5.7.4.4': ('id-it-preferredSymmAlg', ), + '1.3.6.1.5.5.7.4.5': ('id-it-caKeyUpdateInfo', ), + '1.3.6.1.5.5.7.4.6': ('id-it-currentCRL', ), + '1.3.6.1.5.5.7.4.7': ('id-it-unsupportedOIDs', ), + '1.3.6.1.5.5.7.4.8': ('id-it-subscriptionRequest', ), + '1.3.6.1.5.5.7.4.9': ('id-it-subscriptionResponse', ), + '1.3.6.1.5.5.7.4.10': ('id-it-keyPairParamReq', ), + '1.3.6.1.5.5.7.4.11': ('id-it-keyPairParamRep', ), + '1.3.6.1.5.5.7.4.12': ('id-it-revPassphrase', ), + '1.3.6.1.5.5.7.4.13': ('id-it-implicitConfirm', ), + '1.3.6.1.5.5.7.4.14': ('id-it-confirmWaitTime', ), + '1.3.6.1.5.5.7.4.15': ('id-it-origPKIMessage', ), + '1.3.6.1.5.5.7.4.16': ('id-it-suppLangTags', ), + '1.3.6.1.5.5.7.5': ('id-pkip', ), + '1.3.6.1.5.5.7.5.1': ('id-regCtrl', ), + '1.3.6.1.5.5.7.5.1.1': ('id-regCtrl-regToken', ), + '1.3.6.1.5.5.7.5.1.2': ('id-regCtrl-authenticator', ), + '1.3.6.1.5.5.7.5.1.3': ('id-regCtrl-pkiPublicationInfo', ), + '1.3.6.1.5.5.7.5.1.4': ('id-regCtrl-pkiArchiveOptions', ), + '1.3.6.1.5.5.7.5.1.5': ('id-regCtrl-oldCertID', ), + '1.3.6.1.5.5.7.5.1.6': ('id-regCtrl-protocolEncrKey', ), + '1.3.6.1.5.5.7.5.2': ('id-regInfo', ), + '1.3.6.1.5.5.7.5.2.1': ('id-regInfo-utf8Pairs', ), + '1.3.6.1.5.5.7.5.2.2': ('id-regInfo-certReq', ), + '1.3.6.1.5.5.7.6': ('id-alg', ), + '1.3.6.1.5.5.7.6.1': ('id-alg-des40', ), + '1.3.6.1.5.5.7.6.2': ('id-alg-noSignature', ), + '1.3.6.1.5.5.7.6.3': ('id-alg-dh-sig-hmac-sha1', ), + '1.3.6.1.5.5.7.6.4': ('id-alg-dh-pop', ), + '1.3.6.1.5.5.7.7': ('id-cmc', ), + '1.3.6.1.5.5.7.7.1': ('id-cmc-statusInfo', ), + '1.3.6.1.5.5.7.7.2': ('id-cmc-identification', ), + '1.3.6.1.5.5.7.7.3': ('id-cmc-identityProof', ), + '1.3.6.1.5.5.7.7.4': ('id-cmc-dataReturn', ), + '1.3.6.1.5.5.7.7.5': ('id-cmc-transactionId', ), + '1.3.6.1.5.5.7.7.6': ('id-cmc-senderNonce', ), + '1.3.6.1.5.5.7.7.7': ('id-cmc-recipientNonce', ), + '1.3.6.1.5.5.7.7.8': ('id-cmc-addExtensions', ), + '1.3.6.1.5.5.7.7.9': ('id-cmc-encryptedPOP', ), + '1.3.6.1.5.5.7.7.10': ('id-cmc-decryptedPOP', ), + '1.3.6.1.5.5.7.7.11': ('id-cmc-lraPOPWitness', ), + '1.3.6.1.5.5.7.7.15': ('id-cmc-getCert', ), + '1.3.6.1.5.5.7.7.16': ('id-cmc-getCRL', ), + '1.3.6.1.5.5.7.7.17': ('id-cmc-revokeRequest', ), + '1.3.6.1.5.5.7.7.18': ('id-cmc-regInfo', ), + '1.3.6.1.5.5.7.7.19': ('id-cmc-responseInfo', ), + '1.3.6.1.5.5.7.7.21': ('id-cmc-queryPending', ), + '1.3.6.1.5.5.7.7.22': ('id-cmc-popLinkRandom', ), + '1.3.6.1.5.5.7.7.23': ('id-cmc-popLinkWitness', ), + '1.3.6.1.5.5.7.7.24': ('id-cmc-confirmCertAcceptance', ), + '1.3.6.1.5.5.7.8': ('id-on', ), + '1.3.6.1.5.5.7.8.1': ('id-on-personalData', ), + '1.3.6.1.5.5.7.8.3': ('Permanent Identifier', 'id-on-permanentIdentifier'), + '1.3.6.1.5.5.7.9': ('id-pda', ), + '1.3.6.1.5.5.7.9.1': ('id-pda-dateOfBirth', ), + '1.3.6.1.5.5.7.9.2': ('id-pda-placeOfBirth', ), + '1.3.6.1.5.5.7.9.3': ('id-pda-gender', ), + '1.3.6.1.5.5.7.9.4': ('id-pda-countryOfCitizenship', ), + '1.3.6.1.5.5.7.9.5': ('id-pda-countryOfResidence', ), + '1.3.6.1.5.5.7.10': ('id-aca', ), + '1.3.6.1.5.5.7.10.1': ('id-aca-authenticationInfo', ), + '1.3.6.1.5.5.7.10.2': ('id-aca-accessIdentity', ), + '1.3.6.1.5.5.7.10.3': ('id-aca-chargingIdentity', ), + '1.3.6.1.5.5.7.10.4': ('id-aca-group', ), + '1.3.6.1.5.5.7.10.5': ('id-aca-role', ), + '1.3.6.1.5.5.7.10.6': ('id-aca-encAttrs', ), + '1.3.6.1.5.5.7.11': ('id-qcs', ), + '1.3.6.1.5.5.7.11.1': ('id-qcs-pkixQCSyntax-v1', ), + '1.3.6.1.5.5.7.12': ('id-cct', ), + '1.3.6.1.5.5.7.12.1': ('id-cct-crs', ), + '1.3.6.1.5.5.7.12.2': ('id-cct-PKIData', ), + '1.3.6.1.5.5.7.12.3': ('id-cct-PKIResponse', ), + '1.3.6.1.5.5.7.21': ('id-ppl', ), + '1.3.6.1.5.5.7.21.0': ('Any language', 'id-ppl-anyLanguage'), + '1.3.6.1.5.5.7.21.1': ('Inherit all', 'id-ppl-inheritAll'), + '1.3.6.1.5.5.7.21.2': ('Independent', 'id-ppl-independent'), + '1.3.6.1.5.5.7.48': ('id-ad', ), + '1.3.6.1.5.5.7.48.1': ('OCSP', 'OCSP', 'id-pkix-OCSP'), + '1.3.6.1.5.5.7.48.1.1': ('Basic OCSP Response', 'basicOCSPResponse'), + '1.3.6.1.5.5.7.48.1.2': ('OCSP Nonce', 'Nonce'), + '1.3.6.1.5.5.7.48.1.3': ('OCSP CRL ID', 'CrlID'), + '1.3.6.1.5.5.7.48.1.4': ('Acceptable OCSP Responses', 'acceptableResponses'), + '1.3.6.1.5.5.7.48.1.5': ('OCSP No Check', 'noCheck'), + '1.3.6.1.5.5.7.48.1.6': ('OCSP Archive Cutoff', 'archiveCutoff'), + '1.3.6.1.5.5.7.48.1.7': ('OCSP Service Locator', 'serviceLocator'), + '1.3.6.1.5.5.7.48.1.8': ('Extended OCSP Status', 'extendedStatus'), + '1.3.6.1.5.5.7.48.1.9': ('valid', ), + '1.3.6.1.5.5.7.48.1.10': ('path', ), + '1.3.6.1.5.5.7.48.1.11': ('Trust Root', 'trustRoot'), + '1.3.6.1.5.5.7.48.2': ('CA Issuers', 'caIssuers'), + '1.3.6.1.5.5.7.48.3': ('AD Time Stamping', 'ad_timestamping'), + '1.3.6.1.5.5.7.48.4': ('ad dvcs', 'AD_DVCS'), + '1.3.6.1.5.5.7.48.5': ('CA Repository', 'caRepository'), + '1.3.6.1.5.5.8.1.1': ('hmac-md5', 'HMAC-MD5'), + '1.3.6.1.5.5.8.1.2': ('hmac-sha1', 'HMAC-SHA1'), + '1.3.6.1.6': ('SNMPv2', 'snmpv2'), + '1.3.6.1.7': ('Mail', ), + '1.3.6.1.7.1': ('MIME MHS', 'mime-mhs'), + '1.3.6.1.7.1.1': ('mime-mhs-headings', 'mime-mhs-headings'), + '1.3.6.1.7.1.1.1': ('id-hex-partial-message', 'id-hex-partial-message'), + '1.3.6.1.7.1.1.2': ('id-hex-multipart-message', 'id-hex-multipart-message'), + '1.3.6.1.7.1.2': ('mime-mhs-bodies', 'mime-mhs-bodies'), + '1.3.14.3.2': ('algorithm', 'algorithm'), + '1.3.14.3.2.3': ('md5WithRSA', 'RSA-NP-MD5'), + '1.3.14.3.2.6': ('des-ecb', 'DES-ECB'), + '1.3.14.3.2.7': ('des-cbc', 'DES-CBC'), + '1.3.14.3.2.8': ('des-ofb', 'DES-OFB'), + '1.3.14.3.2.9': ('des-cfb', 'DES-CFB'), + '1.3.14.3.2.11': ('rsaSignature', ), + '1.3.14.3.2.12': ('dsaEncryption-old', 'DSA-old'), + '1.3.14.3.2.13': ('dsaWithSHA', 'DSA-SHA'), + '1.3.14.3.2.15': ('shaWithRSAEncryption', 'RSA-SHA'), + '1.3.14.3.2.17': ('des-ede', 'DES-EDE'), + '1.3.14.3.2.18': ('sha', 'SHA'), + '1.3.14.3.2.26': ('sha1', 'SHA1'), + '1.3.14.3.2.27': ('dsaWithSHA1-old', 'DSA-SHA1-old'), + '1.3.14.3.2.29': ('sha1WithRSA', 'RSA-SHA1-2'), + '1.3.36.3.2.1': ('ripemd160', 'RIPEMD160'), + '1.3.36.3.3.1.2': ('ripemd160WithRSA', 'RSA-RIPEMD160'), + '1.3.36.3.3.2.8.1.1.1': ('brainpoolP160r1', ), + '1.3.36.3.3.2.8.1.1.2': ('brainpoolP160t1', ), + '1.3.36.3.3.2.8.1.1.3': ('brainpoolP192r1', ), + '1.3.36.3.3.2.8.1.1.4': ('brainpoolP192t1', ), + '1.3.36.3.3.2.8.1.1.5': ('brainpoolP224r1', ), + '1.3.36.3.3.2.8.1.1.6': ('brainpoolP224t1', ), + '1.3.36.3.3.2.8.1.1.7': ('brainpoolP256r1', ), + '1.3.36.3.3.2.8.1.1.8': ('brainpoolP256t1', ), + '1.3.36.3.3.2.8.1.1.9': ('brainpoolP320r1', ), + '1.3.36.3.3.2.8.1.1.10': ('brainpoolP320t1', ), + '1.3.36.3.3.2.8.1.1.11': ('brainpoolP384r1', ), + '1.3.36.3.3.2.8.1.1.12': ('brainpoolP384t1', ), + '1.3.36.3.3.2.8.1.1.13': ('brainpoolP512r1', ), + '1.3.36.3.3.2.8.1.1.14': ('brainpoolP512t1', ), + '1.3.36.8.3.3': ('Professional Information or basis for Admission', 'x509ExtAdmission'), + '1.3.101.1.4.1': ('Strong Extranet ID', 'SXNetID'), + '1.3.101.110': ('X25519', ), + '1.3.101.111': ('X448', ), + '1.3.101.112': ('ED25519', ), + '1.3.101.113': ('ED448', ), + '1.3.111': ('ieee', ), + '1.3.111.2.1619': ('IEEE Security in Storage Working Group', 'ieee-siswg'), + '1.3.111.2.1619.0.1.1': ('aes-128-xts', 'AES-128-XTS'), + '1.3.111.2.1619.0.1.2': ('aes-256-xts', 'AES-256-XTS'), + '1.3.132': ('certicom-arc', ), + '1.3.132.0': ('secg_ellipticCurve', ), + '1.3.132.0.1': ('sect163k1', ), + '1.3.132.0.2': ('sect163r1', ), + '1.3.132.0.3': ('sect239k1', ), + '1.3.132.0.4': ('sect113r1', ), + '1.3.132.0.5': ('sect113r2', ), + '1.3.132.0.6': ('secp112r1', ), + '1.3.132.0.7': ('secp112r2', ), + '1.3.132.0.8': ('secp160r1', ), + '1.3.132.0.9': ('secp160k1', ), + '1.3.132.0.10': ('secp256k1', ), + '1.3.132.0.15': ('sect163r2', ), + '1.3.132.0.16': ('sect283k1', ), + '1.3.132.0.17': ('sect283r1', ), + '1.3.132.0.22': ('sect131r1', ), + '1.3.132.0.23': ('sect131r2', ), + '1.3.132.0.24': ('sect193r1', ), + '1.3.132.0.25': ('sect193r2', ), + '1.3.132.0.26': ('sect233k1', ), + '1.3.132.0.27': ('sect233r1', ), + '1.3.132.0.28': ('secp128r1', ), + '1.3.132.0.29': ('secp128r2', ), + '1.3.132.0.30': ('secp160r2', ), + '1.3.132.0.31': ('secp192k1', ), + '1.3.132.0.32': ('secp224k1', ), + '1.3.132.0.33': ('secp224r1', ), + '1.3.132.0.34': ('secp384r1', ), + '1.3.132.0.35': ('secp521r1', ), + '1.3.132.0.36': ('sect409k1', ), + '1.3.132.0.37': ('sect409r1', ), + '1.3.132.0.38': ('sect571k1', ), + '1.3.132.0.39': ('sect571r1', ), + '1.3.132.1': ('secg-scheme', ), + '1.3.132.1.11.0': ('dhSinglePass-stdDH-sha224kdf-scheme', ), + '1.3.132.1.11.1': ('dhSinglePass-stdDH-sha256kdf-scheme', ), + '1.3.132.1.11.2': ('dhSinglePass-stdDH-sha384kdf-scheme', ), + '1.3.132.1.11.3': ('dhSinglePass-stdDH-sha512kdf-scheme', ), + '1.3.132.1.14.0': ('dhSinglePass-cofactorDH-sha224kdf-scheme', ), + '1.3.132.1.14.1': ('dhSinglePass-cofactorDH-sha256kdf-scheme', ), + '1.3.132.1.14.2': ('dhSinglePass-cofactorDH-sha384kdf-scheme', ), + '1.3.132.1.14.3': ('dhSinglePass-cofactorDH-sha512kdf-scheme', ), + '1.3.133.16.840.63.0': ('x9-63-scheme', ), + '1.3.133.16.840.63.0.2': ('dhSinglePass-stdDH-sha1kdf-scheme', ), + '1.3.133.16.840.63.0.3': ('dhSinglePass-cofactorDH-sha1kdf-scheme', ), + '2': ('joint-iso-itu-t', 'JOINT-ISO-ITU-T', 'joint-iso-ccitt'), + '2.5': ('directory services (X.500)', 'X500'), + '2.5.1.5': ('Selected Attribute Types', 'selected-attribute-types'), + '2.5.1.5.55': ('clearance', ), + '2.5.4': ('X509', ), + '2.5.4.3': ('commonName', 'CN'), + '2.5.4.4': ('surname', 'SN'), + '2.5.4.5': ('serialNumber', ), + '2.5.4.6': ('countryName', 'C'), + '2.5.4.7': ('localityName', 'L'), + '2.5.4.8': ('stateOrProvinceName', 'ST'), + '2.5.4.9': ('streetAddress', 'street'), + '2.5.4.10': ('organizationName', 'O'), + '2.5.4.11': ('organizationalUnitName', 'OU'), + '2.5.4.12': ('title', 'title'), + '2.5.4.13': ('description', ), + '2.5.4.14': ('searchGuide', ), + '2.5.4.15': ('businessCategory', ), + '2.5.4.16': ('postalAddress', ), + '2.5.4.17': ('postalCode', ), + '2.5.4.18': ('postOfficeBox', ), + '2.5.4.19': ('physicalDeliveryOfficeName', ), + '2.5.4.20': ('telephoneNumber', ), + '2.5.4.21': ('telexNumber', ), + '2.5.4.22': ('teletexTerminalIdentifier', ), + '2.5.4.23': ('facsimileTelephoneNumber', ), + '2.5.4.24': ('x121Address', ), + '2.5.4.25': ('internationaliSDNNumber', ), + '2.5.4.26': ('registeredAddress', ), + '2.5.4.27': ('destinationIndicator', ), + '2.5.4.28': ('preferredDeliveryMethod', ), + '2.5.4.29': ('presentationAddress', ), + '2.5.4.30': ('supportedApplicationContext', ), + '2.5.4.31': ('member', ), + '2.5.4.32': ('owner', ), + '2.5.4.33': ('roleOccupant', ), + '2.5.4.34': ('seeAlso', ), + '2.5.4.35': ('userPassword', ), + '2.5.4.36': ('userCertificate', ), + '2.5.4.37': ('cACertificate', ), + '2.5.4.38': ('authorityRevocationList', ), + '2.5.4.39': ('certificateRevocationList', ), + '2.5.4.40': ('crossCertificatePair', ), + '2.5.4.41': ('name', 'name'), + '2.5.4.42': ('givenName', 'GN'), + '2.5.4.43': ('initials', 'initials'), + '2.5.4.44': ('generationQualifier', ), + '2.5.4.45': ('x500UniqueIdentifier', ), + '2.5.4.46': ('dnQualifier', 'dnQualifier'), + '2.5.4.47': ('enhancedSearchGuide', ), + '2.5.4.48': ('protocolInformation', ), + '2.5.4.49': ('distinguishedName', ), + '2.5.4.50': ('uniqueMember', ), + '2.5.4.51': ('houseIdentifier', ), + '2.5.4.52': ('supportedAlgorithms', ), + '2.5.4.53': ('deltaRevocationList', ), + '2.5.4.54': ('dmdName', ), + '2.5.4.65': ('pseudonym', ), + '2.5.4.72': ('role', 'role'), + '2.5.4.97': ('organizationIdentifier', ), + '2.5.4.98': ('countryCode3c', 'c3'), + '2.5.4.99': ('countryCode3n', 'n3'), + '2.5.4.100': ('dnsName', ), + '2.5.8': ('directory services - algorithms', 'X500algorithms'), + '2.5.8.1.1': ('rsa', 'RSA'), + '2.5.8.3.100': ('mdc2WithRSA', 'RSA-MDC2'), + '2.5.8.3.101': ('mdc2', 'MDC2'), + '2.5.29': ('id-ce', ), + '2.5.29.9': ('X509v3 Subject Directory Attributes', 'subjectDirectoryAttributes'), + '2.5.29.14': ('X509v3 Subject Key Identifier', 'subjectKeyIdentifier'), + '2.5.29.15': ('X509v3 Key Usage', 'keyUsage'), + '2.5.29.16': ('X509v3 Private Key Usage Period', 'privateKeyUsagePeriod'), + '2.5.29.17': ('X509v3 Subject Alternative Name', 'subjectAltName'), + '2.5.29.18': ('X509v3 Issuer Alternative Name', 'issuerAltName'), + '2.5.29.19': ('X509v3 Basic Constraints', 'basicConstraints'), + '2.5.29.20': ('X509v3 CRL Number', 'crlNumber'), + '2.5.29.21': ('X509v3 CRL Reason Code', 'CRLReason'), + '2.5.29.23': ('Hold Instruction Code', 'holdInstructionCode'), + '2.5.29.24': ('Invalidity Date', 'invalidityDate'), + '2.5.29.27': ('X509v3 Delta CRL Indicator', 'deltaCRL'), + '2.5.29.28': ('X509v3 Issuing Distribution Point', 'issuingDistributionPoint'), + '2.5.29.29': ('X509v3 Certificate Issuer', 'certificateIssuer'), + '2.5.29.30': ('X509v3 Name Constraints', 'nameConstraints'), + '2.5.29.31': ('X509v3 CRL Distribution Points', 'crlDistributionPoints'), + '2.5.29.32': ('X509v3 Certificate Policies', 'certificatePolicies'), + '2.5.29.32.0': ('X509v3 Any Policy', 'anyPolicy'), + '2.5.29.33': ('X509v3 Policy Mappings', 'policyMappings'), + '2.5.29.35': ('X509v3 Authority Key Identifier', 'authorityKeyIdentifier'), + '2.5.29.36': ('X509v3 Policy Constraints', 'policyConstraints'), + '2.5.29.37': ('X509v3 Extended Key Usage', 'extendedKeyUsage'), + '2.5.29.37.0': ('Any Extended Key Usage', 'anyExtendedKeyUsage'), + '2.5.29.46': ('X509v3 Freshest CRL', 'freshestCRL'), + '2.5.29.54': ('X509v3 Inhibit Any Policy', 'inhibitAnyPolicy'), + '2.5.29.55': ('X509v3 AC Targeting', 'targetInformation'), + '2.5.29.56': ('X509v3 No Revocation Available', 'noRevAvail'), + '2.16.840.1.101.3': ('csor', ), + '2.16.840.1.101.3.4': ('nistAlgorithms', ), + '2.16.840.1.101.3.4.1': ('aes', ), + '2.16.840.1.101.3.4.1.1': ('aes-128-ecb', 'AES-128-ECB'), + '2.16.840.1.101.3.4.1.2': ('aes-128-cbc', 'AES-128-CBC'), + '2.16.840.1.101.3.4.1.3': ('aes-128-ofb', 'AES-128-OFB'), + '2.16.840.1.101.3.4.1.4': ('aes-128-cfb', 'AES-128-CFB'), + '2.16.840.1.101.3.4.1.5': ('id-aes128-wrap', ), + '2.16.840.1.101.3.4.1.6': ('aes-128-gcm', 'id-aes128-GCM'), + '2.16.840.1.101.3.4.1.7': ('aes-128-ccm', 'id-aes128-CCM'), + '2.16.840.1.101.3.4.1.8': ('id-aes128-wrap-pad', ), + '2.16.840.1.101.3.4.1.21': ('aes-192-ecb', 'AES-192-ECB'), + '2.16.840.1.101.3.4.1.22': ('aes-192-cbc', 'AES-192-CBC'), + '2.16.840.1.101.3.4.1.23': ('aes-192-ofb', 'AES-192-OFB'), + '2.16.840.1.101.3.4.1.24': ('aes-192-cfb', 'AES-192-CFB'), + '2.16.840.1.101.3.4.1.25': ('id-aes192-wrap', ), + '2.16.840.1.101.3.4.1.26': ('aes-192-gcm', 'id-aes192-GCM'), + '2.16.840.1.101.3.4.1.27': ('aes-192-ccm', 'id-aes192-CCM'), + '2.16.840.1.101.3.4.1.28': ('id-aes192-wrap-pad', ), + '2.16.840.1.101.3.4.1.41': ('aes-256-ecb', 'AES-256-ECB'), + '2.16.840.1.101.3.4.1.42': ('aes-256-cbc', 'AES-256-CBC'), + '2.16.840.1.101.3.4.1.43': ('aes-256-ofb', 'AES-256-OFB'), + '2.16.840.1.101.3.4.1.44': ('aes-256-cfb', 'AES-256-CFB'), + '2.16.840.1.101.3.4.1.45': ('id-aes256-wrap', ), + '2.16.840.1.101.3.4.1.46': ('aes-256-gcm', 'id-aes256-GCM'), + '2.16.840.1.101.3.4.1.47': ('aes-256-ccm', 'id-aes256-CCM'), + '2.16.840.1.101.3.4.1.48': ('id-aes256-wrap-pad', ), + '2.16.840.1.101.3.4.2': ('nist_hashalgs', ), + '2.16.840.1.101.3.4.2.1': ('sha256', 'SHA256'), + '2.16.840.1.101.3.4.2.2': ('sha384', 'SHA384'), + '2.16.840.1.101.3.4.2.3': ('sha512', 'SHA512'), + '2.16.840.1.101.3.4.2.4': ('sha224', 'SHA224'), + '2.16.840.1.101.3.4.2.5': ('sha512-224', 'SHA512-224'), + '2.16.840.1.101.3.4.2.6': ('sha512-256', 'SHA512-256'), + '2.16.840.1.101.3.4.2.7': ('sha3-224', 'SHA3-224'), + '2.16.840.1.101.3.4.2.8': ('sha3-256', 'SHA3-256'), + '2.16.840.1.101.3.4.2.9': ('sha3-384', 'SHA3-384'), + '2.16.840.1.101.3.4.2.10': ('sha3-512', 'SHA3-512'), + '2.16.840.1.101.3.4.2.11': ('shake128', 'SHAKE128'), + '2.16.840.1.101.3.4.2.12': ('shake256', 'SHAKE256'), + '2.16.840.1.101.3.4.2.13': ('hmac-sha3-224', 'id-hmacWithSHA3-224'), + '2.16.840.1.101.3.4.2.14': ('hmac-sha3-256', 'id-hmacWithSHA3-256'), + '2.16.840.1.101.3.4.2.15': ('hmac-sha3-384', 'id-hmacWithSHA3-384'), + '2.16.840.1.101.3.4.2.16': ('hmac-sha3-512', 'id-hmacWithSHA3-512'), + '2.16.840.1.101.3.4.3': ('dsa_with_sha2', 'sigAlgs'), + '2.16.840.1.101.3.4.3.1': ('dsa_with_SHA224', ), + '2.16.840.1.101.3.4.3.2': ('dsa_with_SHA256', ), + '2.16.840.1.101.3.4.3.3': ('dsa_with_SHA384', 'id-dsa-with-sha384'), + '2.16.840.1.101.3.4.3.4': ('dsa_with_SHA512', 'id-dsa-with-sha512'), + '2.16.840.1.101.3.4.3.5': ('dsa_with_SHA3-224', 'id-dsa-with-sha3-224'), + '2.16.840.1.101.3.4.3.6': ('dsa_with_SHA3-256', 'id-dsa-with-sha3-256'), + '2.16.840.1.101.3.4.3.7': ('dsa_with_SHA3-384', 'id-dsa-with-sha3-384'), + '2.16.840.1.101.3.4.3.8': ('dsa_with_SHA3-512', 'id-dsa-with-sha3-512'), + '2.16.840.1.101.3.4.3.9': ('ecdsa_with_SHA3-224', 'id-ecdsa-with-sha3-224'), + '2.16.840.1.101.3.4.3.10': ('ecdsa_with_SHA3-256', 'id-ecdsa-with-sha3-256'), + '2.16.840.1.101.3.4.3.11': ('ecdsa_with_SHA3-384', 'id-ecdsa-with-sha3-384'), + '2.16.840.1.101.3.4.3.12': ('ecdsa_with_SHA3-512', 'id-ecdsa-with-sha3-512'), + '2.16.840.1.101.3.4.3.13': ('RSA-SHA3-224', 'id-rsassa-pkcs1-v1_5-with-sha3-224'), + '2.16.840.1.101.3.4.3.14': ('RSA-SHA3-256', 'id-rsassa-pkcs1-v1_5-with-sha3-256'), + '2.16.840.1.101.3.4.3.15': ('RSA-SHA3-384', 'id-rsassa-pkcs1-v1_5-with-sha3-384'), + '2.16.840.1.101.3.4.3.16': ('RSA-SHA3-512', 'id-rsassa-pkcs1-v1_5-with-sha3-512'), + '2.16.840.1.113730': ('Netscape Communications Corp.', 'Netscape'), + '2.16.840.1.113730.1': ('Netscape Certificate Extension', 'nsCertExt'), + '2.16.840.1.113730.1.1': ('Netscape Cert Type', 'nsCertType'), + '2.16.840.1.113730.1.2': ('Netscape Base Url', 'nsBaseUrl'), + '2.16.840.1.113730.1.3': ('Netscape Revocation Url', 'nsRevocationUrl'), + '2.16.840.1.113730.1.4': ('Netscape CA Revocation Url', 'nsCaRevocationUrl'), + '2.16.840.1.113730.1.7': ('Netscape Renewal Url', 'nsRenewalUrl'), + '2.16.840.1.113730.1.8': ('Netscape CA Policy Url', 'nsCaPolicyUrl'), + '2.16.840.1.113730.1.12': ('Netscape SSL Server Name', 'nsSslServerName'), + '2.16.840.1.113730.1.13': ('Netscape Comment', 'nsComment'), + '2.16.840.1.113730.2': ('Netscape Data Type', 'nsDataType'), + '2.16.840.1.113730.2.5': ('Netscape Certificate Sequence', 'nsCertSequence'), + '2.16.840.1.113730.4.1': ('Netscape Server Gated Crypto', 'nsSGC'), + '2.23': ('International Organizations', 'international-organizations'), + '2.23.42': ('Secure Electronic Transactions', 'id-set'), + '2.23.42.0': ('content types', 'set-ctype'), + '2.23.42.0.0': ('setct-PANData', ), + '2.23.42.0.1': ('setct-PANToken', ), + '2.23.42.0.2': ('setct-PANOnly', ), + '2.23.42.0.3': ('setct-OIData', ), + '2.23.42.0.4': ('setct-PI', ), + '2.23.42.0.5': ('setct-PIData', ), + '2.23.42.0.6': ('setct-PIDataUnsigned', ), + '2.23.42.0.7': ('setct-HODInput', ), + '2.23.42.0.8': ('setct-AuthResBaggage', ), + '2.23.42.0.9': ('setct-AuthRevReqBaggage', ), + '2.23.42.0.10': ('setct-AuthRevResBaggage', ), + '2.23.42.0.11': ('setct-CapTokenSeq', ), + '2.23.42.0.12': ('setct-PInitResData', ), + '2.23.42.0.13': ('setct-PI-TBS', ), + '2.23.42.0.14': ('setct-PResData', ), + '2.23.42.0.16': ('setct-AuthReqTBS', ), + '2.23.42.0.17': ('setct-AuthResTBS', ), + '2.23.42.0.18': ('setct-AuthResTBSX', ), + '2.23.42.0.19': ('setct-AuthTokenTBS', ), + '2.23.42.0.20': ('setct-CapTokenData', ), + '2.23.42.0.21': ('setct-CapTokenTBS', ), + '2.23.42.0.22': ('setct-AcqCardCodeMsg', ), + '2.23.42.0.23': ('setct-AuthRevReqTBS', ), + '2.23.42.0.24': ('setct-AuthRevResData', ), + '2.23.42.0.25': ('setct-AuthRevResTBS', ), + '2.23.42.0.26': ('setct-CapReqTBS', ), + '2.23.42.0.27': ('setct-CapReqTBSX', ), + '2.23.42.0.28': ('setct-CapResData', ), + '2.23.42.0.29': ('setct-CapRevReqTBS', ), + '2.23.42.0.30': ('setct-CapRevReqTBSX', ), + '2.23.42.0.31': ('setct-CapRevResData', ), + '2.23.42.0.32': ('setct-CredReqTBS', ), + '2.23.42.0.33': ('setct-CredReqTBSX', ), + '2.23.42.0.34': ('setct-CredResData', ), + '2.23.42.0.35': ('setct-CredRevReqTBS', ), + '2.23.42.0.36': ('setct-CredRevReqTBSX', ), + '2.23.42.0.37': ('setct-CredRevResData', ), + '2.23.42.0.38': ('setct-PCertReqData', ), + '2.23.42.0.39': ('setct-PCertResTBS', ), + '2.23.42.0.40': ('setct-BatchAdminReqData', ), + '2.23.42.0.41': ('setct-BatchAdminResData', ), + '2.23.42.0.42': ('setct-CardCInitResTBS', ), + '2.23.42.0.43': ('setct-MeAqCInitResTBS', ), + '2.23.42.0.44': ('setct-RegFormResTBS', ), + '2.23.42.0.45': ('setct-CertReqData', ), + '2.23.42.0.46': ('setct-CertReqTBS', ), + '2.23.42.0.47': ('setct-CertResData', ), + '2.23.42.0.48': ('setct-CertInqReqTBS', ), + '2.23.42.0.49': ('setct-ErrorTBS', ), + '2.23.42.0.50': ('setct-PIDualSignedTBE', ), + '2.23.42.0.51': ('setct-PIUnsignedTBE', ), + '2.23.42.0.52': ('setct-AuthReqTBE', ), + '2.23.42.0.53': ('setct-AuthResTBE', ), + '2.23.42.0.54': ('setct-AuthResTBEX', ), + '2.23.42.0.55': ('setct-AuthTokenTBE', ), + '2.23.42.0.56': ('setct-CapTokenTBE', ), + '2.23.42.0.57': ('setct-CapTokenTBEX', ), + '2.23.42.0.58': ('setct-AcqCardCodeMsgTBE', ), + '2.23.42.0.59': ('setct-AuthRevReqTBE', ), + '2.23.42.0.60': ('setct-AuthRevResTBE', ), + '2.23.42.0.61': ('setct-AuthRevResTBEB', ), + '2.23.42.0.62': ('setct-CapReqTBE', ), + '2.23.42.0.63': ('setct-CapReqTBEX', ), + '2.23.42.0.64': ('setct-CapResTBE', ), + '2.23.42.0.65': ('setct-CapRevReqTBE', ), + '2.23.42.0.66': ('setct-CapRevReqTBEX', ), + '2.23.42.0.67': ('setct-CapRevResTBE', ), + '2.23.42.0.68': ('setct-CredReqTBE', ), + '2.23.42.0.69': ('setct-CredReqTBEX', ), + '2.23.42.0.70': ('setct-CredResTBE', ), + '2.23.42.0.71': ('setct-CredRevReqTBE', ), + '2.23.42.0.72': ('setct-CredRevReqTBEX', ), + '2.23.42.0.73': ('setct-CredRevResTBE', ), + '2.23.42.0.74': ('setct-BatchAdminReqTBE', ), + '2.23.42.0.75': ('setct-BatchAdminResTBE', ), + '2.23.42.0.76': ('setct-RegFormReqTBE', ), + '2.23.42.0.77': ('setct-CertReqTBE', ), + '2.23.42.0.78': ('setct-CertReqTBEX', ), + '2.23.42.0.79': ('setct-CertResTBE', ), + '2.23.42.0.80': ('setct-CRLNotificationTBS', ), + '2.23.42.0.81': ('setct-CRLNotificationResTBS', ), + '2.23.42.0.82': ('setct-BCIDistributionTBS', ), + '2.23.42.1': ('message extensions', 'set-msgExt'), + '2.23.42.1.1': ('generic cryptogram', 'setext-genCrypt'), + '2.23.42.1.3': ('merchant initiated auth', 'setext-miAuth'), + '2.23.42.1.4': ('setext-pinSecure', ), + '2.23.42.1.5': ('setext-pinAny', ), + '2.23.42.1.7': ('setext-track2', ), + '2.23.42.1.8': ('additional verification', 'setext-cv'), + '2.23.42.3': ('set-attr', ), + '2.23.42.3.0': ('setAttr-Cert', ), + '2.23.42.3.0.0': ('set-rootKeyThumb', ), + '2.23.42.3.0.1': ('set-addPolicy', ), + '2.23.42.3.1': ('payment gateway capabilities', 'setAttr-PGWYcap'), + '2.23.42.3.2': ('setAttr-TokenType', ), + '2.23.42.3.2.1': ('setAttr-Token-EMV', ), + '2.23.42.3.2.2': ('setAttr-Token-B0Prime', ), + '2.23.42.3.3': ('issuer capabilities', 'setAttr-IssCap'), + '2.23.42.3.3.3': ('setAttr-IssCap-CVM', ), + '2.23.42.3.3.3.1': ('generate cryptogram', 'setAttr-GenCryptgrm'), + '2.23.42.3.3.4': ('setAttr-IssCap-T2', ), + '2.23.42.3.3.4.1': ('encrypted track 2', 'setAttr-T2Enc'), + '2.23.42.3.3.4.2': ('cleartext track 2', 'setAttr-T2cleartxt'), + '2.23.42.3.3.5': ('setAttr-IssCap-Sig', ), + '2.23.42.3.3.5.1': ('ICC or token signature', 'setAttr-TokICCsig'), + '2.23.42.3.3.5.2': ('secure device signature', 'setAttr-SecDevSig'), + '2.23.42.5': ('set-policy', ), + '2.23.42.5.0': ('set-policy-root', ), + '2.23.42.7': ('certificate extensions', 'set-certExt'), + '2.23.42.7.0': ('setCext-hashedRoot', ), + '2.23.42.7.1': ('setCext-certType', ), + '2.23.42.7.2': ('setCext-merchData', ), + '2.23.42.7.3': ('setCext-cCertRequired', ), + '2.23.42.7.4': ('setCext-tunneling', ), + '2.23.42.7.5': ('setCext-setExt', ), + '2.23.42.7.6': ('setCext-setQualf', ), + '2.23.42.7.7': ('setCext-PGWYcapabilities', ), + '2.23.42.7.8': ('setCext-TokenIdentifier', ), + '2.23.42.7.9': ('setCext-Track2Data', ), + '2.23.42.7.10': ('setCext-TokenType', ), + '2.23.42.7.11': ('setCext-IssuerCapabilities', ), + '2.23.42.8': ('set-brand', ), + '2.23.42.8.1': ('set-brand-IATA-ATA', ), + '2.23.42.8.4': ('set-brand-Visa', ), + '2.23.42.8.5': ('set-brand-MasterCard', ), + '2.23.42.8.30': ('set-brand-Diners', ), + '2.23.42.8.34': ('set-brand-AmericanExpress', ), + '2.23.42.8.35': ('set-brand-JCB', ), + '2.23.42.8.6011': ('set-brand-Novus', ), + '2.23.43': ('wap', ), + '2.23.43.1': ('wap-wsg', ), + '2.23.43.1.4': ('wap-wsg-idm-ecid', ), + '2.23.43.1.4.1': ('wap-wsg-idm-ecid-wtls1', ), + '2.23.43.1.4.3': ('wap-wsg-idm-ecid-wtls3', ), + '2.23.43.1.4.4': ('wap-wsg-idm-ecid-wtls4', ), + '2.23.43.1.4.5': ('wap-wsg-idm-ecid-wtls5', ), + '2.23.43.1.4.6': ('wap-wsg-idm-ecid-wtls6', ), + '2.23.43.1.4.7': ('wap-wsg-idm-ecid-wtls7', ), + '2.23.43.1.4.8': ('wap-wsg-idm-ecid-wtls8', ), + '2.23.43.1.4.9': ('wap-wsg-idm-ecid-wtls9', ), + '2.23.43.1.4.10': ('wap-wsg-idm-ecid-wtls10', ), + '2.23.43.1.4.11': ('wap-wsg-idm-ecid-wtls11', ), + '2.23.43.1.4.12': ('wap-wsg-idm-ecid-wtls12', ), +} +# ##################################################################################### +# ##################################################################################### + +_OID_LOOKUP = dict() +_NORMALIZE_NAMES = dict() +_NORMALIZE_NAMES_SHORT = dict() + +for dotted, names in _OID_MAP.items(): + for name in names: + if name in _NORMALIZE_NAMES and _OID_LOOKUP[name] != dotted: + raise AssertionError( + 'Name collision during setup: "{0}" for OIDs {1} and {2}' + .format(name, dotted, _OID_LOOKUP[name]) + ) + _NORMALIZE_NAMES[name] = names[0] + _NORMALIZE_NAMES_SHORT[name] = names[-1] + _OID_LOOKUP[name] = dotted +for alias, original in [('userID', 'userId')]: + if alias in _NORMALIZE_NAMES: + raise AssertionError( + 'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}' + .format(alias, original, _OID_LOOKUP[alias]) + ) + _NORMALIZE_NAMES[alias] = original + _NORMALIZE_NAMES_SHORT[alias] = _NORMALIZE_NAMES_SHORT[original] + _OID_LOOKUP[alias] = _OID_LOOKUP[original] + + +def pyopenssl_normalize_name(name, short=False): + nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name)) + if nid != 0: + b_name = OpenSSL._util.lib.OBJ_nid2ln(nid) + name = to_text(OpenSSL._util.ffi.string(b_name)) + if short: + return _NORMALIZE_NAMES_SHORT.get(name, name) + else: + return _NORMALIZE_NAMES.get(name, name) + + +# ##################################################################################### +# ##################################################################################### +# # This excerpt is dual licensed under the terms of the Apache License, Version +# # 2.0, and the BSD License. See the LICENSE file at +# # https://github.com/pyca/cryptography/blob/master/LICENSE for complete details. +# # +# # Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py +# # +# # Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk) +# # Copyright (c) 2017 Fraser Tweedale (@frasertweedale) +# # +# # Relevant commits from cryptography project (https://github.com/pyca/cryptography): +# # pyca/cryptography@719d536dd691e84e208534798f2eb4f82aaa2e07 +# # pyca/cryptography@5ab6d6a5c05572bd1c75f05baf264a2d0001894a +# # pyca/cryptography@2e776e20eb60378e0af9b7439000d0e80da7c7e3 +# # pyca/cryptography@fb309ed24647d1be9e319b61b1f2aa8ebb87b90b +# # pyca/cryptography@2917e460993c475c72d7146c50dc3bbc2414280d +# # pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828 +# # pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f +def _obj2txt(openssl_lib, openssl_ffi, obj): + # Set to 80 on the recommendation of + # https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values + # + # But OIDs longer than this occur in real life (e.g. Active + # Directory makes some very long OIDs). So we need to detect + # and properly handle the case where the default buffer is not + # big enough. + # + buf_len = 80 + buf = openssl_ffi.new("char[]", buf_len) + + # 'res' is the number of bytes that *would* be written if the + # buffer is large enough. If 'res' > buf_len - 1, we need to + # alloc a big-enough buffer and go again. + res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) + if res > buf_len - 1: # account for terminating null byte + buf_len = res + 1 + buf = openssl_ffi.new("char[]", buf_len) + res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) + return openssl_ffi.buffer(buf, res)[:].decode() +# ##################################################################################### +# ##################################################################################### + + +def cryptography_get_extensions_from_cert(cert): + # Since cryptography won't give us the DER value for an extension + # (that is only stored for unrecognized extensions), we have to re-do + # the extension parsing outselves. + result = dict() + backend = cert._backend + x509_obj = cert._x509 + + for i in range(backend._lib.X509_get_ext_count(x509_obj)): + ext = backend._lib.X509_get_ext(x509_obj, i) + if ext == backend._ffi.NULL: + continue + crit = backend._lib.X509_EXTENSION_get_critical(ext) + data = backend._lib.X509_EXTENSION_get_data(ext) + backend.openssl_assert(data != backend._ffi.NULL) + der = backend._ffi.buffer(data.data, data.length)[:] + entry = dict( + critical=(crit == 1), + value=base64.b64encode(der), + ) + oid = _obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) + result[oid] = entry + return result + + +def cryptography_get_extensions_from_csr(csr): + # Since cryptography won't give us the DER value for an extension + # (that is only stored for unrecognized extensions), we have to re-do + # the extension parsing outselves. + result = dict() + backend = csr._backend + + extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req) + extensions = backend._ffi.gc( + extensions, + lambda ext: backend._lib.sk_X509_EXTENSION_pop_free( + ext, + backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free") + ) + ) + + for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)): + ext = backend._lib.sk_X509_EXTENSION_value(extensions, i) + if ext == backend._ffi.NULL: + continue + crit = backend._lib.X509_EXTENSION_get_critical(ext) + data = backend._lib.X509_EXTENSION_get_data(ext) + backend.openssl_assert(data != backend._ffi.NULL) + der = backend._ffi.buffer(data.data, data.length)[:] + entry = dict( + critical=(crit == 1), + value=base64.b64encode(der), + ) + oid = _obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) + result[oid] = entry + return result + + +def pyopenssl_get_extensions_from_cert(cert): + # While pyOpenSSL allows us to get an extension's DER value, it won't + # give us the dotted string for an OID. So we have to do some magic to + # get hold of it. + result = dict() + ext_count = cert.get_extension_count() + for i in range(0, ext_count): + ext = cert.get_extension(i) + entry = dict( + critical=bool(ext.get_critical()), + value=base64.b64encode(ext.get_data()), + ) + oid = _obj2txt( + OpenSSL._util.lib, + OpenSSL._util.ffi, + OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) + ) + # This could also be done a bit simpler: + # + # oid = _obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) + # + # Unfortunately this gives the wrong result in case the linked OpenSSL + # doesn't know the OID. That's why we have to get the OID dotted string + # similarly to how cryptography does it. + result[oid] = entry + return result + + +def pyopenssl_get_extensions_from_csr(csr): + # While pyOpenSSL allows us to get an extension's DER value, it won't + # give us the dotted string for an OID. So we have to do some magic to + # get hold of it. + result = dict() + for ext in csr.get_extensions(): + entry = dict( + critical=bool(ext.get_critical()), + value=base64.b64encode(ext.get_data()), + ) + oid = _obj2txt( + OpenSSL._util.lib, + OpenSSL._util.ffi, + OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) + ) + # This could also be done a bit simpler: + # + # oid = _obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) + # + # Unfortunately this gives the wrong result in case the linked OpenSSL + # doesn't know the OID. That's why we have to get the OID dotted string + # similarly to how cryptography does it. + result[oid] = entry + return result + + +def cryptography_name_to_oid(name): + dotted = _OID_LOOKUP.get(name) + if dotted is None: + raise OpenSSLObjectError('Cannot find OID for "{0}"'.format(name)) + return x509.oid.ObjectIdentifier(dotted) + + +def cryptography_oid_to_name(oid, short=False): + dotted_string = oid.dotted_string + names = _OID_MAP.get(dotted_string) + name = names[0] if names else oid._name + if short: + return _NORMALIZE_NAMES_SHORT.get(name, name) + else: + return _NORMALIZE_NAMES.get(name, name) + + +def cryptography_get_name(name): + ''' + Given a name string, returns a cryptography x509.Name object. + Raises an OpenSSLObjectError if the name is unknown or cannot be parsed. + ''' + try: + if name.startswith('DNS:'): + return x509.DNSName(to_text(name[4:])) + if name.startswith('IP:'): + return x509.IPAddress(ipaddress.ip_address(to_text(name[3:]))) + if name.startswith('email:'): + return x509.RFC822Name(to_text(name[6:])) + if name.startswith('URI:'): + return x509.UniformResourceIdentifier(to_text(name[4:])) + except Exception as e: + raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}": {1}'.format(name, e)) + if ':' not in name: + raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (forgot "DNS:" prefix?)'.format(name)) + raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(name)) + + +def _get_hex(bytesstr): + if bytesstr is None: + return bytesstr + data = binascii.hexlify(bytesstr) + data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2))) + return data + + +def cryptography_decode_name(name): + ''' + Given a cryptography x509.Name object, returns a string. + Raises an OpenSSLObjectError if the name is not supported. + ''' + if isinstance(name, x509.DNSName): + return 'DNS:{0}'.format(name.value) + if isinstance(name, x509.IPAddress): + return 'IP:{0}'.format(name.value.compressed) + if isinstance(name, x509.RFC822Name): + return 'email:{0}'.format(name.value) + if isinstance(name, x509.UniformResourceIdentifier): + return 'URI:{0}'.format(name.value) + if isinstance(name, x509.DirectoryName): + # FIXME: test + return 'DirName:' + ''.join(['/{0}:{1}'.format(attribute.oid._name, attribute.value) for attribute in name.value]) + if isinstance(name, x509.RegisteredID): + # FIXME: test + return 'RegisteredID:{0}'.format(name.value) + if isinstance(name, x509.OtherName): + # FIXME: test + return '{0}:{1}'.format(name.type_id.dotted_string, _get_hex(name.value)) + raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name)) + + +def _cryptography_get_keyusage(usage): + ''' + Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage(). + Raises an OpenSSLObjectError if the identifier is unknown. + ''' + if usage in ('Digital Signature', 'digitalSignature'): + return 'digital_signature' + if usage in ('Non Repudiation', 'nonRepudiation'): + return 'content_commitment' + if usage in ('Key Encipherment', 'keyEncipherment'): + return 'key_encipherment' + if usage in ('Data Encipherment', 'dataEncipherment'): + return 'data_encipherment' + if usage in ('Key Agreement', 'keyAgreement'): + return 'key_agreement' + if usage in ('Certificate Sign', 'keyCertSign'): + return 'key_cert_sign' + if usage in ('CRL Sign', 'cRLSign'): + return 'crl_sign' + if usage in ('Encipher Only', 'encipherOnly'): + return 'encipher_only' + if usage in ('Decipher Only', 'decipherOnly'): + return 'decipher_only' + raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage)) + + +def cryptography_parse_key_usage_params(usages): + ''' + Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage(). + Raises an OpenSSLObjectError if an identifier is unknown. + ''' + params = dict( + digital_signature=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ) + for usage in usages: + params[_cryptography_get_keyusage(usage)] = True + return params + + +def cryptography_get_basic_constraints(constraints): + ''' + Given a list of constraints, returns a tuple (ca, path_length). + Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed. + ''' + ca = False + path_length = None + if constraints: + for constraint in constraints: + if constraint.startswith('CA:'): + if constraint == 'CA:TRUE': + ca = True + elif constraint == 'CA:FALSE': + ca = False + else: + raise OpenSSLObjectError('Unknown basic constraint value "{0}" for CA'.format(constraint[3:])) + elif constraint.startswith('pathlen:'): + v = constraint[len('pathlen:'):] + try: + path_length = int(v) + except Exception as e: + raise OpenSSLObjectError('Cannot parse path length constraint "{0}" ({1})'.format(v, e)) + else: + raise OpenSSLObjectError('Unknown basic constraint "{0}"'.format(constraint)) + return ca, path_length + + +def binary_exp_mod(f, e, m): + '''Computes f^e mod m in O(log e) multiplications modulo m.''' + # Compute len_e = floor(log_2(e)) + len_e = -1 + x = e + while x > 0: + x >>= 1 + len_e += 1 + # Compute f**e mod m + result = 1 + for k in range(len_e, -1, -1): + result = (result * result) % m + if ((e >> k) & 1) != 0: + result = (result * f) % m + return result + + +def simple_gcd(a, b): + '''Compute GCD of its two inputs.''' + while b != 0: + a, b = b, a % b + return a + + +def quick_is_not_prime(n): + '''Does some quick checks to see if we can poke a hole into the primality of n. + + A result of `False` does **not** mean that the number is prime; it just means + that we couldn't detect quickly whether it is not prime. + ''' + if n <= 2: + return True + # The constant in the next line is the product of all primes < 200 + if simple_gcd(n, 7799922041683461553249199106329813876687996789903550945093032474868511536164700810) > 1: + return True + # TODO: maybe do some iterations of Miller-Rabin to increase confidence + # (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test) + return False + + +python_version = (sys.version_info[0], sys.version_info[1]) +if python_version >= (2, 7) or python_version >= (3, 1): + # Ansible still supports Python 2.6 on remote nodes + def count_bits(no): + no = abs(no) + if no == 0: + return 0 + return no.bit_length() +else: + # Slow, but works + def count_bits(no): + no = abs(no) + count = 0 + while no > 0: + no >>= 1 + count += 1 + return count + + +PEM_START = '-----BEGIN ' +PEM_END = '-----' +PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY') +PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY' + + +def identify_private_key_format(content): + '''Given the contents of a private key file, identifies its format.''' + # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85 + # (PEM_read_bio_PrivateKey) + # and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47 + # (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF) + try: + lines = content.decode('utf-8').splitlines(False) + if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): + name = lines[0][len(PEM_START):-len(PEM_END)] + if name in PKCS8_PRIVATEKEY_NAMES: + return 'pkcs8' + if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX): + return 'pkcs1' + return 'unknown-pem' + except UnicodeDecodeError: + pass + return 'raw' + + +def cryptography_key_needs_digest_for_signing(key): + '''Tests whether the given private key requires a digest algorithm for signing. + + Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm. + ''' + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + return False + if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + return False + return True + + +def cryptography_compare_public_keys(key1, key2): + '''Tests whether two public keys are the same. + + Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers(). + ''' + if CRYPTOGRAPHY_HAS_ED25519: + a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) + b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) + if a or b: + if not a or not b: + return False + a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return a == b + if CRYPTOGRAPHY_HAS_ED448: + a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) + b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) + if a or b: + if not a or not b: + return False + a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return a == b + return key1.public_numbers() == key2.public_numbers() + + +if HAS_CRYPTOGRAPHY: + REVOCATION_REASON_MAP = { + 'unspecified': x509.ReasonFlags.unspecified, + 'key_compromise': x509.ReasonFlags.key_compromise, + 'ca_compromise': x509.ReasonFlags.ca_compromise, + 'affiliation_changed': x509.ReasonFlags.affiliation_changed, + 'superseded': x509.ReasonFlags.superseded, + 'cessation_of_operation': x509.ReasonFlags.cessation_of_operation, + 'certificate_hold': x509.ReasonFlags.certificate_hold, + 'privilege_withdrawn': x509.ReasonFlags.privilege_withdrawn, + 'aa_compromise': x509.ReasonFlags.aa_compromise, + 'remove_from_crl': x509.ReasonFlags.remove_from_crl, + } + REVOCATION_REASON_MAP_INVERSE = dict() + for k, v in REVOCATION_REASON_MAP.items(): + REVOCATION_REASON_MAP_INVERSE[v] = k + + +def cryptography_decode_revoked_certificate(cert): + result = { + 'serial_number': cert.serial_number, + 'revocation_date': cert.revocation_date, + 'issuer': None, + 'issuer_critical': False, + 'reason': None, + 'reason_critical': False, + 'invalidity_date': None, + 'invalidity_date_critical': False, + } + try: + ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer) + result['issuer'] = list(ext.value) + result['issuer_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + try: + ext = cert.extensions.get_extension_for_class(x509.CRLReason) + result['reason'] = ext.value.reason + result['reason_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + try: + ext = cert.extensions.get_extension_for_class(x509.InvalidityDate) + result['invalidity_date'] = ext.value.invalidity_date + result['invalidity_date_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + return result diff --git a/plugins/module_utils/ecs/api.py b/plugins/module_utils/ecs/api.py new file mode 100644 index 00000000..d89b0333 --- /dev/null +++ b/plugins/module_utils/ecs/api.py @@ -0,0 +1,364 @@ +# -*- coding: utf-8 -*- + +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is licensed under the +# Modified BSD License. Modules you write using this snippet, which is embedded +# dynamically by Ansible, still belong to the author of the module, and may assign +# their own license to the complete work. +# +# Copyright (c), Entrust Datacard Corporation, 2019 +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +import os +import re +import time +import traceback + +from ansible.module_utils._text import to_text, to_native +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.six.moves.urllib.parse import urlencode +from ansible.module_utils.six.moves.urllib.error import HTTPError +from ansible.module_utils.urls import Request + +YAML_IMP_ERR = None +try: + import yaml +except ImportError: + YAML_FOUND = False + YAML_IMP_ERR = traceback.format_exc() +else: + YAML_FOUND = True + +valid_file_format = re.compile(r".*(\.)(yml|yaml|json)$") + + +def ecs_client_argument_spec(): + return dict( + entrust_api_user=dict(type='str', required=True), + entrust_api_key=dict(type='str', required=True, no_log=True), + entrust_api_client_cert_path=dict(type='path', required=True), + entrust_api_client_cert_key_path=dict(type='path', required=True, no_log=True), + entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'), + ) + + +class SessionConfigurationException(Exception): + """ Raised if we cannot configure a session with the API """ + + pass + + +class RestOperationException(Exception): + """ Encapsulate a REST API error """ + + def __init__(self, error): + self.status = to_native(error.get("status", None)) + self.errors = [to_native(err.get("message")) for err in error.get("errors", {})] + self.message = to_native(" ".join(self.errors)) + + +def generate_docstring(operation_spec): + """Generate a docstring for an operation defined in operation_spec (swagger)""" + # Description of the operation + docs = operation_spec.get("description", "No Description") + docs += "\n\n" + + # Parameters of the operation + parameters = operation_spec.get("parameters", []) + if len(parameters) != 0: + docs += "\tArguments:\n\n" + for parameter in parameters: + docs += "{0} ({1}:{2}): {3}\n".format( + parameter.get("name"), + parameter.get("type", "No Type"), + "Required" if parameter.get("required", False) else "Not Required", + parameter.get("description"), + ) + + return docs + + +def bind(instance, method, operation_spec): + def binding_scope_fn(*args, **kwargs): + return method(instance, *args, **kwargs) + + # Make sure we don't confuse users; add the proper name and documentation to the function. + # Users can use !help() to get help on the function from interactive python or pdb + operation_name = operation_spec.get("operationId").split("Using")[0] + binding_scope_fn.__name__ = str(operation_name) + binding_scope_fn.__doc__ = generate_docstring(operation_spec) + + return binding_scope_fn + + +class RestOperation(object): + def __init__(self, session, uri, method, parameters=None): + self.session = session + self.method = method + if parameters is None: + self.parameters = {} + else: + self.parameters = parameters + self.url = "{scheme}://{host}{base_path}{uri}".format(scheme="https", host=session._spec.get("host"), base_path=session._spec.get("basePath"), uri=uri) + + def restmethod(self, *args, **kwargs): + """Do the hard work of making the request here""" + + # gather named path parameters and do substitution on the URL + if self.parameters: + path_parameters = {} + body_parameters = {} + query_parameters = {} + for x in self.parameters: + expected_location = x.get("in") + key_name = x.get("name", None) + key_value = kwargs.get(key_name, None) + if expected_location == "path" and key_name and key_value: + path_parameters.update({key_name: key_value}) + elif expected_location == "body" and key_name and key_value: + body_parameters.update({key_name: key_value}) + elif expected_location == "query" and key_name and key_value: + query_parameters.update({key_name: key_value}) + + if len(body_parameters.keys()) >= 1: + body_parameters = body_parameters.get(list(body_parameters.keys())[0]) + else: + body_parameters = None + else: + path_parameters = {} + query_parameters = {} + body_parameters = None + + # This will fail if we have not set path parameters with a KeyError + url = self.url.format(**path_parameters) + if query_parameters: + # modify the URL to add path parameters + url = url + "?" + urlencode(query_parameters) + + try: + if body_parameters: + body_parameters_json = json.dumps(body_parameters) + response = self.session.request.open(method=self.method, url=url, data=body_parameters_json) + else: + response = self.session.request.open(method=self.method, url=url) + request_error = False + except HTTPError as e: + # An HTTPError has the same methods available as a valid response from request.open + response = e + request_error = True + + # Return the result if JSON and success ({} for empty responses) + # Raise an exception if there was a failure. + try: + result_code = response.getcode() + result = json.loads(response.read()) + except ValueError: + result = {} + + if result or result == {}: + if result_code and result_code < 400: + return result + else: + raise RestOperationException(result) + + # Raise a generic RestOperationException if this fails + raise RestOperationException({"status": result_code, "errors": [{"message": "REST Operation Failed"}]}) + + +class Resource(object): + """ Implement basic CRUD operations against a path. """ + + def __init__(self, session): + self.session = session + self.parameters = {} + + for url in session._spec.get("paths").keys(): + methods = session._spec.get("paths").get(url) + for method in methods.keys(): + operation_spec = methods.get(method) + operation_name = operation_spec.get("operationId", None) + parameters = operation_spec.get("parameters") + + if not operation_name: + if method.lower() == "post": + operation_name = "Create" + elif method.lower() == "get": + operation_name = "Get" + elif method.lower() == "put": + operation_name = "Update" + elif method.lower() == "delete": + operation_name = "Delete" + elif method.lower() == "patch": + operation_name = "Patch" + else: + raise SessionConfigurationException(to_native("Invalid REST method type {0}".format(method))) + + # Get the non-parameter parts of the URL and append to the operation name + # e.g /application/version -> GetApplicationVersion + # e.g. /application/{id} -> GetApplication + # This may lead to duplicates, which we must prevent. + operation_name += re.sub(r"{(.*)}", "", url).replace("/", " ").title().replace(" ", "") + operation_spec["operationId"] = operation_name + + op = RestOperation(session, url, method, parameters) + setattr(self, operation_name, bind(self, op.restmethod, operation_spec)) + + +# Session to encapsulate the connection parameters of the module_utils Request object, the api spec, etc +class ECSSession(object): + def __init__(self, name, **kwargs): + """ + Initialize our session + """ + + self._set_config(name, **kwargs) + + def client(self): + resource = Resource(self) + return resource + + def _set_config(self, name, **kwargs): + headers = { + "Content-Type": "application/json", + "Connection": "keep-alive", + } + self.request = Request(headers=headers, timeout=60) + + configurators = [self._read_config_vars] + for configurator in configurators: + self._config = configurator(name, **kwargs) + if self._config: + break + if self._config is None: + raise SessionConfigurationException(to_native("No Configuration Found.")) + + # set up auth if passed + entrust_api_user = self.get_config("entrust_api_user") + entrust_api_key = self.get_config("entrust_api_key") + if entrust_api_user and entrust_api_key: + self.request.url_username = entrust_api_user + self.request.url_password = entrust_api_key + else: + raise SessionConfigurationException(to_native("User and key must be provided.")) + + # set up client certificate if passed (support all-in one or cert + key) + entrust_api_cert = self.get_config("entrust_api_cert") + entrust_api_cert_key = self.get_config("entrust_api_cert_key") + if entrust_api_cert: + self.request.client_cert = entrust_api_cert + if entrust_api_cert_key: + self.request.client_key = entrust_api_cert_key + else: + raise SessionConfigurationException(to_native("Client certificate for authentication to the API must be provided.")) + + # set up the spec + entrust_api_specification_path = self.get_config("entrust_api_specification_path") + + if not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path): + raise SessionConfigurationException(to_native("OpenAPI specification was not found at location {0}.".format(entrust_api_specification_path))) + if not valid_file_format.match(entrust_api_specification_path): + raise SessionConfigurationException(to_native("OpenAPI specification filename must end in .json, .yml or .yaml")) + + self.verify = True + + if entrust_api_specification_path.startswith("http"): + try: + http_response = Request().open(method="GET", url=entrust_api_specification_path) + http_response_contents = http_response.read() + if entrust_api_specification_path.endswith(".json"): + self._spec = json.load(http_response_contents) + elif entrust_api_specification_path.endswith(".yml") or entrust_api_specification_path.endswith(".yaml"): + self._spec = yaml.safe_load(http_response_contents) + except HTTPError as e: + raise SessionConfigurationException(to_native("Error downloading specification from address '{0}', received error code '{1}'".format( + entrust_api_specification_path, e.getcode()))) + else: + with open(entrust_api_specification_path) as f: + if ".json" in entrust_api_specification_path: + self._spec = json.load(f) + elif ".yml" in entrust_api_specification_path or ".yaml" in entrust_api_specification_path: + self._spec = yaml.safe_load(f) + + def get_config(self, item): + return self._config.get(item, None) + + def _read_config_vars(self, name, **kwargs): + """ Read configuration from variables passed to the module. """ + config = {} + + entrust_api_specification_path = kwargs.get("entrust_api_specification_path") + if not entrust_api_specification_path or (not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path)): + raise SessionConfigurationException( + to_native( + "Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format( + entrust_api_specification_path + ) + ) + ) + + for required_file in ["entrust_api_cert", "entrust_api_cert_key"]: + file_path = kwargs.get(required_file) + if not file_path or not os.path.isfile(file_path): + raise SessionConfigurationException( + to_native("Parameter provided for {0} of value '{1}' was not a valid file path.".format(required_file, file_path)) + ) + + for required_var in ["entrust_api_user", "entrust_api_key"]: + if not kwargs.get(required_var): + raise SessionConfigurationException(to_native("Parameter provided for {0} was missing.".format(required_var))) + + config["entrust_api_cert"] = kwargs.get("entrust_api_cert") + config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key") + config["entrust_api_specification_path"] = kwargs.get("entrust_api_specification_path") + config["entrust_api_user"] = kwargs.get("entrust_api_user") + config["entrust_api_key"] = kwargs.get("entrust_api_key") + + return config + + +def ECSClient(entrust_api_user=None, entrust_api_key=None, entrust_api_cert=None, entrust_api_cert_key=None, entrust_api_specification_path=None): + """Create an ECS client""" + + if not YAML_FOUND: + raise SessionConfigurationException(missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) + + if entrust_api_specification_path is None: + entrust_api_specification_path = "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml" + + # Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases + entrust_api_user = to_text(entrust_api_user) + entrust_api_key = to_text(entrust_api_key) + entrust_api_cert_key = to_text(entrust_api_cert_key) + entrust_api_specification_path = to_text(entrust_api_specification_path) + + return ECSSession( + "ecs", + entrust_api_user=entrust_api_user, + entrust_api_key=entrust_api_key, + entrust_api_cert=entrust_api_cert, + entrust_api_cert_key=entrust_api_cert_key, + entrust_api_specification_path=entrust_api_specification_path, + ).client() diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/modules/acme_account.py b/plugins/modules/acme_account.py new file mode 100644 index 00000000..bb75eefe --- /dev/null +++ b/plugins/modules/acme_account.py @@ -0,0 +1,278 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016 Michael Gruener +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: acme_account +author: "Felix Fontein (@felixfontein)" +short_description: Create, modify or delete ACME accounts +description: + - "Allows to create, modify or delete accounts with a CA supporting the + L(ACME protocol,https://tools.ietf.org/html/rfc8555), + such as L(Let's Encrypt,https://letsencrypt.org/)." + - "This module only works with the ACME v2 protocol." +notes: + - "The M(acme_certificate) module also allows to do basic account management. + When using both modules, it is recommended to disable account management + for M(acme_certificate). For that, use the C(modify_account) option of + M(acme_certificate)." +seealso: + - name: Automatic Certificate Management Environment (ACME) + description: The specification of the ACME protocol (RFC 8555). + link: https://tools.ietf.org/html/rfc8555 + - module: acme_account_info + description: Retrieves facts about an ACME account. + - module: openssl_privatekey + description: Can be used to create a private account key. + - module: acme_inspect + description: Allows to debug problems. +extends_documentation_fragment: +- community.crypto.acme + +options: + state: + description: + - "The state of the account, to be identified by its account key." + - "If the state is C(absent), the account will either not exist or be + deactivated." + - "If the state is C(changed_key), the account must exist. The account + key will be changed; no other information will be touched." + type: str + required: true + choices: + - present + - absent + - changed_key + allow_creation: + description: + - "Whether account creation is allowed (when state is C(present))." + type: bool + default: yes + contact: + description: + - "A list of contact URLs." + - "Email addresses must be prefixed with C(mailto:)." + - "See U(https://tools.ietf.org/html/rfc8555#section-7.3) + for what is allowed." + - "Must be specified when state is C(present). Will be ignored + if state is C(absent) or C(changed_key)." + type: list + elements: str + default: [] + terms_agreed: + description: + - "Boolean indicating whether you agree to the terms of service document." + - "ACME servers can require this to be true." + type: bool + default: no + new_account_key_src: + description: + - "Path to a file containing the ACME account RSA or Elliptic Curve key to change to." + - "Same restrictions apply as to C(account_key_src)." + - "Mutually exclusive with C(new_account_key_content)." + - "Required if C(new_account_key_content) is not used and state is C(changed_key)." + type: path + new_account_key_content: + description: + - "Content of the ACME account RSA or Elliptic Curve key to change to." + - "Same restrictions apply as to C(account_key_content)." + - "Mutually exclusive with C(new_account_key_src)." + - "Required if C(new_account_key_src) is not used and state is C(changed_key)." + type: str +''' + +EXAMPLES = ''' +- name: Make sure account exists and has given contacts. We agree to TOS. + acme_account: + account_key_src: /etc/pki/cert/private/account.key + state: present + terms_agreed: yes + contact: + - mailto:me@example.com + - mailto:myself@example.org + +- name: Make sure account has given email address. Don't create account if it doesn't exist + acme_account: + account_key_src: /etc/pki/cert/private/account.key + state: present + allow_creation: no + contact: + - mailto:me@example.com + +- name: Change account's key to the one stored in the variable new_account_key + acme_account: + account_key_src: /etc/pki/cert/private/account.key + new_account_key_content: '{{ new_account_key }}' + state: changed_key + +- name: Delete account (we have to use the new key) + acme_account: + account_key_content: '{{ new_account_key }}' + state: absent +''' + +RETURN = ''' +account_uri: + description: ACME account URI, or None if account does not exist. + returned: always + type: str +''' + +from ansible_collections.community.crypto.plugins.module_utils.acme import ( + ModuleFailException, + ACMEAccount, + handle_standard_module_arguments, + get_default_argspec, +) + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + terms_agreed=dict(type='bool', default=False), + state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']), + allow_creation=dict(type='bool', default=True), + contact=dict(type='list', elements='str', default=[]), + new_account_key_src=dict(type='path'), + new_account_key_content=dict(type='str', no_log=True), + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ['new_account_key_src', 'new_account_key_content'], + ), + required_if=( + # Make sure that for state == changed_key, one of + # new_account_key_src and new_account_key_content are specified + ['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True], + ), + supports_check_mode=True, + ) + handle_standard_module_arguments(module, needs_acme_v2=True) + + try: + account = ACMEAccount(module) + changed = False + state = module.params.get('state') + diff_before = {} + diff_after = {} + if state == 'absent': + created, account_data = account.setup_account(allow_creation=False) + if account_data: + diff_before = dict(account_data) + diff_before['public_account_key'] = account.key_data['jwk'] + if created: + raise AssertionError('Unwanted account creation') + if account_data is not None: + # Account is not yet deactivated + if not module.check_mode: + # Deactivate it + payload = { + 'status': 'deactivated' + } + result, info = account.send_signed_request(account.uri, payload) + if info['status'] != 200: + raise ModuleFailException('Error deactivating account: {0} {1}'.format(info['status'], result)) + changed = True + elif state == 'present': + allow_creation = module.params.get('allow_creation') + # Make sure contact is a list of strings (unfortunately, Ansible doesn't do that for us) + contact = [str(v) for v in module.params.get('contact')] + terms_agreed = module.params.get('terms_agreed') + created, account_data = account.setup_account( + contact, + terms_agreed=terms_agreed, + allow_creation=allow_creation, + ) + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + if created: + diff_before = {} + else: + diff_before = dict(account_data) + diff_before['public_account_key'] = account.key_data['jwk'] + updated = False + if not created: + updated, account_data = account.update_account(account_data, contact) + changed = created or updated + diff_after = dict(account_data) + diff_after['public_account_key'] = account.key_data['jwk'] + elif state == 'changed_key': + # Parse new account key + error, new_key_data = account.parse_key( + module.params.get('new_account_key_src'), + module.params.get('new_account_key_content') + ) + if error: + raise ModuleFailException("error while parsing account key: %s" % error) + # Verify that the account exists and has not been deactivated + created, account_data = account.setup_account(allow_creation=False) + if created: + raise AssertionError('Unwanted account creation') + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + diff_before = dict(account_data) + diff_before['public_account_key'] = account.key_data['jwk'] + # Now we can start the account key rollover + if not module.check_mode: + # Compose inner signed message + # https://tools.ietf.org/html/rfc8555#section-7.3.5 + url = account.directory['keyChange'] + protected = { + "alg": new_key_data['alg'], + "jwk": new_key_data['jwk'], + "url": url, + } + payload = { + "account": account.uri, + "newKey": new_key_data['jwk'], # specified in draft 12 and older + "oldKey": account.jwk, # specified in draft 13 and newer + } + data = account.sign_request(protected, payload, new_key_data) + # Send request and verify result + result, info = account.send_signed_request(url, data) + if info['status'] != 200: + raise ModuleFailException('Error account key rollover: {0} {1}'.format(info['status'], result)) + if module._diff: + account.key_data = new_key_data + account.jws_header['alg'] = new_key_data['alg'] + diff_after = account.get_account_data() + elif module._diff: + # Kind of fake diff_after + diff_after = dict(diff_before) + diff_after['public_account_key'] = new_key_data['jwk'] + changed = True + result = { + 'changed': changed, + 'account_uri': account.uri, + } + if module._diff: + result['diff'] = { + 'before': diff_before, + 'after': diff_after, + } + module.exit_json(**result) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/acme_account_facts.py b/plugins/modules/acme_account_facts.py new file mode 120000 index 00000000..ffd88bce --- /dev/null +++ b/plugins/modules/acme_account_facts.py @@ -0,0 +1 @@ +acme_account_info.py \ No newline at end of file diff --git a/plugins/modules/acme_account_info.py b/plugins/modules/acme_account_info.py new file mode 100644 index 00000000..cdc2ccc8 --- /dev/null +++ b/plugins/modules/acme_account_info.py @@ -0,0 +1,300 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018 Felix Fontein +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: acme_account_info +author: "Felix Fontein (@felixfontein)" +short_description: Retrieves information on ACME accounts +description: + - "Allows to retrieve information on accounts a CA supporting the + L(ACME protocol,https://tools.ietf.org/html/rfc8555), + such as L(Let's Encrypt,https://letsencrypt.org/)." + - "This module only works with the ACME v2 protocol." +notes: + - "The M(acme_account) module allows to modify, create and delete ACME accounts." + - "This module was called C(acme_account_facts) before Ansible 2.8. The usage + did not change." +options: + retrieve_orders: + description: + - "Whether to retrieve the list of order URLs or order objects, if provided + by the ACME server." + - "A value of C(ignore) will not fetch the list of orders." + - "Currently, Let's Encrypt does not return orders, so the C(orders) result + will always be empty." + type: str + choices: + - ignore + - url_list + - object_list + default: ignore +seealso: + - module: acme_account + description: Allows to create, modify or delete an ACME account. +extends_documentation_fragment: +- community.crypto.acme + +''' + +EXAMPLES = ''' +- name: Check whether an account with the given account key exists + acme_account_info: + account_key_src: /etc/pki/cert/private/account.key + register: account_data +- name: Verify that account exists + assert: + that: + - account_data.exists +- name: Print account URI + debug: var=account_data.account_uri +- name: Print account contacts + debug: var=account_data.account.contact + +- name: Check whether the account exists and is accessible with the given account key + acme_account_info: + account_key_content: "{{ acme_account_key }}" + account_uri: "{{ acme_account_uri }}" + register: account_data +- name: Verify that account exists + assert: + that: + - account_data.exists +- name: Print account contacts + debug: var=account_data.account.contact +''' + +RETURN = ''' +exists: + description: Whether the account exists. + returned: always + type: bool + +account_uri: + description: ACME account URI, or None if account does not exist. + returned: always + type: str + +account: + description: The account information, as retrieved from the ACME server. + returned: if account exists + type: dict + contains: + contact: + description: the challenge resource that must be created for validation + returned: always + type: list + elements: str + sample: "['mailto:me@example.com', 'tel:00123456789']" + status: + description: the account's status + returned: always + type: str + choices: ['valid', 'deactivated', 'revoked'] + sample: valid + orders: + description: + - A URL where a list of orders can be retrieved for this account. + - Use the I(retrieve_orders) option to query this URL and retrieve the + complete list of orders. + returned: always + type: str + sample: https://example.ca/account/1/orders + public_account_key: + description: the public account key as a L(JSON Web Key,https://tools.ietf.org/html/rfc7517). + returned: always + type: str + sample: '{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"}' + +orders: + description: + - "The list of orders." + - "If I(retrieve_orders) is C(url_list), this will be a list of URLs." + - "If I(retrieve_orders) is C(object_list), this will be a list of objects." + type: list + #elements: ... depends on retrieve_orders + returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing + contains: + status: + description: The order's status. + type: str + choices: + - pending + - ready + - processing + - valid + - invalid + expires: + description: + - When the order expires. + - Timestamp should be formatted as described in RFC3339. + - Only required to be included in result when I(status) is C(pending) or C(valid). + type: str + returned: when server gives expiry date + identifiers: + description: + - List of identifiers this order is for. + type: list + elements: dict + contains: + type: + description: Type of identifier. C(dns) or C(ip). + type: str + value: + description: Name of identifier. Hostname or IP address. + type: str + wildcard: + description: "Whether I(value) is actually a wildcard. The wildcard + prefix C(*.) is not included in I(value) if this is C(true)." + type: bool + returned: required to be included if the identifier is wildcarded + notBefore: + description: + - The requested value of the C(notBefore) field in the certificate. + - Date should be formatted as described in RFC3339. + - Server is not required to return this. + type: str + returned: when server returns this + notAfter: + description: + - The requested value of the C(notAfter) field in the certificate. + - Date should be formatted as described in RFC3339. + - Server is not required to return this. + type: str + returned: when server returns this + error: + description: + - In case an error occurred during processing, this contains information about the error. + - The field is structured as a problem document (RFC7807). + type: dict + returned: when an error occurred + authorizations: + description: + - A list of URLs for authorizations for this order. + type: list + elements: str + finalize: + description: + - A URL used for finalizing an ACME order. + type: str + certificate: + description: + - The URL for retrieving the certificate. + type: str + returned: when certificate was issued +''' + +from ansible_collections.community.crypto.plugins.module_utils.acme import ( + ModuleFailException, + ACMEAccount, + handle_standard_module_arguments, + process_links, + get_default_argspec, +) + +from ansible.module_utils.basic import AnsibleModule + + +def get_orders_list(module, account, orders_url): + ''' + Retrieves orders list (handles pagination). + ''' + orders = [] + while orders_url: + # Get part of orders list + res, info = account.get_request(orders_url, parse_json_result=True, fail_on_error=True) + if not res.get('orders'): + if orders: + module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url)) + break + # Add order URLs to result list + orders.extend(res['orders']) + # Extract URL of next part of results list + new_orders_url = [] + + def f(link, relation): + if relation == 'next': + new_orders_url.append(link) + + process_links(info, f) + new_orders_url.append(None) + previous_orders_url, orders_url = orders_url, new_orders_url.pop(0) + if orders_url == previous_orders_url: + # Prevent infinite loop + orders_url = None + return orders + + +def get_order(account, order_url): + ''' + Retrieve order data. + ''' + return account.get_request(order_url, parse_json_result=True, fail_on_error=True)[0] + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']), + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ), + supports_check_mode=True, + ) + if module._name == 'acme_account_facts': + module.deprecate("The 'acme_account_facts' module has been renamed to 'acme_account_info'", version='2.12') + handle_standard_module_arguments(module, needs_acme_v2=True) + + try: + account = ACMEAccount(module) + # Check whether account exists + created, account_data = account.setup_account( + [], + allow_creation=False, + remove_account_uri_if_not_exists=True, + ) + if created: + raise AssertionError('Unwanted account creation') + result = { + 'changed': False, + 'exists': account.uri is not None, + 'account_uri': account.uri, + } + if account.uri is not None: + # Make sure promised data is there + if 'contact' not in account_data: + account_data['contact'] = [] + account_data['public_account_key'] = account.key_data['jwk'] + result['account'] = account_data + # Retrieve orders list + if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore': + orders = get_orders_list(module, account, account_data['orders']) + if module.params['retrieve_orders'] == 'url_list': + result['orders'] = orders + else: + result['orders'] = [get_order(account, order) for order in orders] + module.exit_json(**result) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/acme_certificate.py b/plugins/modules/acme_certificate.py new file mode 100644 index 00000000..e27c975f --- /dev/null +++ b/plugins/modules/acme_certificate.py @@ -0,0 +1,1257 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016 Michael Gruener +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: acme_certificate +author: "Michael Gruener (@mgruener)" +short_description: Create SSL/TLS certificates with the ACME protocol +description: + - "Create and renew SSL/TLS certificates with a CA supporting the + L(ACME protocol,https://tools.ietf.org/html/rfc8555), + such as L(Let's Encrypt,https://letsencrypt.org/) or + L(Buypass,https://www.buypass.com/). The current implementation + supports the C(http-01), C(dns-01) and C(tls-alpn-01) challenges." + - "To use this module, it has to be executed twice. Either as two + different tasks in the same run or during two runs. Note that the output + of the first run needs to be recorded and passed to the second run as the + module argument C(data)." + - "Between these two tasks you have to fulfill the required steps for the + chosen challenge by whatever means necessary. For C(http-01) that means + creating the necessary challenge file on the destination webserver. For + C(dns-01) the necessary dns record has to be created. For C(tls-alpn-01) + the necessary certificate has to be created and served. + It is I(not) the responsibility of this module to perform these steps." + - "For details on how to fulfill these challenges, you might have to read through + L(the main ACME specification,https://tools.ietf.org/html/rfc8555#section-8) + and the L(TLS-ALPN-01 specification,https://www.rfc-editor.org/rfc/rfc8737.html#section-3). + Also, consider the examples provided for this module." + - "The module includes experimental support for IP identifiers according to + the L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html)." +notes: + - "At least one of C(dest) and C(fullchain_dest) must be specified." + - "This module includes basic account management functionality. + If you want to have more control over your ACME account, use the M(acme_account) + module and disable account management for this module using the C(modify_account) + option." + - "This module was called C(letsencrypt) before Ansible 2.6. The usage + did not change." +seealso: + - name: The Let's Encrypt documentation + description: Documentation for the Let's Encrypt Certification Authority. + Provides useful information for example on rate limits. + link: https://letsencrypt.org/docs/ + - name: Buypass Go SSL + description: Documentation for the Buypass Certification Authority. + Provides useful information for example on rate limits. + link: https://www.buypass.com/ssl/products/acme + - name: Automatic Certificate Management Environment (ACME) + description: The specification of the ACME protocol (RFC 8555). + link: https://tools.ietf.org/html/rfc8555 + - name: ACME TLS ALPN Challenge Extension + description: The specification of the C(tls-alpn-01) challenge (RFC 8737). + link: https://www.rfc-editor.org/rfc/rfc8737.html-05 + - module: acme_challenge_cert_helper + description: Helps preparing C(tls-alpn-01) challenges. + - module: openssl_privatekey + description: Can be used to create private keys (both for certificates and accounts). + - module: openssl_csr + description: Can be used to create a Certificate Signing Request (CSR). + - module: certificate_complete_chain + description: Allows to find the root certificate for the returned fullchain. + - module: acme_certificate_revoke + description: Allows to revoke certificates. + - module: acme_account + description: Allows to create, modify or delete an ACME account. + - module: acme_inspect + description: Allows to debug problems. +extends_documentation_fragment: +- community.crypto.acme + +options: + account_email: + description: + - "The email address associated with this account." + - "It will be used for certificate expiration warnings." + - "Note that when C(modify_account) is not set to C(no) and you also + used the M(acme_account) module to specify more than one contact + for your account, this module will update your account and restrict + it to the (at most one) contact email address specified here." + type: str + agreement: + description: + - "URI to a terms of service document you agree to when using the + ACME v1 service at C(acme_directory)." + - Default is latest gathered from C(acme_directory) URL. + - This option will only be used when C(acme_version) is 1. + type: str + terms_agreed: + description: + - "Boolean indicating whether you agree to the terms of service document." + - "ACME servers can require this to be true." + - This option will only be used when C(acme_version) is not 1. + type: bool + default: no + modify_account: + description: + - "Boolean indicating whether the module should create the account if + necessary, and update its contact data." + - "Set to C(no) if you want to use the M(acme_account) module to manage + your account instead, and to avoid accidental creation of a new account + using an old key if you changed the account key with M(acme_account)." + - "If set to C(no), C(terms_agreed) and C(account_email) are ignored." + type: bool + default: yes + challenge: + description: The challenge to be performed. + type: str + default: 'http-01' + choices: [ 'http-01', 'dns-01', 'tls-alpn-01' ] + csr: + description: + - "File containing the CSR for the new certificate." + - "Can be created with C(openssl req ...)." + - "The CSR may contain multiple Subject Alternate Names, but each one + will lead to an individual challenge that must be fulfilled for the + CSR to be signed." + - "I(Note): the private key used to create the CSR I(must not) be the + account key. This is a bad idea from a security point of view, and + the CA should not accept the CSR. The ACME server should return an + error in this case." + type: path + required: true + aliases: ['src'] + data: + description: + - "The data to validate ongoing challenges. This must be specified for + the second run of the module only." + - "The value that must be used here will be provided by a previous use + of this module. See the examples for more details." + - "Note that for ACME v2, only the C(order_uri) entry of C(data) will + be used. For ACME v1, C(data) must be non-empty to indicate the + second stage is active; all needed data will be taken from the + CSR." + - "I(Note): the C(data) option was marked as C(no_log) up to + Ansible 2.5. From Ansible 2.6 on, it is no longer marked this way + as it causes error messages to be come unusable, and C(data) does + not contain any information which can be used without having + access to the account key or which are not public anyway." + type: dict + dest: + description: + - "The destination file for the certificate." + - "Required if C(fullchain_dest) is not specified." + type: path + aliases: ['cert'] + fullchain_dest: + description: + - "The destination file for the full chain (i.e. certificate followed + by chain of intermediate certificates)." + - "Required if C(dest) is not specified." + type: path + aliases: ['fullchain'] + chain_dest: + description: + - If specified, the intermediate certificate will be written to this file. + type: path + aliases: ['chain'] + remaining_days: + description: + - "The number of days the certificate must have left being valid. + If C(cert_days < remaining_days), then it will be renewed. + If the certificate is not renewed, module return values will not + include C(challenge_data)." + - "To make sure that the certificate is renewed in any case, you can + use the C(force) option." + type: int + default: 10 + deactivate_authzs: + description: + - "Deactivate authentication objects (authz) after issuing a certificate, + or when issuing the certificate failed." + - "Authentication objects are bound to an account key and remain valid + for a certain amount of time, and can be used to issue certificates + without having to re-authenticate the domain. This can be a security + concern." + type: bool + default: no + force: + description: + - Enforces the execution of the challenge and validation, even if an + existing certificate is still valid for more than C(remaining_days). + - This is especially helpful when having an updated CSR e.g. with + additional domains for which a new certificate is desired. + type: bool + default: no + retrieve_all_alternates: + description: + - "When set to C(yes), will retrieve all alternate trust chains offered by the ACME CA. + These will not be written to disk, but will be returned together with the main + chain as C(all_chains). See the documentation for the C(all_chains) return + value for details." + type: bool + default: no + select_chain: + description: + - "Allows to specify criteria by which an (alternate) trust chain can be selected." + - "The list of criteria will be processed one by one until a chain is found + matching a criterium. If such a chain is found, it will be used by the + module instead of the default chain." + - "If a criterium matches multiple chains, the first one matching will be + returned. The order is determined by the ordering of the C(Link) headers + returned by the ACME server and might not be deterministic." + - "Every criterium can consist of multiple different conditions, like I(issuer) + and I(subject). For the criterium to match a chain, all conditions must apply + to the same certificate in the chain." + - "This option can only be used with the C(cryptography) backend." + type: list + suboptions: + test_certificates: + description: + - "Determines which certificates in the chain will be tested." + - "I(all) tests all certificates in the chain (excluding the leaf, which is + identical in all chains)." + - "I(last) only tests the last certificate in the chain, i.e. the one furthest + away from the leaf. Its issuer is the root certificate of this chain." + type: str + default: all + choices: [last, all] + issuer: + description: + - "Allows to specify parts of the issuer of a certificate in the chain must + have to be selected." + - "If I(issuer) is empty, any certificate will match." + - 'An example value would be C({"commonName": "My Preferred CA Root"}).' + type: dict + subject: + description: + - "Allows to specify parts of the subject of a certificate in the chain must + have to be selected." + - "If I(subject) is empty, any certificate will match." + - 'An example value would be C({"CN": "My Preferred CA Intermediate"})' + type: dict + subject_key_identifier: + description: + - "Checks for the SubjectKeyIdentifier extension. This is an identifier based + on the private key of the intermediate certificate." + - "The identifier must be of the form + C(A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1)." + type: str + authority_key_identifier: + description: + - "Checks for the AuthorityKeyIdentifier extension. This is an identifier based + on the private key of the issuer of the intermediate certificate." + - "The identifier must be of the form + C(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)." + type: str +''' + +EXAMPLES = r''' +### Example with HTTP challenge ### + +- name: Create a challenge for sample.com using a account key from a variable. + acme_certificate: + account_key_content: "{{ account_private_key }}" + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + register: sample_com_challenge + +# Alternative first step: +- name: Create a challenge for sample.com using a account key from hashi vault. + acme_certificate: + account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}" + csr: /etc/pki/cert/csr/sample.com.csr + fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt + register: sample_com_challenge + +# Alternative first step: +- name: Create a challenge for sample.com using a account key file. + acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt + register: sample_com_challenge + +# perform the necessary steps to fulfill the challenge +# for example: +# +# - copy: +# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }} +# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}" +# when: sample_com_challenge is changed + +- name: Let the challenge be validated and retrieve the cert and intermediate certificate + acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt + chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt + data: "{{ sample_com_challenge }}" + +### Example with DNS challenge against production ACME server ### + +- name: Create a challenge for sample.com using a account key file. + acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + account_email: myself@sample.com + src: /etc/pki/cert/csr/sample.com.csr + cert: /etc/httpd/ssl/sample.com.crt + challenge: dns-01 + acme_directory: https://acme-v01.api.letsencrypt.org/directory + # Renew if the certificate is at least 30 days old + remaining_days: 60 + register: sample_com_challenge + +# perform the necessary steps to fulfill the challenge +# for example: +# +# - route53: +# zone: sample.com +# record: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].record }}" +# type: TXT +# ttl: 60 +# state: present +# wait: yes +# # Note: route53 requires TXT entries to be enclosed in quotes +# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | regex_replace('^(.*)$', '\"\\1\"') }}" +# when: sample_com_challenge is changed +# +# Alternative way: +# +# - route53: +# zone: sample.com +# record: "{{ item.key }}" +# type: TXT +# ttl: 60 +# state: present +# wait: yes +# # Note: item.value is a list of TXT entries, and route53 +# # requires every entry to be enclosed in quotes +# value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}" +# loop: "{{ sample_com_challenge.challenge_data_dns | dictsort }}" +# when: sample_com_challenge is changed + +- name: Let the challenge be validated and retrieve the cert and intermediate certificate + acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + account_email: myself@sample.com + src: /etc/pki/cert/csr/sample.com.csr + cert: /etc/httpd/ssl/sample.com.crt + fullchain: /etc/httpd/ssl/sample.com-fullchain.crt + chain: /etc/httpd/ssl/sample.com-intermediate.crt + challenge: dns-01 + acme_directory: https://acme-v01.api.letsencrypt.org/directory + remaining_days: 60 + data: "{{ sample_com_challenge }}" + when: sample_com_challenge is changed + +# Alternative second step: +- name: Let the challenge be validated and retrieve the cert and intermediate certificate + acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + account_email: myself@sample.com + src: /etc/pki/cert/csr/sample.com.csr + cert: /etc/httpd/ssl/sample.com.crt + fullchain: /etc/httpd/ssl/sample.com-fullchain.crt + chain: /etc/httpd/ssl/sample.com-intermediate.crt + challenge: tls-alpn-01 + remaining_days: 60 + data: "{{ sample_com_challenge }}" + # We use Let's Encrypt's ACME v2 endpoint + acme_directory: https://acme-v02.api.letsencrypt.org/directory + acme_version: 2 + # The following makes sure that if a chain with /CN=DST Root CA X3 in its issuer is provided + # as an alternative, it will be selected. These are the roots cross-signed by IdenTrust. + # As long as Let's Encrypt provides alternate chains with the cross-signed root(s) when + # switching to their own ISRG Root X1 root, this will use the chain ending with a cross-signed + # root. This chain is more compatible with older TLS clients. + select_chain: + - test_certificates: last + issuer: + CN: DST Root CA X3 + O: Digital Signature Trust Co. + when: sample_com_challenge is changed +''' + +RETURN = ''' +cert_days: + description: The number of days the certificate remains valid. + returned: success + type: int +challenge_data: + description: + - Per identifier / challenge type challenge data. + - Since Ansible 2.8.5, only challenges which are not yet valid are returned. + returned: changed + type: list + elements: dict + contains: + resource: + description: The challenge resource that must be created for validation. + returned: changed + type: str + sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA + resource_original: + description: + - The original challenge resource including type identifier for C(tls-alpn-01) + challenges. + returned: changed and challenge is C(tls-alpn-01) + type: str + sample: DNS:example.com + version_added: "2.8" + resource_value: + description: + - The value the resource has to produce for the validation. + - For C(http-01) and C(dns-01) challenges, the value can be used as-is. + - "For C(tls-alpn-01) challenges, note that this return value contains a + Base64 encoded version of the correct binary blob which has to be put + into the acmeValidation x509 extension; see + U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3) + for details. To do this, you might need the C(b64decode) Jinja filter + to extract the binary blob from this return value." + returned: changed + type: str + sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA + record: + description: The full DNS record's name for the challenge. + returned: changed and challenge is C(dns-01) + type: str + sample: _acme-challenge.example.com + version_added: "2.5" +challenge_data_dns: + description: + - List of TXT values per DNS record, in case challenge is C(dns-01). + - Since Ansible 2.8.5, only challenges which are not yet valid are returned. + returned: changed + type: dict + version_added: "2.5" +authorizations: + description: + - ACME authorization data. + - Maps an identifier to ACME authorization objects. See U(https://tools.ietf.org/html/rfc8555#section-7.1.4). + returned: changed + type: dict + sample: '{"example.com":{...}}' +order_uri: + description: ACME order URI. + returned: changed + type: str + version_added: "2.5" +finalization_uri: + description: ACME finalization URI. + returned: changed + type: str + version_added: "2.5" +account_uri: + description: ACME account URI. + returned: changed + type: str + version_added: "2.5" +all_chains: + description: + - When I(retrieve_all_alternates) is set to C(yes), the module will query the ACME server + for alternate chains. This return value will contain a list of all chains returned, + the first entry being the main chain returned by the server. + - See L(Section 7.4.2 of RFC8555,https://tools.ietf.org/html/rfc8555#section-7.4.2) for details. + returned: when certificate was retrieved and I(retrieve_all_alternates) is set to C(yes) + type: list + elements: dict + contains: + cert: + description: + - The leaf certificate itself, in PEM format. + type: str + returned: always + chain: + description: + - The certificate chain, excluding the root, as concatenated PEM certificates. + type: str + returned: always + full_chain: + description: + - The certificate chain, excluding the root, but including the leaf certificate, + as concatenated PEM certificates. + type: str + returned: always +''' + +from ansible_collections.community.crypto.plugins.module_utils.acme import ( + ModuleFailException, + write_file, + nopad_b64, + pem_to_der, + ACMEAccount, + HAS_CURRENT_CRYPTOGRAPHY, + cryptography_get_csr_identifiers, + openssl_get_csr_identifiers, + cryptography_get_cert_days, + handle_standard_module_arguments, + process_links, + get_default_argspec, +) + +import base64 +import binascii +import hashlib +import os +import re +import textwrap +import time +import traceback +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_bytes, to_native +from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress as compat_ipaddress +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils + +try: + import cryptography + import cryptography.hazmat.backends + import cryptography.x509 +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +def get_cert_days(module, cert_file): + ''' + Return the days the certificate in cert_file remains valid and -1 + if the file was not found. If cert_file contains more than one + certificate, only the first one will be considered. + ''' + if HAS_CURRENT_CRYPTOGRAPHY: + return cryptography_get_cert_days(module, cert_file) + if not os.path.exists(cert_file): + return -1 + + openssl_bin = module.get_bin_path('openssl', True) + openssl_cert_cmd = [openssl_bin, "x509", "-in", cert_file, "-noout", "-text"] + dummy, out, dummy = module.run_command(openssl_cert_cmd, check_rc=True, encoding=None) + try: + not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", out.decode('utf8')).group(1) + not_after = datetime.fromtimestamp(time.mktime(time.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z'))) + except AttributeError: + raise ModuleFailException("No 'Not after' date found in {0}".format(cert_file)) + except ValueError: + raise ModuleFailException("Failed to parse 'Not after' date of {0}".format(cert_file)) + now = datetime.utcnow() + return (not_after - now).days + + +class ACMEClient(object): + ''' + ACME client class. Uses an ACME account object and a CSR to + start and validate ACME challenges and download the respective + certificates. + ''' + + def __init__(self, module): + self.module = module + self.version = module.params['acme_version'] + self.challenge = module.params['challenge'] + self.csr = module.params['csr'] + self.dest = module.params.get('dest') + self.fullchain_dest = module.params.get('fullchain_dest') + self.chain_dest = module.params.get('chain_dest') + self.account = ACMEAccount(module) + self.directory = self.account.directory + self.data = module.params['data'] + self.authorizations = None + self.cert_days = -1 + self.order_uri = self.data.get('order_uri') if self.data else None + self.finalize_uri = None + + # Make sure account exists + modify_account = module.params['modify_account'] + if modify_account or self.version > 1: + contact = [] + if module.params['account_email']: + contact.append('mailto:' + module.params['account_email']) + created, account_data = self.account.setup_account( + contact, + agreement=module.params.get('agreement'), + terms_agreed=module.params.get('terms_agreed'), + allow_creation=modify_account, + ) + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + updated = False + if not created and account_data and modify_account: + updated, account_data = self.account.update_account(account_data, contact) + self.changed = created or updated + else: + # This happens if modify_account is False and the ACME v1 + # protocol is used. In this case, we do not call setup_account() + # to avoid accidental creation of an account. This is OK + # since for ACME v1, the account URI is not needed to send a + # signed ACME request. + pass + + if not os.path.exists(self.csr): + raise ModuleFailException("CSR %s not found" % (self.csr)) + + self._openssl_bin = module.get_bin_path('openssl', True) + + # Extract list of identifiers from CSR + self.identifiers = self._get_csr_identifiers() + + def _get_csr_identifiers(self): + ''' + Parse the CSR and return the list of requested identifiers + ''' + if HAS_CURRENT_CRYPTOGRAPHY: + return cryptography_get_csr_identifiers(self.module, self.csr) + else: + return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr) + + def _add_or_update_auth(self, identifier_type, identifier, auth): + ''' + Add or update the given authorization in the global authorizations list. + Return True if the auth was updated/added and False if no change was + necessary. + ''' + if self.authorizations.get(identifier_type + ':' + identifier) == auth: + return False + self.authorizations[identifier_type + ':' + identifier] = auth + return True + + def _new_authz_v1(self, identifier_type, identifier): + ''' + Create a new authorization for the given identifier. + Return the authorization object of the new authorization + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4 + ''' + new_authz = { + "resource": "new-authz", + "identifier": {"type": identifier_type, "value": identifier}, + } + + result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz) + if info['status'] not in [200, 201]: + raise ModuleFailException("Error requesting challenges: CODE: {0} RESULT: {1}".format(info['status'], result)) + else: + result['uri'] = info['location'] + return result + + def _get_challenge_data(self, auth, identifier_type, identifier): + ''' + Returns a dict with the data for all proposed (and supported) challenges + of the given authorization. + ''' + + data = {} + # no need to choose a specific challenge here as this module + # is not responsible for fulfilling the challenges. Calculate + # and return the required information for each challenge. + for challenge in auth['challenges']: + challenge_type = challenge['type'] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = self.account.get_keyauthorization(token) + + if challenge_type == 'http-01': + # https://tools.ietf.org/html/rfc8555#section-8.3 + resource = '.well-known/acme-challenge/' + token + data[challenge_type] = {'resource': resource, 'resource_value': keyauthorization} + elif challenge_type == 'dns-01': + if identifier_type != 'dns': + continue + # https://tools.ietf.org/html/rfc8555#section-8.4 + resource = '_acme-challenge' + value = nopad_b64(hashlib.sha256(to_bytes(keyauthorization)).digest()) + record = (resource + identifier[1:]) if identifier.startswith('*.') else (resource + '.' + identifier) + data[challenge_type] = {'resource': resource, 'resource_value': value, 'record': record} + elif challenge_type == 'tls-alpn-01': + # https://www.rfc-editor.org/rfc/rfc8737.html#section-3 + if identifier_type == 'ip': + # IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596) + resource = compat_ipaddress.ip_address(identifier).reverse_pointer + if not resource.endswith('.'): + resource += '.' + else: + resource = identifier + value = base64.b64encode(hashlib.sha256(to_bytes(keyauthorization)).digest()) + data[challenge_type] = {'resource': resource, 'resource_original': identifier_type + ':' + identifier, 'resource_value': value} + else: + continue + + return data + + def _fail_challenge(self, identifier_type, identifier, auth, error): + ''' + Aborts with a specific error for a challenge. + ''' + error_details = '' + # multiple challenges could have failed at this point, gather error + # details for all of them before failing + for challenge in auth['challenges']: + if challenge['status'] == 'invalid': + error_details += ' CHALLENGE: {0}'.format(challenge['type']) + if 'error' in challenge: + error_details += ' DETAILS: {0};'.format(challenge['error']['detail']) + else: + error_details += ';' + raise ModuleFailException("{0}: {1}".format(error.format(identifier_type + ':' + identifier), error_details)) + + def _validate_challenges(self, identifier_type, identifier, auth): + ''' + Validate the authorization provided in the auth dict. Returns True + when the validation was successful and False when it was not. + ''' + for challenge in auth['challenges']: + if self.challenge != challenge['type']: + continue + + uri = challenge['uri'] if self.version == 1 else challenge['url'] + + challenge_response = {} + if self.version == 1: + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = self.account.get_keyauthorization(token) + challenge_response["resource"] = "challenge" + challenge_response["keyAuthorization"] = keyauthorization + challenge_response["type"] = self.challenge + result, info = self.account.send_signed_request(uri, challenge_response) + if info['status'] not in [200, 202]: + raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result)) + + status = '' + + while status not in ['valid', 'invalid', 'revoked']: + result, dummy = self.account.get_request(auth['uri']) + result['uri'] = auth['uri'] + if self._add_or_update_auth(identifier_type, identifier, result): + self.changed = True + # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2 + # "status (required, string): ... + # If this field is missing, then the default value is "pending"." + if self.version == 1 and 'status' not in result: + status = 'pending' + else: + status = result['status'] + time.sleep(2) + + if status == 'invalid': + self._fail_challenge(identifier_type, identifier, result, 'Authorization for {0} returned invalid') + + return status == 'valid' + + def _finalize_cert(self): + ''' + Create a new certificate based on the csr. + Return the certificate object as dict + https://tools.ietf.org/html/rfc8555#section-7.4 + ''' + csr = pem_to_der(self.csr) + new_cert = { + "csr": nopad_b64(csr), + } + result, info = self.account.send_signed_request(self.finalize_uri, new_cert) + if info['status'] not in [200]: + raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result)) + + status = result['status'] + while status not in ['valid', 'invalid']: + time.sleep(2) + result, dummy = self.account.get_request(self.order_uri) + status = result['status'] + + if status != 'valid': + raise ModuleFailException("Error new cert: CODE: {0} STATUS: {1} RESULT: {2}".format(info['status'], status, result)) + + return result['certificate'] + + def _der_to_pem(self, der_cert): + ''' + Convert the DER format certificate in der_cert to a PEM format + certificate and return it. + ''' + return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( + "\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64))) + + def _download_cert(self, url): + ''' + Download and parse the certificate chain. + https://tools.ietf.org/html/rfc8555#section-7.4.2 + ''' + content, info = self.account.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'}) + + if not content or not info['content-type'].startswith('application/pem-certificate-chain'): + raise ModuleFailException("Cannot download certificate chain from {0}: {1} (headers: {2})".format(url, content, info)) + + cert = None + chain = [] + + # Parse data + lines = content.decode('utf-8').splitlines(True) + current = [] + for line in lines: + if line.strip(): + current.append(line) + if line.startswith('-----END CERTIFICATE-----'): + if cert is None: + cert = ''.join(current) + else: + chain.append(''.join(current)) + current = [] + + alternates = [] + + def f(link, relation): + if relation == 'up': + # Process link-up headers if there was no chain in reply + if not chain: + chain_result, chain_info = self.account.get_request(link, parse_json_result=False) + if chain_info['status'] in [200, 201]: + chain.append(self._der_to_pem(chain_result)) + elif relation == 'alternate': + alternates.append(link) + + process_links(info, f) + + if cert is None or current: + raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info)) + return {'cert': cert, 'chain': chain, 'alternates': alternates} + + def _new_cert_v1(self): + ''' + Create a new certificate based on the CSR (ACME v1 protocol). + Return the certificate object as dict + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5 + ''' + csr = pem_to_der(self.csr) + new_cert = { + "resource": "new-cert", + "csr": nopad_b64(csr), + } + result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert) + + chain = [] + + def f(link, relation): + if relation == 'up': + chain_result, chain_info = self.account.get_request(link, parse_json_result=False) + if chain_info['status'] in [200, 201]: + del chain[:] + chain.append(self._der_to_pem(chain_result)) + + process_links(info, f) + + if info['status'] not in [200, 201]: + raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result)) + else: + return {'cert': self._der_to_pem(result), 'uri': info['location'], 'chain': chain} + + def _new_order_v2(self): + ''' + Start a new certificate order (ACME v2 protocol). + https://tools.ietf.org/html/rfc8555#section-7.4 + ''' + identifiers = [] + for identifier_type, identifier in self.identifiers: + identifiers.append({ + 'type': identifier_type, + 'value': identifier, + }) + new_order = { + "identifiers": identifiers + } + result, info = self.account.send_signed_request(self.directory['newOrder'], new_order) + + if info['status'] not in [201]: + raise ModuleFailException("Error new order: CODE: {0} RESULT: {1}".format(info['status'], result)) + + for auth_uri in result['authorizations']: + auth_data, dummy = self.account.get_request(auth_uri) + auth_data['uri'] = auth_uri + identifier_type = auth_data['identifier']['type'] + identifier = auth_data['identifier']['value'] + if auth_data.get('wildcard', False): + identifier = '*.{0}'.format(identifier) + self.authorizations[identifier_type + ':' + identifier] = auth_data + + self.order_uri = info['location'] + self.finalize_uri = result['finalize'] + + def is_first_step(self): + ''' + Return True if this is the first execution of this module, i.e. if a + sufficient data object from a first run has not been provided. + ''' + if self.data is None: + return True + if self.version == 1: + # As soon as self.data is a non-empty object, we are in the second stage. + return not self.data + else: + # We are in the second stage if data.order_uri is given (which has been + # stored in self.order_uri by the constructor). + return self.order_uri is None + + def start_challenges(self): + ''' + Create new authorizations for all identifiers of the CSR, + respectively start a new order for ACME v2. + ''' + self.authorizations = {} + if self.version == 1: + for identifier_type, identifier in self.identifiers: + if identifier_type != 'dns': + raise ModuleFailException('ACME v1 only supports DNS identifiers!') + for identifier_type, identifier in self.identifiers: + new_auth = self._new_authz_v1(identifier_type, identifier) + self._add_or_update_auth(identifier_type, identifier, new_auth) + else: + self._new_order_v2() + self.changed = True + + def get_challenges_data(self): + ''' + Get challenge details for the chosen challenge type. + Return a tuple of generic challenge details, and specialized DNS challenge details. + ''' + # Get general challenge data + data = {} + for type_identifier, auth in self.authorizations.items(): + identifier_type, identifier = type_identifier.split(':', 1) + auth = self.authorizations[type_identifier] + # Skip valid authentications: their challenges are already valid + # and do not need to be returned + if auth['status'] == 'valid': + continue + # We drop the type from the key to preserve backwards compatibility + data[identifier] = self._get_challenge_data(auth, identifier_type, identifier) + # Get DNS challenge data + data_dns = {} + if self.challenge == 'dns-01': + for identifier, challenges in data.items(): + if self.challenge in challenges: + values = data_dns.get(challenges[self.challenge]['record']) + if values is None: + values = [] + data_dns[challenges[self.challenge]['record']] = values + values.append(challenges[self.challenge]['resource_value']) + return data, data_dns + + def finish_challenges(self): + ''' + Verify challenges for all identifiers of the CSR. + ''' + self.authorizations = {} + + # Step 1: obtain challenge information + if self.version == 1: + # For ACME v1, we attempt to create new authzs. Existing ones + # will be returned instead. + for identifier_type, identifier in self.identifiers: + new_auth = self._new_authz_v1(identifier_type, identifier) + self._add_or_update_auth(identifier_type, identifier, new_auth) + else: + # For ACME v2, we obtain the order object by fetching the + # order URI, and extract the information from there. + result, info = self.account.get_request(self.order_uri) + + if not result: + raise ModuleFailException("Cannot download order from {0}: {1} (headers: {2})".format(self.order_uri, result, info)) + + if info['status'] not in [200]: + raise ModuleFailException("Error on downloading order: CODE: {0} RESULT: {1}".format(info['status'], result)) + + for auth_uri in result['authorizations']: + auth_data, dummy = self.account.get_request(auth_uri) + auth_data['uri'] = auth_uri + identifier_type = auth_data['identifier']['type'] + identifier = auth_data['identifier']['value'] + if auth_data.get('wildcard', False): + identifier = '*.{0}'.format(identifier) + self.authorizations[identifier_type + ':' + identifier] = auth_data + + self.finalize_uri = result['finalize'] + + # Step 2: validate challenges + for type_identifier, auth in self.authorizations.items(): + if auth['status'] == 'pending': + identifier_type, identifier = type_identifier.split(':', 1) + self._validate_challenges(identifier_type, identifier, auth) + + def _chain_matches(self, chain, criterium): + ''' + Check whether an alternate chain matches the specified criterium. + ''' + if criterium['test_certificates'] == 'last': + chain = chain[-1:] + for cert in chain: + try: + x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend()) + matches = True + if criterium['subject']: + for k, v in crypto_utils.parse_name_field(criterium['subject']): + oid = crypto_utils.cryptography_name_to_oid(k) + value = to_native(v) + found = False + for attribute in x509.subject: + if attribute.oid == oid and value == to_native(attribute.value): + found = True + break + if not found: + matches = False + break + if criterium['issuer']: + for k, v in crypto_utils.parse_name_field(criterium['issuer']): + oid = crypto_utils.cryptography_name_to_oid(k) + value = to_native(v) + found = False + for attribute in x509.issuer: + if attribute.oid == oid and value == to_native(attribute.value): + found = True + break + if not found: + matches = False + break + if criterium['subject_key_identifier']: + try: + ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier) + if criterium['subject_key_identifier'] != ext.value.digest: + matches = False + except cryptography.x509.ExtensionNotFound: + matches = False + if criterium['authority_key_identifier']: + try: + ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier) + if criterium['authority_key_identifier'] != ext.value.key_identifier: + matches = False + except cryptography.x509.ExtensionNotFound: + matches = False + if matches: + return True + except Exception as e: + self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e)) + return False + + def get_certificate(self): + ''' + Request a new certificate and write it to the destination file. + First verifies whether all authorizations are valid; if not, aborts + with an error. + ''' + for identifier_type, identifier in self.identifiers: + auth = self.authorizations.get(identifier_type + ':' + identifier) + if auth is None: + raise ModuleFailException('Found no authorization information for "{0}"!'.format(identifier_type + ':' + identifier)) + if 'status' not in auth: + self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned no status') + if auth['status'] != 'valid': + self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned status ' + str(auth['status'])) + + if self.version == 1: + cert = self._new_cert_v1() + else: + cert_uri = self._finalize_cert() + cert = self._download_cert(cert_uri) + if self.module.params['retrieve_all_alternates'] or self.module.params['select_chain']: + # Retrieve alternate chains + alternate_chains = [] + for alternate in cert['alternates']: + try: + alt_cert = self._download_cert(alternate) + except ModuleFailException as e: + self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e)) + continue + alternate_chains.append(alt_cert) + + # Prepare return value for all alternate chains + if self.module.params['retrieve_all_alternates']: + self.all_chains = [] + + def _append_all_chains(cert_data): + self.all_chains.append(dict( + cert=cert_data['cert'].encode('utf8'), + chain=("\n".join(cert_data.get('chain', []))).encode('utf8'), + full_chain=(cert_data['cert'] + "\n".join(cert_data.get('chain', []))).encode('utf8'), + )) + + _append_all_chains(cert) + for alt_chain in alternate_chains: + _append_all_chains(alt_chain) + + # Try to select alternate chain depending on criteria + if self.module.params['select_chain']: + matching_chain = None + all_chains = [cert] + alternate_chains + for criterium_idx, criterium in enumerate(self.module.params['select_chain']): + for v in ('subject_key_identifier', 'authority_key_identifier'): + if criterium[v]: + try: + criterium[v] = binascii.unhexlify(criterium[v].replace(':', '')) + except Exception: + self.module.warn('Criterium {0} in select_chain has invalid {1} value. ' + 'Ignoring criterium.'.format(criterium_idx, v)) + continue + for alt_chain in all_chains: + if self._chain_matches(alt_chain.get('chain', []), criterium): + self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx)) + matching_chain = alt_chain + break + if matching_chain: + break + if matching_chain: + cert.update(matching_chain) + else: + self.module.debug('Found no matching alternative chain') + + if cert['cert'] is not None: + pem_cert = cert['cert'] + + chain = [link for link in cert.get('chain', [])] + + if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')): + self.cert_days = get_cert_days(self.module, self.dest) + self.changed = True + + if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')): + self.cert_days = get_cert_days(self.module, self.fullchain_dest) + self.changed = True + + if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')): + self.changed = True + + def deactivate_authzs(self): + ''' + Deactivates all valid authz's. Does not raise exceptions. + https://community.letsencrypt.org/t/authorization-deactivation/19860/2 + https://tools.ietf.org/html/rfc8555#section-7.5.2 + ''' + authz_deactivate = { + 'status': 'deactivated' + } + if self.version == 1: + authz_deactivate['resource'] = 'authz' + if self.authorizations: + for identifier_type, identifier in self.identifiers: + auth = self.authorizations.get(identifier_type + ':' + identifier) + if auth is None or auth.get('status') != 'valid': + continue + try: + result, info = self.account.send_signed_request(auth['uri'], authz_deactivate) + if 200 <= info['status'] < 300 and result.get('status') == 'deactivated': + auth['status'] = 'deactivated' + except Exception as dummy: + # Ignore errors on deactivating authzs + pass + if auth.get('status') != 'deactivated': + self.module.warn(warning='Could not deactivate authz object {0}.'.format(auth['uri'])) + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + modify_account=dict(type='bool', default=True), + account_email=dict(type='str'), + agreement=dict(type='str'), + terms_agreed=dict(type='bool', default=False), + challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01']), + csr=dict(type='path', required=True, aliases=['src']), + data=dict(type='dict'), + dest=dict(type='path', aliases=['cert']), + fullchain_dest=dict(type='path', aliases=['fullchain']), + chain_dest=dict(type='path', aliases=['chain']), + remaining_days=dict(type='int', default=10), + deactivate_authzs=dict(type='bool', default=False), + force=dict(type='bool', default=False), + retrieve_all_alternates=dict(type='bool', default=False), + select_chain=dict(type='list', elements='dict', options=dict( + test_certificates=dict(type='str', default='all', choices=['last', 'all']), + issuer=dict(type='dict'), + subject=dict(type='dict'), + subject_key_identifier=dict(type='str'), + authority_key_identifier=dict(type='str'), + )), + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content'], + ['dest', 'fullchain_dest'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ), + supports_check_mode=True, + ) + backend = handle_standard_module_arguments(module) + if module.params['select_chain']: + if backend != 'cryptography': + module.fail_json(msg="The 'select_chain' can only be used with the 'cryptography' backend.") + elif not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography')) + + try: + if module.params.get('dest'): + cert_days = get_cert_days(module, module.params['dest']) + else: + cert_days = get_cert_days(module, module.params['fullchain_dest']) + + if module.params['force'] or cert_days < module.params['remaining_days']: + # If checkmode is active, base the changed state solely on the status + # of the certificate file as all other actions (accessing an account, checking + # the authorization status...) would lead to potential changes of the current + # state + if module.check_mode: + module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days) + else: + client = ACMEClient(module) + client.cert_days = cert_days + other = dict() + if client.is_first_step(): + # First run: start challenges / start new order + client.start_challenges() + else: + # Second run: finish challenges, and get certificate + try: + client.finish_challenges() + client.get_certificate() + if module.params['retrieve_all_alternates']: + other['all_chains'] = client.all_chains + finally: + if module.params['deactivate_authzs']: + client.deactivate_authzs() + data, data_dns = client.get_challenges_data() + auths = dict() + for k, v in client.authorizations.items(): + # Remove "type:" from key + auths[k.split(':', 1)[1]] = v + module.exit_json( + changed=client.changed, + authorizations=auths, + finalize_uri=client.finalize_uri, + order_uri=client.order_uri, + account_uri=client.account.uri, + challenge_data=data, + challenge_data_dns=data_dns, + cert_days=client.cert_days, + **other + ) + else: + module.exit_json(changed=False, cert_days=cert_days) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/acme_certificate_revoke.py b/plugins/modules/acme_certificate_revoke.py new file mode 100644 index 00000000..4af4d662 --- /dev/null +++ b/plugins/modules/acme_certificate_revoke.py @@ -0,0 +1,223 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016 Michael Gruener +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: acme_certificate_revoke +author: "Felix Fontein (@felixfontein)" +short_description: Revoke certificates with the ACME protocol +description: + - "Allows to revoke certificates issued by a CA supporting the + L(ACME protocol,https://tools.ietf.org/html/rfc8555), + such as L(Let's Encrypt,https://letsencrypt.org/)." +notes: + - "Exactly one of C(account_key_src), C(account_key_content), + C(private_key_src) or C(private_key_content) must be specified." + - "Trying to revoke an already revoked certificate + should result in an unchanged status, even if the revocation reason + was different than the one specified here. Also, depending on the + server, it can happen that some other error is returned if the + certificate has already been revoked." +seealso: + - name: The Let's Encrypt documentation + description: Documentation for the Let's Encrypt Certification Authority. + Provides useful information for example on rate limits. + link: https://letsencrypt.org/docs/ + - name: Automatic Certificate Management Environment (ACME) + description: The specification of the ACME protocol (RFC 8555). + link: https://tools.ietf.org/html/rfc8555 + - module: acme_inspect + description: Allows to debug problems. +extends_documentation_fragment: +- community.crypto.acme + +options: + certificate: + description: + - "Path to the certificate to revoke." + type: path + required: yes + account_key_src: + description: + - "Path to a file containing the ACME account RSA or Elliptic Curve + key." + - "RSA keys can be created with C(openssl rsa ...). Elliptic curve keys can + be created with C(openssl ecparam -genkey ...). Any other tool creating + private keys in PEM format can be used as well." + - "Mutually exclusive with C(account_key_content)." + - "Required if C(account_key_content) is not used." + type: path + account_key_content: + description: + - "Content of the ACME account RSA or Elliptic Curve key." + - "Note that exactly one of C(account_key_src), C(account_key_content), + C(private_key_src) or C(private_key_content) must be specified." + - "I(Warning): the content will be written into a temporary file, which will + be deleted by Ansible when the module completes. Since this is an + important private key — it can be used to change the account key, + or to revoke your certificates without knowing their private keys + —, this might not be acceptable." + - "In case C(cryptography) is used, the content is not written into a + temporary file. It can still happen that it is written to disk by + Ansible in the process of moving the module with its argument to + the node where it is executed." + type: str + private_key_src: + description: + - "Path to the certificate's private key." + - "Note that exactly one of C(account_key_src), C(account_key_content), + C(private_key_src) or C(private_key_content) must be specified." + type: path + private_key_content: + description: + - "Content of the certificate's private key." + - "Note that exactly one of C(account_key_src), C(account_key_content), + C(private_key_src) or C(private_key_content) must be specified." + - "I(Warning): the content will be written into a temporary file, which will + be deleted by Ansible when the module completes. Since this is an + important private key — it can be used to change the account key, + or to revoke your certificates without knowing their private keys + —, this might not be acceptable." + - "In case C(cryptography) is used, the content is not written into a + temporary file. It can still happen that it is written to disk by + Ansible in the process of moving the module with its argument to + the node where it is executed." + type: str + revoke_reason: + description: + - "One of the revocation reasonCodes defined in + L(Section 5.3.1 of RFC5280,https://tools.ietf.org/html/rfc5280#section-5.3.1)." + - "Possible values are C(0) (unspecified), C(1) (keyCompromise), + C(2) (cACompromise), C(3) (affiliationChanged), C(4) (superseded), + C(5) (cessationOfOperation), C(6) (certificateHold), + C(8) (removeFromCRL), C(9) (privilegeWithdrawn), + C(10) (aACompromise)" + type: int +''' + +EXAMPLES = ''' +- name: Revoke certificate with account key + acme_certificate_revoke: + account_key_src: /etc/pki/cert/private/account.key + certificate: /etc/httpd/ssl/sample.com.crt + +- name: Revoke certificate with certificate's private key + acme_certificate_revoke: + private_key_src: /etc/httpd/ssl/sample.com.key + certificate: /etc/httpd/ssl/sample.com.crt +''' + +RETURN = ''' +''' + +from ansible_collections.community.crypto.plugins.module_utils.acme import ( + ModuleFailException, + ACMEAccount, + nopad_b64, + pem_to_der, + handle_standard_module_arguments, + get_default_argspec, +) + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + private_key_src=dict(type='path'), + private_key_content=dict(type='str', no_log=True), + certificate=dict(type='path', required=True), + revoke_reason=dict(type='int'), + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], + ), + supports_check_mode=False, + ) + handle_standard_module_arguments(module) + + try: + account = ACMEAccount(module) + # Load certificate + certificate = pem_to_der(module.params.get('certificate')) + certificate = nopad_b64(certificate) + # Construct payload + payload = { + 'certificate': certificate + } + if module.params.get('revoke_reason') is not None: + payload['reason'] = module.params.get('revoke_reason') + # Determine endpoint + if module.params.get('acme_version') == 1: + endpoint = account.directory['revoke-cert'] + payload['resource'] = 'revoke-cert' + else: + endpoint = account.directory['revokeCert'] + # Get hold of private key (if available) and make sure it comes from disk + private_key = module.params.get('private_key_src') + private_key_content = module.params.get('private_key_content') + # Revoke certificate + if private_key or private_key_content: + # Step 1: load and parse private key + error, private_key_data = account.parse_key(private_key, private_key_content) + if error: + raise ModuleFailException("error while parsing private key: %s" % error) + # Step 2: sign revokation request with private key + jws_header = { + "alg": private_key_data['alg'], + "jwk": private_key_data['jwk'], + } + result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header) + else: + # Step 1: get hold of account URI + created, account_data = account.setup_account(allow_creation=False) + if created: + raise AssertionError('Unwanted account creation') + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + # Step 2: sign revokation request with account key + result, info = account.send_signed_request(endpoint, payload) + if info['status'] != 200: + already_revoked = False + # Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6) + if result.get('type') == 'urn:ietf:params:acme:error:alreadyRevoked': + already_revoked = True + else: + # Hack for Boulder errors + if module.params.get('acme_version') == 1: + error_type = 'urn:acme:error:malformed' + else: + error_type = 'urn:ietf:params:acme:error:malformed' + if result.get('type') == error_type and result.get('detail') == 'Certificate already revoked': + # Fallback: boulder returns this in case the certificate was already revoked. + already_revoked = True + # If we know the certificate was already revoked, we don't fail, + # but successfully terminate while indicating no change + if already_revoked: + module.exit_json(changed=False) + raise ModuleFailException('Error revoking certificate: {0} {1}'.format(info['status'], result)) + module.exit_json(changed=True) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/acme_challenge_cert_helper.py b/plugins/modules/acme_challenge_cert_helper.py new file mode 100644 index 00000000..5c2a8c0c --- /dev/null +++ b/plugins/modules/acme_challenge_cert_helper.py @@ -0,0 +1,298 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018 Felix Fontein +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: acme_challenge_cert_helper +author: "Felix Fontein (@felixfontein)" +short_description: Prepare certificates required for ACME challenges such as C(tls-alpn-01) +description: + - "Prepares certificates for ACME challenges such as C(tls-alpn-01)." + - "The raw data is provided by the M(acme_certificate) module, and needs to be + converted to a certificate to be used for challenge validation. This module + provides a simple way to generate the required certificates." +seealso: + - name: Automatic Certificate Management Environment (ACME) + description: The specification of the ACME protocol (RFC 8555). + link: https://tools.ietf.org/html/rfc8555 + - name: ACME TLS ALPN Challenge Extension + description: The specification of the C(tls-alpn-01) challenge (RFC 8737). + link: https://www.rfc-editor.org/rfc/rfc8737.html +requirements: + - "cryptography >= 1.3" +options: + challenge: + description: + - "The challenge type." + type: str + required: yes + choices: + - tls-alpn-01 + challenge_data: + description: + - "The C(challenge_data) entry provided by M(acme_certificate) for the challenge." + type: dict + required: yes + private_key_src: + description: + - "Path to a file containing the private key file to use for this challenge + certificate." + - "Mutually exclusive with C(private_key_content)." + type: path + private_key_content: + description: + - "Content of the private key to use for this challenge certificate." + - "Mutually exclusive with C(private_key_src)." + type: str +''' + +EXAMPLES = ''' +- name: Create challenges for a given CRT for sample.com + acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + challenge: tls-alpn-01 + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + register: sample_com_challenge + +- name: Create certificates for challenges + acme_challenge_cert_helper: + challenge: tls-alpn-01 + challenge_data: "{{ item.value['tls-alpn-01'] }}" + private_key_src: /etc/pki/cert/key/sample.com.key + loop: "{{ sample_com_challenge.challenge_data | dictsort }}" + register: sample_com_challenge_certs + +- name: Install challenge certificates + # We need to set up HTTPS such that for the domain, + # regular_certificate is delivered for regular connections, + # except if ALPN selects the "acme-tls/1"; then, the + # challenge_certificate must be delivered. + # This can for example be achieved with very new versions + # of NGINX; search for ssl_preread and + # ssl_preread_alpn_protocols for information on how to + # route by ALPN protocol. + ...: + domain: "{{ item.domain }}" + challenge_certificate: "{{ item.challenge_certificate }}" + regular_certificate: "{{ item.regular_certificate }}" + private_key: /etc/pki/cert/key/sample.com.key + loop: "{{ sample_com_challenge_certs.results }}" + +- name: Create certificate for a given CSR for sample.com + acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + challenge: tls-alpn-01 + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + data: "{{ sample_com_challenge }}" +''' + +RETURN = ''' +domain: + description: + - "The domain the challenge is for. The certificate should be provided if + this is specified in the request's the C(Host) header." + returned: always + type: str +identifier_type: + description: + - "The identifier type for the actual resource identifier. Will be C(dns) + or C(ip)." + returned: always + type: str + version_added: "2.8" +identifier: + description: + - "The identifier for the actual resource. Will be a domain name if the + type is C(dns), or an IP address if the type is C(ip)." + returned: always + type: str + version_added: "2.8" +challenge_certificate: + description: + - "The challenge certificate in PEM format." + returned: always + type: str +regular_certificate: + description: + - "A self-signed certificate for the challenge domain." + - "If no existing certificate exists, can be used to set-up + https in the first place if that is needed for providing + the challenge." + returned: always + type: str +''' + +from ansible_collections.community.crypto.plugins.module_utils.acme import ( + ModuleFailException, + read_file, +) + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_bytes, to_text + +import base64 +import datetime +import sys +import traceback + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization + import cryptography.hazmat.primitives.asymmetric.rsa + import cryptography.hazmat.primitives.asymmetric.ec + import cryptography.hazmat.primitives.asymmetric.padding + import cryptography.hazmat.primitives.hashes + import cryptography.hazmat.primitives.asymmetric.utils + import cryptography.x509 + import cryptography.x509.oid + import ipaddress + from distutils.version import LooseVersion + HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3')) + _cryptography_backend = cryptography.hazmat.backends.default_backend() +except ImportError as dummy: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + HAS_CRYPTOGRAPHY = False + + +# Convert byte string to ASN1 encoded octet string +if sys.version_info[0] >= 3: + def encode_octet_string(octet_string): + if len(octet_string) >= 128: + raise ModuleFailException('Cannot handle octet strings with more than 128 bytes') + return bytes([0x4, len(octet_string)]) + octet_string +else: + def encode_octet_string(octet_string): + if len(octet_string) >= 128: + raise ModuleFailException('Cannot handle octet strings with more than 128 bytes') + return b'\x04' + chr(len(octet_string)) + octet_string + + +def main(): + module = AnsibleModule( + argument_spec=dict( + challenge=dict(type='str', required=True, choices=['tls-alpn-01']), + challenge_data=dict(type='dict', required=True), + private_key_src=dict(type='path'), + private_key_content=dict(type='str', no_log=True), + ), + required_one_of=( + ['private_key_src', 'private_key_content'], + ), + mutually_exclusive=( + ['private_key_src', 'private_key_content'], + ), + ) + if not HAS_CRYPTOGRAPHY: + module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR) + + try: + # Get parameters + challenge = module.params['challenge'] + challenge_data = module.params['challenge_data'] + + # Get hold of private key + private_key_content = module.params.get('private_key_content') + if private_key_content is None: + private_key_content = read_file(module.params['private_key_src']) + else: + private_key_content = to_bytes(private_key_content) + try: + private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(private_key_content, password=None, backend=_cryptography_backend) + except Exception as e: + raise ModuleFailException('Error while loading private key: {0}'.format(e)) + + # Some common attributes + domain = to_text(challenge_data['resource']) + identifier_type, identifier = to_text(challenge_data.get('resource_original', 'dns:' + challenge_data['resource'])).split(':', 1) + subject = issuer = cryptography.x509.Name([]) + not_valid_before = datetime.datetime.utcnow() + not_valid_after = datetime.datetime.utcnow() + datetime.timedelta(days=10) + if identifier_type == 'dns': + san = cryptography.x509.DNSName(identifier) + elif identifier_type == 'ip': + san = cryptography.x509.IPAddress(ipaddress.ip_address(identifier)) + else: + raise ModuleFailException('Unsupported identifier type "{0}"'.format(identifier_type)) + + # Generate regular self-signed certificate + regular_certificate = cryptography.x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + private_key.public_key() + ).serial_number( + cryptography.x509.random_serial_number() + ).not_valid_before( + not_valid_before + ).not_valid_after( + not_valid_after + ).add_extension( + cryptography.x509.SubjectAlternativeName([san]), + critical=False, + ).sign( + private_key, + cryptography.hazmat.primitives.hashes.SHA256(), + _cryptography_backend + ) + + # Process challenge + if challenge == 'tls-alpn-01': + value = base64.b64decode(challenge_data['resource_value']) + challenge_certificate = cryptography.x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + private_key.public_key() + ).serial_number( + cryptography.x509.random_serial_number() + ).not_valid_before( + not_valid_before + ).not_valid_after( + not_valid_after + ).add_extension( + cryptography.x509.SubjectAlternativeName([san]), + critical=False, + ).add_extension( + cryptography.x509.UnrecognizedExtension( + cryptography.x509.ObjectIdentifier("1.3.6.1.5.5.7.1.31"), + encode_octet_string(value), + ), + critical=True, + ).sign( + private_key, + cryptography.hazmat.primitives.hashes.SHA256(), + _cryptography_backend + ) + + module.exit_json( + changed=True, + domain=domain, + identifier_type=identifier_type, + identifier=identifier, + challenge_certificate=challenge_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM), + regular_certificate=regular_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM) + ) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/acme_inspect.py b/plugins/modules/acme_inspect.py new file mode 100644 index 00000000..2f7a5091 --- /dev/null +++ b/plugins/modules/acme_inspect.py @@ -0,0 +1,320 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018 Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = r''' +--- +module: acme_inspect +author: "Felix Fontein (@felixfontein)" +short_description: Send direct requests to an ACME server +description: + - "Allows to send direct requests to an ACME server with the + L(ACME protocol,https://tools.ietf.org/html/rfc8555), + which is supported by CAs such as L(Let's Encrypt,https://letsencrypt.org/)." + - "This module can be used to debug failed certificate request attempts, + for example when M(acme_certificate) fails or encounters a problem which + you wish to investigate." + - "The module can also be used to directly access features of an ACME servers + which are not yet supported by the Ansible ACME modules." +notes: + - "The I(account_uri) option must be specified for properly authenticated + ACME v2 requests (except a C(new-account) request)." + - "Using the C(ansible) tool, M(acme_inspect) can be used to directly execute + ACME requests without the need of writing a playbook. For example, the + following command retrieves the ACME account with ID 1 from Let's Encrypt + (assuming C(/path/to/key) is the correct private account key): + C(ansible localhost -m acme_inspect -a \"account_key_src=/path/to/key + acme_directory=https://acme-v02.api.letsencrypt.org/directory acme_version=2 + account_uri=https://acme-v02.api.letsencrypt.org/acme/acct/1 method=get + url=https://acme-v02.api.letsencrypt.org/acme/acct/1\")" +seealso: + - name: Automatic Certificate Management Environment (ACME) + description: The specification of the ACME protocol (RFC 8555). + link: https://tools.ietf.org/html/rfc8555 + - name: ACME TLS ALPN Challenge Extension + description: The specification of the C(tls-alpn-01) challenge (RFC 8737). + link: https://www.rfc-editor.org/rfc/rfc8737.html +extends_documentation_fragment: +- community.crypto.acme + +options: + url: + description: + - "The URL to send the request to." + - "Must be specified if I(method) is not C(directory-only)." + type: str + method: + description: + - "The method to use to access the given URL on the ACME server." + - "The value C(post) executes an authenticated POST request. The content + must be specified in the I(content) option." + - "The value C(get) executes an authenticated POST-as-GET request for ACME v2, + and a regular GET request for ACME v1." + - "The value C(directory-only) only retrieves the directory, without doing + a request." + type: str + default: get + choices: + - get + - post + - directory-only + content: + description: + - "An encoded JSON object which will be sent as the content if I(method) + is C(post)." + - "Required when I(method) is C(post), and not allowed otherwise." + type: str + fail_on_acme_error: + description: + - "If I(method) is C(post) or C(get), make the module fail in case an ACME + error is returned." + type: bool + default: yes +''' + +EXAMPLES = r''' +- name: Get directory + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + method: directory-only + register: directory + +- name: Create an account + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + url: "{{ directory.newAccount}}" + method: post + content: '{"termsOfServiceAgreed":true}' + register: account_creation + # account_creation.headers.location contains the account URI + # if creation was successful + +- name: Get account information + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + account_uri: "{{ account_creation.headers.location }}" + url: "{{ account_creation.headers.location }}" + method: get + +- name: Update account contacts + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + account_uri: "{{ account_creation.headers.location }}" + url: "{{ account_creation.headers.location }}" + method: post + content: '{{ account_info | to_json }}' + vars: + account_info: + # For valid values, see + # https://tools.ietf.org/html/rfc8555#section-7.3 + contact: + - mailto:me@example.com + +- name: Create certificate order + acme_certificate: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + account_uri: "{{ account_creation.headers.location }}" + csr: /etc/pki/cert/csr/sample.com.csr + fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt + challenge: http-01 + register: certificate_request + +# Assume something went wrong. certificate_request.order_uri contains +# the order URI. + +- name: Get order information + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + account_uri: "{{ account_creation.headers.location }}" + url: "{{ certificate_request.order_uri }}" + method: get + register: order + +- name: Get first authz for order + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + account_uri: "{{ account_creation.headers.location }}" + url: "{{ order.output_json.authorizations[0] }}" + method: get + register: authz + +- name: Get HTTP-01 challenge for authz + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + account_uri: "{{ account_creation.headers.location }}" + url: "{{ authz.output_json.challenges | selectattr('type', 'equalto', 'http-01') }}" + method: get + register: http01challenge + +- name: Activate HTTP-01 challenge manually + acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + account_key_src: /etc/pki/cert/private/account.key + account_uri: "{{ account_creation.headers.location }}" + url: "{{ http01challenge.url }}" + method: post + content: '{}' +''' + +RETURN = ''' +directory: + description: The ACME directory's content + returned: always + type: dict + sample: | + { + "a85k3x9f91A4": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417", + "keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change", + "meta": { + "caaIdentities": [ + "letsencrypt.org" + ], + "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf", + "website": "https://letsencrypt.org" + }, + "newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct", + "newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce", + "newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order", + "revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert" + } +headers: + description: The request's HTTP headers (with lowercase keys) + returned: always + type: dict + sample: | + { + "boulder-requester": "12345", + "cache-control": "max-age=0, no-cache, no-store", + "connection": "close", + "content-length": "904", + "content-type": "application/json", + "cookies": {}, + "cookies_string": "", + "date": "Wed, 07 Nov 2018 12:34:56 GMT", + "expires": "Wed, 07 Nov 2018 12:44:56 GMT", + "link": ";rel=\"terms-of-service\"", + "msg": "OK (904 bytes)", + "pragma": "no-cache", + "replay-nonce": "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGH", + "server": "nginx", + "status": 200, + "strict-transport-security": "max-age=604800", + "url": "https://acme-v02.api.letsencrypt.org/acme/acct/46161", + "x-frame-options": "DENY" + } +output_text: + description: The raw text output + returned: always + type: str + sample: "{\\n \\\"id\\\": 12345,\\n \\\"key\\\": {\\n \\\"kty\\\": \\\"RSA\\\",\\n ..." +output_json: + description: The output parsed as JSON + returned: if output can be parsed as JSON + type: dict + sample: + - id: 12345 + - key: + - kty: RSA + - ... +''' + +from ansible_collections.community.crypto.plugins.module_utils.acme import ( + ModuleFailException, + ACMEAccount, + handle_standard_module_arguments, + get_default_argspec, +) + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native, to_bytes + +import json + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + url=dict(type='str'), + method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'), + content=dict(type='str'), + fail_on_acme_error=dict(type='bool', default=True), + )) + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ), + required_if=( + ['method', 'get', ['url']], + ['method', 'post', ['url', 'content']], + ['method', 'get', ['account_key_src', 'account_key_content'], True], + ['method', 'post', ['account_key_src', 'account_key_content'], True], + ), + ) + handle_standard_module_arguments(module) + + result = dict() + changed = False + try: + # Get hold of ACMEAccount object (includes directory) + account = ACMEAccount(module) + method = module.params['method'] + result['directory'] = account.directory.directory + # Do we have to do more requests? + if method != 'directory-only': + url = module.params['url'] + fail_on_acme_error = module.params['fail_on_acme_error'] + # Do request + if method == 'get': + data, info = account.get_request(url, parse_json_result=False, fail_on_error=False) + elif method == 'post': + changed = True # only POSTs can change + data, info = account.send_signed_request(url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False) + # Update results + result.update(dict( + headers=info, + output_text=to_native(data), + )) + # See if we can parse the result as JSON + try: + result['output_json'] = json.loads(data) + except Exception as dummy: + pass + # Fail if error was returned + if fail_on_acme_error and info['status'] >= 400: + raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], data)) + # Done! + module.exit_json(changed=changed, **result) + except ModuleFailException as e: + e.do_fail(module, **result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/certificate_complete_chain.py b/plugins/modules/certificate_complete_chain.py new file mode 100644 index 00000000..72dec773 --- /dev/null +++ b/plugins/modules/certificate_complete_chain.py @@ -0,0 +1,349 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2018, Felix Fontein +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: certificate_complete_chain +author: "Felix Fontein (@felixfontein)" +short_description: Complete certificate chain given a set of untrusted and root certificates +description: + - "This module completes a given chain of certificates in PEM format by finding + intermediate certificates from a given set of certificates, until it finds a root + certificate in another given set of certificates." + - "This can for example be used to find the root certificate for a certificate chain + returned by M(acme_certificate)." + - "Note that this module does I(not) check for validity of the chains. It only + checks that issuer and subject match, and that the signature is correct. It + ignores validity dates and key usage completely. If you need to verify that a + generated chain is valid, please use C(openssl verify ...)." +requirements: + - "cryptography >= 1.5" +options: + input_chain: + description: + - A concatenated set of certificates in PEM format forming a chain. + - The module will try to complete this chain. + type: str + required: yes + root_certificates: + description: + - "A list of filenames or directories." + - "A filename is assumed to point to a file containing one or more certificates + in PEM format. All certificates in this file will be added to the set of + root certificates." + - "If a directory name is given, all files in the directory and its + subdirectories will be scanned and tried to be parsed as concatenated + certificates in PEM format." + - "Symbolic links will be followed." + type: list + elements: path + required: yes + intermediate_certificates: + description: + - "A list of filenames or directories." + - "A filename is assumed to point to a file containing one or more certificates + in PEM format. All certificates in this file will be added to the set of + root certificates." + - "If a directory name is given, all files in the directory and its + subdirectories will be scanned and tried to be parsed as concatenated + certificates in PEM format." + - "Symbolic links will be followed." + type: list + elements: path + default: [] +''' + + +EXAMPLES = ''' +# Given a leaf certificate for www.ansible.com and one or more intermediate +# certificates, finds the associated root certificate. +- name: Find root certificate + certificate_complete_chain: + input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com-fullchain.pem') }}" + root_certificates: + - /etc/ca-certificates/ + register: www_ansible_com +- name: Write root certificate to disk + copy: + dest: /etc/ssl/csr/www.ansible.com-root.pem + content: "{{ www_ansible_com.root }}" + +# Given a leaf certificate for www.ansible.com, and a list of intermediate +# certificates, finds the associated root certificate. +- name: Find root certificate + certificate_complete_chain: + input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.pem') }}" + intermediate_certificates: + - /etc/ssl/csr/www.ansible.com-chain.pem + root_certificates: + - /etc/ca-certificates/ + register: www_ansible_com +- name: Write complete chain to disk + copy: + dest: /etc/ssl/csr/www.ansible.com-completechain.pem + content: "{{ ''.join(www_ansible_com.complete_chain) }}" +- name: Write root chain (intermediates and root) to disk + copy: + dest: /etc/ssl/csr/www.ansible.com-rootchain.pem + content: "{{ ''.join(www_ansible_com.chain) }}" +''' + + +RETURN = ''' +root: + description: + - "The root certificate in PEM format." + returned: success + type: str +chain: + description: + - "The chain added to the given input chain. Includes the root certificate." + - "Returned as a list of PEM certificates." + returned: success + type: list + elements: str +complete_chain: + description: + - "The completed chain, including leaf, all intermediates, and root." + - "Returned as a list of PEM certificates." + returned: success + type: list + elements: str +''' + +import os +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_bytes + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization + import cryptography.hazmat.primitives.asymmetric.rsa + import cryptography.hazmat.primitives.asymmetric.ec + import cryptography.hazmat.primitives.asymmetric.padding + import cryptography.hazmat.primitives.hashes + import cryptography.hazmat.primitives.asymmetric.utils + import cryptography.x509 + import cryptography.x509.oid + from distutils.version import LooseVersion + HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5')) + _cryptography_backend = cryptography.hazmat.backends.default_backend() +except ImportError as dummy: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + HAS_CRYPTOGRAPHY = False + + +class Certificate(object): + ''' + Stores PEM with parsed certificate. + ''' + def __init__(self, pem, cert): + if not (pem.endswith('\n') or pem.endswith('\r')): + pem = pem + '\n' + self.pem = pem + self.cert = cert + + +def is_parent(module, cert, potential_parent): + ''' + Tests whether the given certificate has been issued by the potential parent certificate. + ''' + # Check issuer + if cert.cert.issuer != potential_parent.cert.subject: + return False + # Check signature + public_key = potential_parent.cert.public_key() + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): + public_key.verify( + cert.cert.signature, + cert.cert.tbs_certificate_bytes, + cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(), + cert.cert.signature_hash_algorithm + ) + elif isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): + public_key.verify( + cert.cert.signature, + cert.cert.tbs_certificate_bytes, + cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cert.cert.signature_hash_algorithm), + ) + else: + # Unknown public key type + module.warn('Unknown public key type "{0}"'.format(public_key)) + return False + return True + except cryptography.exceptions.InvalidSignature as dummy: + return False + except Exception as e: + module.fail_json(msg='Unknown error on signature validation: {0}'.format(e)) + + +def parse_PEM_list(module, text, source, fail_on_error=True): + ''' + Parse concatenated PEM certificates. Return list of ``Certificate`` objects. + ''' + result = [] + lines = text.splitlines(True) + current = None + for line in lines: + if line.strip(): + if line.startswith('-----BEGIN '): + current = [line] + elif current is not None: + current.append(line) + if line.startswith('-----END '): + cert_pem = ''.join(current) + current = None + # Try to load PEM certificate + try: + cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend) + result.append(Certificate(cert_pem, cert)) + except Exception as e: + msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e) + if fail_on_error: + module.fail_json(msg=msg) + else: + module.warn(msg) + return result + + +def load_PEM_list(module, path, fail_on_error=True): + ''' + Load concatenated PEM certificates from file. Return list of ``Certificate`` objects. + ''' + try: + with open(path, "rb") as f: + return parse_PEM_list(module, f.read().decode('utf-8'), source=path, fail_on_error=fail_on_error) + except Exception as e: + msg = 'Cannot read certificate file {0}: {1}'.format(path, e) + if fail_on_error: + module.fail_json(msg=msg) + else: + module.warn(msg) + return [] + + +class CertificateSet(object): + ''' + Stores a set of certificates. Allows to search for parent (issuer of a certificate). + ''' + + def __init__(self, module): + self.module = module + self.certificates = set() + self.certificate_by_issuer = dict() + + def _load_file(self, path): + certs = load_PEM_list(self.module, path, fail_on_error=False) + for cert in certs: + self.certificates.add(cert) + self.certificate_by_issuer[cert.cert.subject] = cert + + def load(self, path): + ''' + Load lists of PEM certificates from a file or a directory. + ''' + b_path = to_bytes(path, errors='surrogate_or_strict') + if os.path.isdir(b_path): + for directory, dummy, files in os.walk(b_path, followlinks=True): + for file in files: + self._load_file(os.path.join(directory, file)) + else: + self._load_file(b_path) + + def find_parent(self, cert): + ''' + Search for the parent (issuer) of a certificate. Return ``None`` if none was found. + ''' + potential_parent = self.certificate_by_issuer.get(cert.cert.issuer) + if potential_parent is not None: + if is_parent(self.module, cert, potential_parent): + return potential_parent + return None + + +def format_cert(cert): + ''' + Return human readable representation of certificate for error messages. + ''' + return str(cert.cert) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + input_chain=dict(type='str', required=True), + root_certificates=dict(type='list', required=True, elements='path'), + intermediate_certificates=dict(type='list', default=[], elements='path'), + ), + supports_check_mode=True, + ) + + if not HAS_CRYPTOGRAPHY: + module.fail_json(msg=missing_required_lib('cryptography >= 1.5'), exception=CRYPTOGRAPHY_IMP_ERR) + + # Load chain + chain = parse_PEM_list(module, module.params['input_chain'], source='input chain') + if len(chain) == 0: + module.fail_json(msg='Input chain must contain at least one certificate') + + # Check chain + for i, parent in enumerate(chain): + if i > 0: + if not is_parent(module, chain[i - 1], parent): + module.fail_json(msg=('Cannot verify input chain: certificate #{2}: {3} is not issuer ' + + 'of certificate #{0}: {1}').format(i, format_cert(chain[i - 1]), i + 1, format_cert(parent))) + + # Load intermediate certificates + intermediates = CertificateSet(module) + for path in module.params['intermediate_certificates']: + intermediates.load(path) + + # Load root certificates + roots = CertificateSet(module) + for path in module.params['root_certificates']: + roots.load(path) + + # Try to complete chain + current = chain[-1] + completed = [] + while current: + root = roots.find_parent(current) + if root: + completed.append(root) + break + intermediate = intermediates.find_parent(current) + if intermediate: + completed.append(intermediate) + current = intermediate + else: + module.fail_json(msg='Cannot complete chain. Stuck at certificate {0}'.format(format_cert(current))) + + # Return results + complete_chain = chain + completed + module.exit_json( + changed=False, + root=complete_chain[-1].pem, + chain=[cert.pem for cert in completed], + complete_chain=[cert.pem for cert in complete_chain], + ) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ecs_certificate.py b/plugins/modules/ecs_certificate.py new file mode 100644 index 00000000..925204f9 --- /dev/null +++ b/plugins/modules/ecs_certificate.py @@ -0,0 +1,952 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c), Entrust Datacard Corporation, 2019 +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ecs_certificate +author: + - Chris Trufan (@ctrufan) +short_description: Request SSL/TLS certificates with the Entrust Certificate Services (ECS) API +description: + - Create, reissue, and renew certificates with the Entrust Certificate Services (ECS) API. + - Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API. + - In order to request a certificate, the domain and organization used in the certificate signing request must be already + validated in the ECS system. It is I(not) the responsibility of this module to perform those steps. +notes: + - C(path) must be specified as the output location of the certificate. +requirements: + - cryptography >= 1.6 +options: + backup: + description: + - Whether a backup should be made for the certificate in I(path). + type: bool + default: false + force: + description: + - If force is used, a certificate is requested regardless of whether I(path) points to an existing valid certificate. + - If C(request_type=renew), a forced renew will fail if the certificate being renewed has been issued within the past 30 days, regardless of the + value of I(remaining_days) or the return value of I(cert_days) - the ECS API does not support the "renew" operation for certificates that are not + at least 30 days old. + type: bool + default: false + path: + description: + - The destination path for the generated certificate as a PEM encoded cert. + - If the certificate at this location is not an Entrust issued certificate, a new certificate will always be requested even if the current + certificate is technically valid. + - If there is already an Entrust certificate at this location, whether it is replaced is depends on the I(remaining_days) calculation. + - If an existing certificate is being replaced (see I(remaining_days), I(force), and I(tracking_id)), whether a new certificate is requested + or the existing certificate is renewed or reissued is based on I(request_type). + type: path + required: true + full_chain_path: + description: + - The destination path for the full certificate chain of the certificate, intermediates, and roots. + type: path + csr: + description: + - Base-64 encoded Certificate Signing Request (CSR). I(csr) is accepted with or without PEM formatting around the Base-64 string. + - If no I(csr) is provided when C(request_type=reissue) or C(request_type=renew), the certificate will be generated with the same public key as + the certificate being renewed or reissued. + - If I(subject_alt_name) is specified, it will override the subject alternate names in the CSR. + - If I(eku) is specified, it will override the extended key usage in the CSR. + - If I(ou) is specified, it will override the organizational units "ou=" present in the subject distinguished name of the CSR, if any. + - The organization "O=" field from the CSR will not be used. It will be replaced in the issued certificate by I(org) if present, and if not present, + the organization tied to I(client_id). + type: str + tracking_id: + description: + - The tracking ID of the certificate to reissue or renew. + - I(tracking_id) is invalid if C(request_type=new) or C(request_type=validate_only). + - If there is a certificate present in I(path) and it is an ECS certificate, I(tracking_id) will be ignored. + - If there is no certificate present in I(path) or there is but it is from another provider, the certificate represented by I(tracking_id) will + be renewed or reissued and saved to I(path). + - If there is no certificate present in I(path) and the I(force) and I(remaining_days) parameters do not indicate a new certificate is needed, + the certificate referenced by I(tracking_id) certificate will be saved to I(path). + - This can be used when a known certificate is not currently present on a server, but you want to renew or reissue it to be managed by an ansible + playbook. For example, if you specify C(request_type=renew), I(tracking_id) of an issued certificate, and I(path) to a file that does not exist, + the first run of a task will download the certificate specified by I(tracking_id) (assuming it is still valid). Future runs of the task will + (if applicable - see I(force) and I(remaining_days)) renew the certificate now present in I(path). + type: int + remaining_days: + description: + - The number of days the certificate must have left being valid. If C(cert_days < remaining_days) then a new certificate will be + obtained using I(request_type). + - If C(request_type=renew), a renewal will fail if the certificate being renewed has been issued within the past 30 days, so do not set a + I(remaining_days) value that is within 30 days of the full lifetime of the certificate being acted upon. (e.g. if you are requesting Certificates + with a 90 day lifetime, do not set remaining_days to a value C(60) or higher). + - The I(force) option may be used to ensure that a new certificate is always obtained. + type: int + default: 30 + request_type: + description: + - The operation performed if I(tracking_id) references a valid certificate to reissue, or there is already a certificate present in I(path) but + either I(force) is specified or C(cert_days < remaining_days). + - Specifying C(request_type=validate_only) means the request will be validated against the ECS API, but no certificate will be issued. + - Specifying C(request_type=new) means a certificate request will always be submitted and a new certificate issued. + - Specifying C(request_type=renew) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be renewed. + If there is no certificate to renew, a new certificate is requested. + - Specifying C(request_type=reissue) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be + reissued. + If there is no certificate to reissue, a new certificate is requested. + - If a certificate was issued within the past 30 days, the 'renew' operation is not a valid operation and will fail. + - Note that C(reissue) is an operation that will result in the revocation of the certificate that is reissued, be cautious with it's use. + - I(check_mode) is only supported if C(request_type=new) + - For example, setting C(request_type=renew) and C(remaining_days=30) and pointing to the same certificate on multiple playbook runs means that on + the first run new certificate will be requested. It will then be left along on future runs until it is within 30 days of expiry, then the + ECS "renew" operation will be performed. + type: str + choices: [ 'new', 'renew', 'reissue', 'validate_only'] + default: new + cert_type: + description: + - Specify the type of certificate requested. + - If a certificate is being reissued or renewed, this parameter is ignored, and the C(cert_type) of the initial certificate is used. + type: str + choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CODE_SIGNING', 'EV_CODE_SIGNING', + 'CDS_INDIVIDUAL', 'CDS_GROUP', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ] + subject_alt_name: + description: + - The subject alternative name identifiers, as an array of values (applies to I(cert_type) with a value of C(STANDARD_SSL), C(ADVANTAGE_SSL), + C(UC_SSL), C(EV_SSL), C(WILDCARD_SSL), C(PRIVATE_SSL), and C(PD_SSL)). + - If you are requesting a new SSL certificate, and you pass a I(subject_alt_name) parameter, any SAN names in the CSR are ignored. + If no subjectAltName parameter is passed, the SAN names in the CSR are used. + - See I(request_type) to understand more about SANs during reissues and renewals. + - In the case of certificates of type C(STANDARD_SSL) certificates, if the CN of the certificate is . only the www.. value + is accepted. If the CN of the certificate is www.. only the . value is accepted. + type: list + elements: str + eku: + description: + - If specified, overrides the key usage in the I(csr). + type: str + choices: [ SERVER_AUTH, CLIENT_AUTH, SERVER_AND_CLIENT_AUTH ] + ct_log: + description: + - In compliance with browser requirements, this certificate may be posted to the Certificate Transparency (CT) logs. This is a best practice + technique that helps domain owners monitor certificates issued to their domains. Note that not all certificates are eligible for CT logging. + - If I(ct_log) is not specified, the certificate uses the account default. + - If I(ct_log) is specified and the account settings allow it, I(ct_log) overrides the account default. + - If I(ct_log) is set to C(false), but the account settings are set to "always log", the certificate generation will fail. + type: bool + client_id: + description: + - The client ID to submit the Certificate Signing Request under. + - If no client ID is specified, the certificate will be submitted under the primary client with ID of 1. + - When using a client other than the primary client, the I(org) parameter cannot be specified. + - The issued certificate will have an organization value in the subject distinguished name represented by the client. + type: int + default: 1 + org: + description: + - Organization "O=" to include in the certificate. + - If I(org) is not specified, the organization from the client represented by I(client_id) is used. + - Unless the I(cert_type) is C(PD_SSL), this field may not be specified if the value of I(client_id) is not "1" (the primary client). + non-primary clients, certificates may only be issued with the organization of that client. + type: str + ou: + description: + - Organizational unit "OU=" to include in the certificate. + - I(ou) behavior is dependent on whether organizational units are enabled for your account. If organizational unit support is disabled for your + account, organizational units from the I(csr) and the I(ou) parameter are ignored. + - If both I(csr) and I(ou) are specified, the value in I(ou) will override the OU fields present in the subject distinguished name in the I(csr) + - If neither I(csr) nor I(ou) are specified for a renew or reissue operation, the OU fields in the initial certificate are reused. + - An invalid OU from I(csr) is ignored, but any invalid organizational units in I(ou) will result in an error indicating "Unapproved OU". The I(ou) + parameter can be used to force failure if an unapproved organizational unit is provided. + - A maximum of one OU may be specified for current products. Multiple OUs are reserved for future products. + type: list + elements: str + end_user_key_storage_agreement: + description: + - The end user of the Code Signing certificate must generate and store the private key for this request on cryptographically secure + hardware to be compliant with the Entrust CSP and Subscription agreement. If requesting a certificate of type C(CODE_SIGNING) or + C(EV_CODE_SIGNING), you must set I(end_user_key_storage_agreement) to true if and only if you acknowledge that you will inform the user of this + requirement. + - Applicable only to I(cert_type) of values C(CODE_SIGNING) and C(EV_CODE_SIGNING). + type: bool + tracking_info: + description: Free form tracking information to attach to the record for the certificate. + type: str + requester_name: + description: The requester name to associate with certificate tracking information. + type: str + required: true + requester_email: + description: The requester email to associate with certificate tracking information and receive delivery and expiry notices for the certificate. + type: str + required: true + requester_phone: + description: The requester phone number to associate with certificate tracking information. + type: str + required: true + additional_emails: + description: A list of additional email addresses to receive the delivery notice and expiry notification for the certificate. + type: list + elements: str + custom_fields: + description: + - Mapping of custom fields to associate with the certificate request and certificate. + - Only supported if custom fields are enabled for your account. + - Each custom field specified must be a custom field you have defined for your account. + type: dict + suboptions: + text1: + description: Custom text field (maximum 500 characters) + type: str + text2: + description: Custom text field (maximum 500 characters) + type: str + text3: + description: Custom text field (maximum 500 characters) + type: str + text4: + description: Custom text field (maximum 500 characters) + type: str + text5: + description: Custom text field (maximum 500 characters) + type: str + text6: + description: Custom text field (maximum 500 characters) + type: str + text7: + description: Custom text field (maximum 500 characters) + type: str + text8: + description: Custom text field (maximum 500 characters) + type: str + text9: + description: Custom text field (maximum 500 characters) + type: str + text10: + description: Custom text field (maximum 500 characters) + type: str + text11: + description: Custom text field (maximum 500 characters) + type: str + text12: + description: Custom text field (maximum 500 characters) + type: str + text13: + description: Custom text field (maximum 500 characters) + type: str + text14: + description: Custom text field (maximum 500 characters) + type: str + text15: + description: Custom text field (maximum 500 characters) + type: str + number1: + description: Custom number field. + type: float + number2: + description: Custom number field. + type: float + number3: + description: Custom number field. + type: float + number4: + description: Custom number field. + type: float + number5: + description: Custom number field. + type: float + date1: + description: Custom date field. + type: str + date2: + description: Custom date field. + type: str + date3: + description: Custom date field. + type: str + date4: + description: Custom date field. + type: str + date5: + description: Custom date field. + type: str + email1: + description: Custom email field. + type: str + email2: + description: Custom email field. + type: str + email3: + description: Custom email field. + type: str + email4: + description: Custom email field. + type: str + email5: + description: Custom email field. + type: str + dropdown1: + description: Custom dropdown field. + type: str + dropdown2: + description: Custom dropdown field. + type: str + dropdown3: + description: Custom dropdown field. + type: str + dropdown4: + description: Custom dropdown field. + type: str + dropdown5: + description: Custom dropdown field. + type: str + cert_expiry: + description: + - The date the certificate should be set to expire, in RFC3339 compliant date or date-time format. For example, + C(2020-02-23), C(2020-02-23T15:00:00.05Z). + - I(cert_expiry) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue), + I(cert_expiry) will be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial + certificate. + - A reissued certificate will always have the same expiry as the original certificate. + - Note that only the date (day, month, year) is supported for specifying the expiry date. If you choose to specify an expiry time with the expiry + date, the time will be adjusted to Eastern Standard Time (EST). This could have the unintended effect of moving your expiry date to the previous + day. + - Applies only to accounts with a pooling inventory model. + - Only one of I(cert_expiry) or I(cert_lifetime) may be specified. + type: str + cert_lifetime: + description: + - The lifetime of the certificate. + - Applies to all certificates for accounts with a non-pooling inventory model. + - I(cert_lifetime) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue), I(cert_lifetime) will + be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial certificate. + - Applies to certificates of I(cert_type)=C(CDS_INDIVIDUAL, CDS_GROUP, CDS_ENT_LITE, CDS_ENT_PRO, SMIME_ENT) for accounts with a pooling inventory + model. + - C(P1Y) is a certificate with a 1 year lifetime. + - C(P2Y) is a certificate with a 2 year lifetime. + - C(P3Y) is a certificate with a 3 year lifetime. + - Only one of I(cert_expiry) or I(cert_lifetime) may be specified. + type: str + choices: [ P1Y, P2Y, P3Y ] +seealso: + - module: openssl_privatekey + description: Can be used to create private keys (both for certificates and accounts). + - module: openssl_csr + description: Can be used to create a Certificate Signing Request (CSR). +extends_documentation_fragment: +- community.crypto.ecs_credential + +''' + +EXAMPLES = r''' +- name: Request a new certificate from Entrust with bare minimum parameters. + Will request a new certificate if current one is valid but within 30 + days of expiry. If replacing an existing file in path, will back it up. + ecs_certificate: + backup: true + path: /etc/ssl/crt/ansible.com.crt + full_chain_path: /etc/ssl/crt/ansible.com.chain.crt + csr: /etc/ssl/csr/ansible.com.csr + cert_type: EV_SSL + requester_name: Jo Doe + requester_email: jdoe@ansible.com + requester_phone: 555-555-5555 + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: If there is no certificate present in path, request a new certificate + of type EV_SSL. Otherwise, if there is an Entrust managed certificate + in path and it is within 63 days of expiration, request a renew of that + certificate. + ecs_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr: /etc/ssl/csr/ansible.com.csr + cert_type: EV_SSL + cert_expiry: '2020-08-20' + request_type: renew + remaining_days: 63 + requester_name: Jo Doe + requester_email: jdoe@ansible.com + requester_phone: 555-555-5555 + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: If there is no certificate present in path, download certificate + specified by tracking_id if it is still valid. Otherwise, if the + certificate is within 79 days of expiration, request a renew of that + certificate and save it in path. This can be used to "migrate" a + certificate to be Ansible managed. + ecs_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr: /etc/ssl/csr/ansible.com.csr + tracking_id: 2378915 + request_type: renew + remaining_days: 79 + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: Force a reissue of the certificate specified by tracking_id. + ecs_certificate: + path: /etc/ssl/crt/ansible.com.crt + force: true + tracking_id: 2378915 + request_type: reissue + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: Request a new certificate with an alternative client. Note that the + issued certificate will have it's Subject Distinguished Name use the + organization details associated with that client, rather than what is + in the CSR. + ecs_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr: /etc/ssl/csr/ansible.com.csr + client_id: 2 + requester_name: Jo Doe + requester_email: jdoe@ansible.com + requester_phone: 555-555-5555 + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: Request a new certificate with a number of CSR parameters overridden + and tracking information + ecs_certificate: + path: /etc/ssl/crt/ansible.com.crt + full_chain_path: /etc/ssl/crt/ansible.com.chain.crt + csr: /etc/ssl/csr/ansible.com.csr + subject_alt_name: + - ansible.testcertificates.com + - www.testcertificates.com + eku: SERVER_AND_CLIENT_AUTH + ct_log: true + org: Test Organization Inc. + ou: + - Administration + tracking_info: "Submitted via Ansible" + additional_emails: + - itsupport@testcertificates.com + - jsmith@ansible.com + custom_fields: + text1: Admin + text2: Invoice 25 + number1: 342 + date1: '2018-01-01' + email1: sales@ansible.testcertificates.com + dropdown1: red + cert_expiry: '2020-08-15' + requester_name: Jo Doe + requester_email: jdoe@ansible.com + requester_phone: 555-555-5555 + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +''' + +RETURN = ''' +filename: + description: The destination path for the generated certificate. + returned: changed or success + type: str + sample: /etc/ssl/crt/www.ansible.com.crt +backup_file: + description: Name of backup file created for the certificate. + returned: changed and if I(backup) is C(true) + type: str + sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~ +backup_full_chain_file: + description: Name of the backup file created for the certificate chain. + returned: changed and if I(backup) is C(true) and I(full_chain_path) is set. + type: str + sample: /path/to/ca.chain.crt.2019-03-09@11:22~ +tracking_id: + description: The tracking ID to reference and track the certificate in ECS. + returned: success + type: int + sample: 380079 +serial_number: + description: The serial number of the issued certificate. + returned: success + type: int + sample: 1235262234164342 +cert_days: + description: The number of days the certificate remains valid. + returned: success + type: int + sample: 253 +cert_status: + description: + - The certificate status in ECS. + - 'Current possible values (which may be expanded in the future) are: C(ACTIVE), C(APPROVED), C(DEACTIVATED), C(DECLINED), C(EXPIRED), C(NA), + C(PENDING), C(PENDING_QUORUM), C(READY), C(REISSUED), C(REISSUING), C(RENEWED), C(RENEWING), C(REVOKED), C(SUSPENDED)' + returned: success + type: str + sample: ACTIVE +cert_details: + description: + - The full response JSON from the Get Certificate call of the ECS API. + - 'While the response contents are guaranteed to be forwards compatible with new ECS API releases, Entrust recommends that you do not make any + playbooks take actions based on the content of this field. However it may be useful for debugging, logging, or auditing purposes.' + returned: success + type: dict + +''' + +from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ( + ecs_client_argument_spec, + ECSClient, + RestOperationException, + SessionConfigurationException, +) + +import datetime +import json +import os +import re +import time +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native, to_bytes + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' + + +def validate_cert_expiry(cert_expiry): + search_string_partial = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])\Z') + search_string_full = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):' + r'([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))\Z') + if search_string_partial.match(cert_expiry) or search_string_full.match(cert_expiry): + return True + return False + + +def calculate_cert_days(expires_after): + cert_days = 0 + if expires_after: + expires_after_datetime = datetime.datetime.strptime(expires_after, '%Y-%m-%dT%H:%M:%SZ') + cert_days = (expires_after_datetime - datetime.datetime.now()).days + return cert_days + + +# Populate the value of body[dict_param_name] with the JSON equivalent of +# module parameter of param_name if that parameter is present, otherwise leave field +# out of resulting dict +def convert_module_param_to_json_bool(module, dict_param_name, param_name): + body = {} + if module.params[param_name] is not None: + if module.params[param_name]: + body[dict_param_name] = 'true' + else: + body[dict_param_name] = 'false' + return body + + +class EcsCertificate(object): + ''' + Entrust Certificate Services certificate class. + ''' + + def __init__(self, module): + self.path = module.params['path'] + self.full_chain_path = module.params['full_chain_path'] + self.force = module.params['force'] + self.backup = module.params['backup'] + self.request_type = module.params['request_type'] + self.csr = module.params['csr'] + + # All return values + self.changed = False + self.filename = None + self.tracking_id = None + self.cert_status = None + self.serial_number = None + self.cert_days = None + self.cert_details = None + self.backup_file = None + self.backup_full_chain_file = None + + self.cert = None + self.ecs_client = None + if self.path and os.path.exists(self.path): + try: + self.cert = crypto_utils.load_certificate(self.path, backend='cryptography') + except Exception as dummy: + self.cert = None + # Instantiate the ECS client and then try a no-op connection to verify credentials are valid + try: + self.ecs_client = ECSClient( + entrust_api_user=module.params['entrust_api_user'], + entrust_api_key=module.params['entrust_api_key'], + entrust_api_cert=module.params['entrust_api_client_cert_path'], + entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'], + entrust_api_specification_path=module.params['entrust_api_specification_path'] + ) + except SessionConfigurationException as e: + module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e))) + try: + self.ecs_client.GetAppVersion() + except RestOperationException as e: + module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message))) + + # Conversion of the fields that go into the 'tracking' parameter of the request object + def convert_tracking_params(self, module): + body = {} + tracking = {} + if module.params['requester_name']: + tracking['requesterName'] = module.params['requester_name'] + if module.params['requester_email']: + tracking['requesterEmail'] = module.params['requester_email'] + if module.params['requester_phone']: + tracking['requesterPhone'] = module.params['requester_phone'] + if module.params['tracking_info']: + tracking['trackingInfo'] = module.params['tracking_info'] + if module.params['custom_fields']: + # Omit custom fields from submitted dict if not present, instead of submitting them with value of 'null' + # The ECS API does technically accept null without error, but it complicates debugging user escalations and is unnecessary bandwidth. + custom_fields = {} + for k, v in module.params['custom_fields'].items(): + if v is not None: + custom_fields[k] = v + tracking['customFields'] = custom_fields + if module.params['additional_emails']: + tracking['additionalEmails'] = module.params['additional_emails'] + body['tracking'] = tracking + return body + + def convert_cert_subject_params(self, module): + body = {} + if module.params['subject_alt_name']: + body['subjectAltName'] = module.params['subject_alt_name'] + if module.params['org']: + body['org'] = module.params['org'] + if module.params['ou']: + body['ou'] = module.params['ou'] + return body + + def convert_general_params(self, module): + body = {} + if module.params['eku']: + body['eku'] = module.params['eku'] + if self.request_type == 'new': + body['certType'] = module.params['cert_type'] + body['clientId'] = module.params['client_id'] + body.update(convert_module_param_to_json_bool(module, 'ctLog', 'ct_log')) + body.update(convert_module_param_to_json_bool(module, 'endUserKeyStorageAgreement', 'end_user_key_storage_agreement')) + return body + + def convert_expiry_params(self, module): + body = {} + if module.params['cert_lifetime']: + body['certLifetime'] = module.params['cert_lifetime'] + elif module.params['cert_expiry']: + body['certExpiryDate'] = module.params['cert_expiry'] + # If neither cerTLifetime or certExpiryDate was specified and the request type is new, default to 365 days + elif self.request_type != 'reissue': + gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())) + expiry = gmt_now + datetime.timedelta(days=365) + body['certExpiryDate'] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") + return body + + def set_tracking_id_by_serial_number(self, module): + try: + # Use serial_number to identify if certificate is an Entrust Certificate + # with an associated tracking ID + serial_number = "{0:X}".format(self.cert.serial_number) + cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {}) + if len(cert_results) == 1: + self.tracking_id = cert_results[0].get('trackingId') + except RestOperationException as dummy: + # If we fail to find a cert by serial number, that's fine, we just don't set self.tracking_id + return + + def set_cert_details(self, module): + try: + self.cert_details = self.ecs_client.GetCertificate(trackingId=self.tracking_id) + self.cert_status = self.cert_details.get('status') + self.serial_number = self.cert_details.get('serialNumber') + self.cert_days = calculate_cert_days(self.cert_details.get('expiresAfter')) + except RestOperationException as e: + module.fail_json('Failed to get details of certificate with tracking_id="{0}", Error: '.format(self.tracking_id), to_native(e.message)) + + def check(self, module): + if self.cert: + # We will only treat a certificate as valid if it is found as a managed entrust cert. + # We will only set updated tracking ID based on certificate in "path" if it is managed by entrust. + self.set_tracking_id_by_serial_number(module) + + if module.params['tracking_id'] and self.tracking_id and module.params['tracking_id'] != self.tracking_id: + module.warn('tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with ' + 'tracking_id of "{2}".'.format(module.params['tracking_id'], self.path, self.tracking_id)) + + # If we did not end up setting tracking_id based on existing cert, get from module params + if not self.tracking_id: + self.tracking_id = module.params['tracking_id'] + + if not self.tracking_id: + return False + + self.set_cert_details(module) + + if self.cert_status == 'EXPIRED' or self.cert_status == 'SUSPENDED' or self.cert_status == 'REVOKED': + return False + if self.cert_days < module.params['remaining_days']: + return False + + return True + + def request_cert(self, module): + if not self.check(module) or self.force: + body = {} + + # Read the CSR contents + if self.csr and os.path.exists(self.csr): + with open(self.csr, 'r') as csr_file: + body['csr'] = csr_file.read() + + # Check if the path is already a cert + # tracking_id may be set as a parameter or by get_cert_details if an entrust cert is in 'path'. If tracking ID is null + # We will be performing a reissue operation. + if self.request_type != 'new' and not self.tracking_id: + module.warn('No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task' + 'run. Future playbook runs that point to the pathination file in {1} will use request_type={2}' + .format(self.path, self.path, self.request_type)) + self.request_type = 'new' + elif self.request_type == 'new' and self.tracking_id: + module.warn('Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a' + 'reissue or renew') + # Use cases where request type is new and no existing certificate, or where request type is reissue/renew and a valid + # existing certificate is found, do not need warnings. + + body.update(self.convert_tracking_params(module)) + body.update(self.convert_cert_subject_params(module)) + body.update(self.convert_general_params(module)) + body.update(self.convert_expiry_params(module)) + + if not module.check_mode: + try: + if self.request_type == 'validate_only': + body['validateOnly'] = 'true' + result = self.ecs_client.NewCertRequest(Body=body) + if self.request_type == 'new': + result = self.ecs_client.NewCertRequest(Body=body) + elif self.request_type == 'renew': + result = self.ecs_client.RenewCertRequest(trackingId=self.tracking_id, Body=body) + elif self.request_type == 'reissue': + result = self.ecs_client.ReissueCertRequest(trackingId=self.tracking_id, Body=body) + self.tracking_id = result.get('trackingId') + self.set_cert_details(module) + except RestOperationException as e: + module.fail_json(msg='Failed to request new certificate from Entrust (ECS) {0}'.format(e.message)) + + if self.request_type != 'validate_only': + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) + if self.full_chain_path and self.cert_details.get('chainCerts'): + if self.backup: + self.backup_full_chain_file = module.backup_local(self.full_chain_path) + chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' + crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) + self.changed = True + # If there is no certificate present in path but a tracking ID was specified, save it to disk + elif not os.path.exists(self.path) and self.tracking_id: + if not module.check_mode: + crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) + if self.full_chain_path and self.cert_details.get('chainCerts'): + chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' + crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) + self.changed = True + + def dump(self): + result = { + 'changed': self.changed, + 'filename': self.path, + 'tracking_id': self.tracking_id, + 'cert_status': self.cert_status, + 'serial_number': self.serial_number, + 'cert_days': self.cert_days, + 'cert_details': self.cert_details, + } + if self.backup_file: + result['backup_file'] = self.backup_file + result['backup_full_chain_file'] = self.backup_full_chain_file + return result + + +def custom_fields_spec(): + return dict( + text1=dict(type='str'), + text2=dict(type='str'), + text3=dict(type='str'), + text4=dict(type='str'), + text5=dict(type='str'), + text6=dict(type='str'), + text7=dict(type='str'), + text8=dict(type='str'), + text9=dict(type='str'), + text10=dict(type='str'), + text11=dict(type='str'), + text12=dict(type='str'), + text13=dict(type='str'), + text14=dict(type='str'), + text15=dict(type='str'), + number1=dict(type='float'), + number2=dict(type='float'), + number3=dict(type='float'), + number4=dict(type='float'), + number5=dict(type='float'), + date1=dict(type='str'), + date2=dict(type='str'), + date3=dict(type='str'), + date4=dict(type='str'), + date5=dict(type='str'), + email1=dict(type='str'), + email2=dict(type='str'), + email3=dict(type='str'), + email4=dict(type='str'), + email5=dict(type='str'), + dropdown1=dict(type='str'), + dropdown2=dict(type='str'), + dropdown3=dict(type='str'), + dropdown4=dict(type='str'), + dropdown5=dict(type='str'), + ) + + +def ecs_certificate_argument_spec(): + return dict( + backup=dict(type='bool', default=False), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + full_chain_path=dict(type='path'), + tracking_id=dict(type='int'), + remaining_days=dict(type='int', default=30), + request_type=dict(type='str', default='new', choices=['new', 'renew', 'reissue', 'validate_only']), + cert_type=dict(type='str', choices=['STANDARD_SSL', + 'ADVANTAGE_SSL', + 'UC_SSL', + 'EV_SSL', + 'WILDCARD_SSL', + 'PRIVATE_SSL', + 'PD_SSL', + 'CODE_SIGNING', + 'EV_CODE_SIGNING', + 'CDS_INDIVIDUAL', + 'CDS_GROUP', + 'CDS_ENT_LITE', + 'CDS_ENT_PRO', + 'SMIME_ENT', + ]), + csr=dict(type='str'), + subject_alt_name=dict(type='list', elements='str'), + eku=dict(type='str', choices=['SERVER_AUTH', 'CLIENT_AUTH', 'SERVER_AND_CLIENT_AUTH']), + ct_log=dict(type='bool'), + client_id=dict(type='int', default=1), + org=dict(type='str'), + ou=dict(type='list', elements='str'), + end_user_key_storage_agreement=dict(type='bool'), + tracking_info=dict(type='str'), + requester_name=dict(type='str', required=True), + requester_email=dict(type='str', required=True), + requester_phone=dict(type='str', required=True), + additional_emails=dict(type='list', elements='str'), + custom_fields=dict(type='dict', default=None, options=custom_fields_spec()), + cert_expiry=dict(type='str'), + cert_lifetime=dict(type='str', choices=['P1Y', 'P2Y', 'P3Y']), + ) + + +def main(): + ecs_argument_spec = ecs_client_argument_spec() + ecs_argument_spec.update(ecs_certificate_argument_spec()) + module = AnsibleModule( + argument_spec=ecs_argument_spec, + required_if=( + ['request_type', 'new', ['cert_type']], + ['request_type', 'validate_only', ['cert_type']], + ['cert_type', 'CODE_SIGNING', ['end_user_key_storage_agreement']], + ['cert_type', 'EV_CODE_SIGNING', ['end_user_key_storage_agreement']], + ), + mutually_exclusive=( + ['cert_expiry', 'cert_lifetime'], + ), + supports_check_mode=True, + ) + + if not CRYPTOGRAPHY_FOUND or CRYPTOGRAPHY_VERSION < LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION): + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + + # If validate_only is used, pointing to an existing tracking_id is an invalid operation + if module.params['tracking_id']: + if module.params['request_type'] == 'new' or module.params['request_type'] == 'validate_only': + module.fail_json(msg='The tracking_id field is invalid when request_type="{0}".'.format(module.params['request_type'])) + + # A reissued request can not specify an expiration date or lifetime + if module.params['request_type'] == 'reissue': + if module.params['cert_expiry']: + module.fail_json(msg='The cert_expiry field is invalid when request_type="reissue".') + elif module.params['cert_lifetime']: + module.fail_json(msg='The cert_lifetime field is invalid when request_type="reissue".') + # Only a reissued request can omit the CSR + else: + module_params_csr = module.params['csr'] + if module_params_csr is None: + module.fail_json(msg='The csr field is required when request_type={0}'.format(module.params['request_type'])) + elif not os.path.exists(module_params_csr): + module.fail_json(msg='The csr field of {0} was not a valid path. csr is required when request_type={1}'.format( + module_params_csr, module.params['request_type'])) + + if module.params['ou'] and len(module.params['ou']) > 1: + module.fail_json(msg='Multiple "ou" values are not currently supported.') + + if module.params['end_user_key_storage_agreement']: + if module.params['cert_type'] != 'CODE_SIGNING' and module.params['cert_type'] != 'EV_CODE_SIGNING': + module.fail_json(msg='Parameter "end_user_key_storage_agreement" is valid only for cert_types "CODE_SIGNING" and "EV_CODE_SIGNING"') + + if module.params['org'] and module.params['client_id'] != 1 and module.params['cert_type'] != 'PD_SSL': + module.fail_json(msg='The "org" parameter is not supported when client_id parameter is set to a value other than 1, unless cert_type is "PD_SSL".') + + if module.params['cert_expiry']: + if not validate_cert_expiry(module.params['cert_expiry']): + module.fail_json(msg='The "cert_expiry" parameter of "{0}" is not a valid date or date-time'.format(module.params['cert_expiry'])) + + certificate = EcsCertificate(module) + certificate.request_cert(module) + result = certificate.dump() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ecs_domain.py b/plugins/modules/ecs_domain.py new file mode 100644 index 00000000..d20249b2 --- /dev/null +++ b/plugins/modules/ecs_domain.py @@ -0,0 +1,409 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2019 Entrust Datacard Corporation. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: ecs_domain +author: + - Chris Trufan (@ctrufan) +short_description: Request validation of a domain with the Entrust Certificate Services (ECS) API +description: + - Request validation or re-validation of a domain with the Entrust Certificate Services (ECS) API. + - Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API. + - If the domain is already in the validation process, no new validation will be requested, but the validation data (if applicable) will be returned. + - If the domain is already in the validation process but the I(verification_method) specified is different than the current I(verification_method), + the I(verification_method) will be updated and validation data (if applicable) will be returned. + - If the domain is an active, validated domain, the return value of I(changed) will be false, unless C(domain_status=EXPIRED), in which case a re-validation + will be performed. + - If C(verification_method=dns), details about the required DNS entry will be specified in the return parameters I(dns_contents), I(dns_location), and + I(dns_resource_type). + - If C(verification_method=web_server), details about the required file details will be specified in the return parameters I(file_contents) and + I(file_location). + - If C(verification_method=email), the email address(es) that the validation email(s) were sent to will be in the return parameter I(emails). This is + purely informational. For domains requested using this module, this will always be a list of size 1. +notes: + - There is a small delay (typically about 5 seconds, but can be as long as 60 seconds) before obtaining the random values when requesting a validation + while C(verification_method=dns) or C(verification_method=web_server). Be aware of that if doing many domain validation requests. +options: + client_id: + description: + - The client ID to request the domain be associated with. + - If no client ID is specified, the domain will be added under the primary client with ID of 1. + type: int + default: 1 + domain_name: + description: + - The domain name to be verified or reverified. + type: str + required: true + verification_method: + description: + - The verification method to be used to prove control of the domain. + - If C(verification_method=email) and the value I(verification_email) is specified, that value is used for the email validation. If + I(verification_email) is not provided, the first value present in WHOIS data will be used. An email will be sent to the address in + I(verification_email) with instructions on how to verify control of the domain. + - If C(verification_method=dns), the value I(dns_contents) must be stored in location I(dns_location), with a DNS record type of + I(verification_dns_record_type). To prove domain ownership, update your DNS records so the text string returned by I(dns_contents) is available at + I(dns_location). + - If C(verification_method=web_server), the contents of return value I(file_contents) must be made available on a web server accessible at location + I(file_location). + - If C(verification_method=manual), the domain will be validated with a manual process. This is not recommended. + type: str + choices: [ 'dns', 'email', 'manual', 'web_server'] + required: true + verification_email: + description: + - Email address to be used to verify domain ownership. + - 'Email address must be either an email address present in the WHOIS data for I(domain_name), or one of the following constructed emails: + admin@I(domain_name), administrator@I(domain_name), webmaster@I(domain_name), hostmaster@I(domain_name), postmaster@I(domain_name)' + - 'Note that if I(domain_name) includes subdomains, the top level domain should be used. For example, if requesting validation of + example1.ansible.com, or test.example2.ansible.com, and you want to use the "admin" preconstructed name, the email address should be + admin@ansible.com.' + - If using the email values from the WHOIS data for the domain or it's top level namespace, they must be exact matches. + - If C(verification_method=email) but I(verification_email) is not provided, the first email address found in WHOIS data for the domain will be + used. + - To verify domain ownership, domain owner must follow the instructions in the email they receive. + - Only allowed if C(verification_method=email) + type: str +seealso: + - module: openssl_certificate + description: Can be used to request certificates from ECS, with C(provider=entrust). + - module: ecs_certificate + description: Can be used to request a Certificate from ECS using a verified domain. +extends_documentation_fragment: +- community.crypto.ecs_credential + +''' + +EXAMPLES = r''' +- name: Request domain validation using email validation for client ID of 2. + ecs_domain: + domain_name: ansible.com + client_id: 2 + verification_method: email + verification_email: admin@ansible.com + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: Request domain validation using DNS. If domain is already valid, + request revalidation if expires within 90 days + ecs_domain: + domain_name: ansible.com + verification_method: dns + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: Request domain validation using web server validation, and revalidate + if fewer than 60 days remaining of EV eligibility. + ecs_domain: + domain_name: ansible.com + verification_method: web_server + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key + +- name: Request domain validation using manual validation. + ecs_domain: + domain_name: ansible.com + verification_method: manual + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key +''' + +RETURN = ''' +domain_status: + description: Status of the current domain. Will be one of C(APPROVED), C(DECLINED), C(CANCELLED), C(INITIAL_VERIFICATION), C(DECLINED), C(CANCELLED), + C(RE_VERIFICATION), C(EXPIRED), C(EXPIRING) + returned: changed or success + type: str + sample: APPROVED +verification_method: + description: Verification method used to request the domain validation. If C(changed) will be the same as I(verification_method) input parameter. + returned: changed or success + type: str + sample: dns +file_location: + description: The location that ECS will be expecting to be able to find the file for domain verification, containing the contents of I(file_contents). + returned: I(verification_method) is C(web_server) + type: str + sample: http://ansible.com/.well-known/pki-validation/abcd.txt +file_contents: + description: The contents of the file that ECS will be expecting to find at C(file_location). + returned: I(verification_method) is C(web_server) + type: str + sample: AB23CD41432522FF2526920393982FAB +emails: + description: + - The list of emails used to request validation of this domain. + - Domains requested using this module will only have a list of size 1. + returned: I(verification_method) is C(email) + type: list + sample: [ admin@ansible.com, administrator@ansible.com ] +dns_location: + description: The location that ECS will be expecting to be able to find the DNS entry for domain verification, containing the contents of I(dns_contents). + returned: changed and if I(verification_method) is C(dns) + type: str + sample: _pki-validation.ansible.com +dns_contents: + description: The value that ECS will be expecting to find in the DNS record located at I(dns_location). + returned: changed and if I(verification_method) is C(dns) + type: str + sample: AB23CD41432522FF2526920393982FAB +dns_resource_type: + description: The type of resource record that ECS will be expecting for the DNS record located at I(dns_location). + returned: changed and if I(verification_method) is C(dns) + type: str + sample: TXT +client_id: + description: Client ID that the domain belongs to. If the input value I(client_id) is specified, this will always be the same as I(client_id) + returned: changed or success + type: int + sample: 1 +ov_eligible: + description: Whether the domain is eligible for submission of "OV" certificates. Will never be C(false) if I(ov_eligible) is C(true) + returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION), C(EXPIRING), or C(EXPIRED). + type: bool + sample: true +ov_days_remaining: + description: The number of days the domain remains eligible for submission of "OV" certificates. Will never be less than the value of I(ev_days_remaining) + returned: success and I(ov_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING). + type: int + sample: 129 +ev_eligible: + description: Whether the domain is eligible for submission of "EV" certificates. Will never be C(true) if I(ov_eligible) is C(false) + returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING), or C(EXPIRED). + type: bool + sample: true +ev_days_remaining: + description: The number of days the domain remains eligible for submission of "EV" certificates. Will never be greater than the value of + I(ov_days_remaining) + returned: success and I(ev_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING). + type: int + sample: 94 + +''' + +from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ( + ecs_client_argument_spec, + ECSClient, + RestOperationException, + SessionConfigurationException, +) + +import datetime +import time +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +def calculate_days_remaining(expiry_date): + days_remaining = None + if expiry_date: + expiry_datetime = datetime.datetime.strptime(expiry_date, '%Y-%m-%dT%H:%M:%SZ') + days_remaining = (expiry_datetime - datetime.datetime.now()).days + return days_remaining + + +class EcsDomain(object): + ''' + Entrust Certificate Services domain class. + ''' + + def __init__(self, module): + self.changed = False + self.domain_status = None + self.verification_method = None + self.file_location = None + self.file_contents = None + self.dns_location = None + self.dns_contents = None + self.dns_resource_type = None + self.emails = None + self.ov_eligible = None + self.ov_days_remaining = None + self.ev_eligble = None + self.ev_days_remaining = None + # Note that verification_method is the 'current' verification + # method of the domain, we'll use module.params when requesting a new + # one, in case the verification method has changed. + self.verification_method = None + + self.ecs_client = None + # Instantiate the ECS client and then try a no-op connection to verify credentials are valid + try: + self.ecs_client = ECSClient( + entrust_api_user=module.params['entrust_api_user'], + entrust_api_key=module.params['entrust_api_key'], + entrust_api_cert=module.params['entrust_api_client_cert_path'], + entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'], + entrust_api_specification_path=module.params['entrust_api_specification_path'] + ) + except SessionConfigurationException as e: + module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e))) + try: + self.ecs_client.GetAppVersion() + except RestOperationException as e: + module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message))) + + def set_domain_details(self, domain_details): + if domain_details.get('verificationMethod'): + self.verification_method = domain_details['verificationMethod'].lower() + self.domain_status = domain_details['verificationStatus'] + self.ov_eligible = domain_details.get('ovEligible') + self.ov_days_remaining = calculate_days_remaining(domain_details.get('ovExpiry')) + self.ev_eligible = domain_details.get('evEligible') + self.ev_days_remaining = calculate_days_remaining(domain_details.get('evExpiry')) + self.client_id = domain_details['clientId'] + + if self.verification_method == 'dns' and domain_details.get('dnsMethod'): + self.dns_location = domain_details['dnsMethod']['recordDomain'] + self.dns_resource_type = domain_details['dnsMethod']['recordType'] + self.dns_contents = domain_details['dnsMethod']['recordValue'] + elif self.verification_method == 'web_server' and domain_details.get('webServerMethod'): + self.file_location = domain_details['webServerMethod']['fileLocation'] + self.file_contents = domain_details['webServerMethod']['fileContents'] + elif self.verification_method == 'email' and domain_details.get('emailMethod'): + self.emails = domain_details['emailMethod'] + + def check(self, module): + try: + domain_details = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name']) + self.set_domain_details(domain_details) + if self.domain_status != 'APPROVED' and self.domain_status != 'INITIAL_VERIFICATION' and self.domain_status != 'RE_VERIFICATION': + return False + + # If domain verification is in process, we want to return the random values and treat it as a valid. + if self.domain_status == 'INITIAL_VERIFICATION' or self.domain_status == 'RE_VERIFICATION': + # Unless the verification method has changed, in which case we need to do a reverify request. + if self.verification_method != module.params['verification_method']: + return False + + if self.domain_status == 'EXPIRING': + return False + + return True + except RestOperationException as dummy: + return False + + def request_domain(self, module): + if not self.check(module): + body = {} + + body['verificationMethod'] = module.params['verification_method'].upper() + if module.params['verification_method'] == 'email': + emailMethod = {} + if module.params['verification_email']: + emailMethod['emailSource'] = 'SPECIFIED' + emailMethod['email'] = module.params['verification_email'] + else: + emailMethod['emailSource'] = 'INCLUDE_WHOIS' + body['emailMethod'] = emailMethod + # Only populate domain name in body if it is not an existing domain + if not self.domain_status: + body['domainName'] = module.params['domain_name'] + try: + if not self.domain_status: + self.ecs_client.AddDomain(clientId=module.params['client_id'], Body=body) + else: + self.ecs_client.ReverifyDomain(clientId=module.params['client_id'], domain=module.params['domain_name'], Body=body) + + time.sleep(5) + result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name']) + + # It takes a bit of time before the random values are available + if module.params['verification_method'] == 'dns' or module.params['verification_method'] == 'web_server': + for i in range(4): + # Check both that random values are now available, and that they're different than were populated by previous 'check' + if module.params['verification_method'] == 'dns': + if result.get('dnsMethod') and result['dnsMethod']['recordValue'] != self.dns_contents: + break + elif module.params['verification_method'] == 'web_server': + if result.get('webServerMethod') and result['webServerMethod']['fileContents'] != self.file_contents: + break + time.sleep(10) + result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name']) + self.changed = True + self.set_domain_details(result) + except RestOperationException as e: + module.fail_json(msg='Failed to request domain validation from Entrust (ECS) {0}'.format(e.message)) + + def dump(self): + result = { + 'changed': self.changed, + 'client_id': self.client_id, + 'domain_status': self.domain_status, + } + + if self.verification_method: + result['verification_method'] = self.verification_method + if self.ov_eligible is not None: + result['ov_eligible'] = self.ov_eligible + if self.ov_days_remaining: + result['ov_days_remaining'] = self.ov_days_remaining + if self.ev_eligible is not None: + result['ev_eligible'] = self.ev_eligible + if self.ev_days_remaining: + result['ev_days_remaining'] = self.ev_days_remaining + if self.emails: + result['emails'] = self.emails + + if self.verification_method == 'dns': + result['dns_location'] = self.dns_location + result['dns_contents'] = self.dns_contents + result['dns_resource_type'] = self.dns_resource_type + elif self.verification_method == 'web_server': + result['file_location'] = self.file_location + result['file_contents'] = self.file_contents + elif self.verification_method == 'email': + result['emails'] = self.emails + + return result + + +def ecs_domain_argument_spec(): + return dict( + client_id=dict(type='int', default=1), + domain_name=dict(type='str', required=True), + verification_method=dict(type='str', required=True, choices=['dns', 'email', 'manual', 'web_server']), + verification_email=dict(type='str'), + ) + + +def main(): + ecs_argument_spec = ecs_client_argument_spec() + ecs_argument_spec.update(ecs_domain_argument_spec()) + module = AnsibleModule( + argument_spec=ecs_argument_spec, + supports_check_mode=False, + ) + + if module.params['verification_email'] and module.params['verification_method'] != 'email': + module.fail_json(msg='The verification_email field is invalid when verification_method="{0}".'.format(module.params['verification_method'])) + + domain = EcsDomain(module) + domain.request_domain(module) + result = domain.dump() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/get_certificate.py b/plugins/modules/get_certificate.py new file mode 100644 index 00000000..f29b579d --- /dev/null +++ b/plugins/modules/get_certificate.py @@ -0,0 +1,367 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: get_certificate +author: "John Westcott IV (@john-westcott-iv)" +short_description: Get a certificate from a host:port +description: + - Makes a secure connection and returns information about the presented certificate + - The module can use the cryptography Python library, or the pyOpenSSL Python + library. By default, it tries to detect which one is available. This can be + overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL + backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." +options: + host: + description: + - The host to get the cert for (IP is fine) + type: str + required: true + ca_cert: + description: + - A PEM file containing one or more root certificates; if present, the cert will be validated against these root certs. + - Note that this only validates the certificate is signed by the chain; not that the cert is valid for the host presenting it. + type: path + port: + description: + - The port to connect to + type: int + required: true + proxy_host: + description: + - Proxy host used when get a certificate. + type: str + proxy_port: + description: + - Proxy port used when get a certificate. + type: int + default: 8080 + timeout: + description: + - The timeout in seconds + type: int + default: 10 + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + +notes: + - When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed. + +requirements: + - "python >= 2.7 when using C(proxy_host)" + - "cryptography >= 1.6 or pyOpenSSL >= 0.15" +''' + +RETURN = ''' +cert: + description: The certificate retrieved from the port + returned: success + type: str +expired: + description: Boolean indicating if the cert is expired + returned: success + type: bool +extensions: + description: Extensions applied to the cert + returned: success + type: list + elements: dict + contains: + critical: + returned: success + type: bool + description: Whether the extension is critical. + asn1_data: + returned: success + type: str + description: The Base64 encoded ASN.1 content of the extnesion. + name: + returned: success + type: str + description: The extension's name. +issuer: + description: Information about the issuer of the cert + returned: success + type: dict +not_after: + description: Expiration date of the cert + returned: success + type: str +not_before: + description: Issue date of the cert + returned: success + type: str +serial_number: + description: The serial number of the cert + returned: success + type: str +signature_algorithm: + description: The algorithm used to sign the cert + returned: success + type: str +subject: + description: Information about the subject of the cert (OU, CN, etc) + returned: success + type: dict +version: + description: The version number of the certificate + returned: success + type: str +''' + +EXAMPLES = ''' +- name: Get the cert from an RDP port + get_certificate: + host: "1.2.3.4" + port: 3389 + delegate_to: localhost + run_once: true + register: cert + +- name: Get a cert from an https port + get_certificate: + host: "www.google.com" + port: 443 + delegate_to: localhost + run_once: true + register: cert + +- name: How many days until cert expires + debug: + msg: "cert expires in: {{ expire_days }} days." + vars: + expire_days: "{{ (( cert.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')) ).days }}" +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_bytes +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils + +from distutils.version import LooseVersion +from os.path import isfile +from socket import setdefaulttimeout, socket +from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_OPTIONAL + +import atexit +import base64 +import datetime +import traceback + +MINIMAL_PYOPENSSL_VERSION = '0.15' +MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' + +CREATE_DEFAULT_CONTEXT_IMP_ERR = None +try: + from ssl import create_default_context +except ImportError: + CREATE_DEFAULT_CONTEXT_IMP_ERR = traceback.format_exc() + HAS_CREATE_DEFAULT_CONTEXT = False +else: + HAS_CREATE_DEFAULT_CONTEXT = True + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.exceptions + import cryptography.x509 + from cryptography.hazmat.backends import default_backend as cryptography_backend + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +def main(): + module = AnsibleModule( + argument_spec=dict( + ca_cert=dict(type='path'), + host=dict(type='str', required=True), + port=dict(type='int', required=True), + proxy_host=dict(type='str'), + proxy_port=dict(type='int', default=8080), + timeout=dict(type='int', default=10), + select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), + ), + ) + + ca_cert = module.params.get('ca_cert') + host = module.params.get('host') + port = module.params.get('port') + proxy_host = module.params.get('proxy_host') + proxy_port = module.params.get('proxy_port') + timeout = module.params.get('timeout') + + backend = module.params.get('select_crypto_backend') + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # First try cryptography, then pyOpenSSL + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + + result = dict( + changed=False, + ) + + if timeout: + setdefaulttimeout(timeout) + + if ca_cert: + if not isfile(ca_cert): + module.fail_json(msg="ca_cert file does not exist") + + if proxy_host: + if not HAS_CREATE_DEFAULT_CONTEXT: + module.fail_json(msg='To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.', + exception=CREATE_DEFAULT_CONTEXT_IMP_ERR) + + try: + connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port) + sock = socket() + atexit.register(sock.close) + sock.connect((proxy_host, proxy_port)) + sock.send(connect.encode()) + sock.recv(8192) + + ctx = create_default_context() + ctx.check_hostname = False + ctx.verify_mode = CERT_NONE + + if ca_cert: + ctx.verify_mode = CERT_OPTIONAL + ctx.load_verify_locations(cafile=ca_cert) + + cert = ctx.wrap_socket(sock, server_hostname=host).getpeercert(True) + cert = DER_cert_to_PEM_cert(cert) + except Exception as e: + module.fail_json(msg="Failed to get cert from port with error: {0}".format(e)) + + else: + try: + cert = get_server_certificate((host, port), ca_certs=ca_cert) + except Exception as e: + module.fail_json(msg="Failed to get cert from port with error: {0}".format(e)) + + result['cert'] = cert + + if backend == 'pyopenssl': + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + result['subject'] = {} + for component in x509.get_subject().get_components(): + result['subject'][component[0]] = component[1] + + result['expired'] = x509.has_expired() + + result['extensions'] = [] + extension_count = x509.get_extension_count() + for index in range(0, extension_count): + extension = x509.get_extension(index) + result['extensions'].append({ + 'critical': extension.get_critical(), + 'asn1_data': extension.get_data(), + 'name': extension.get_short_name(), + }) + + result['issuer'] = {} + for component in x509.get_issuer().get_components(): + result['issuer'][component[0]] = component[1] + + result['not_after'] = x509.get_notAfter() + result['not_before'] = x509.get_notBefore() + + result['serial_number'] = x509.get_serial_number() + result['signature_algorithm'] = x509.get_signature_algorithm() + + result['version'] = x509.get_version() + + elif backend == 'cryptography': + x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend()) + result['subject'] = {} + for attribute in x509.subject: + result['subject'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value + + result['expired'] = x509.not_valid_after < datetime.datetime.utcnow() + + result['extensions'] = [] + for dotted_number, entry in crypto_utils.cryptography_get_extensions_from_cert(x509).items(): + oid = cryptography.x509.oid.ObjectIdentifier(dotted_number) + result['extensions'].append({ + 'critical': entry['critical'], + 'asn1_data': base64.b64decode(entry['value']), + 'name': crypto_utils.cryptography_oid_to_name(oid, short=True), + }) + + result['issuer'] = {} + for attribute in x509.issuer: + result['issuer'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value + + result['not_after'] = x509.not_valid_after.strftime('%Y%m%d%H%M%SZ') + result['not_before'] = x509.not_valid_before.strftime('%Y%m%d%H%M%SZ') + + result['serial_number'] = x509.serial_number + result['signature_algorithm'] = crypto_utils.cryptography_oid_to_name(x509.signature_algorithm_oid) + + # We need the -1 offset to get the same values as pyOpenSSL + if x509.version == cryptography.x509.Version.v1: + result['version'] = 1 - 1 + elif x509.version == cryptography.x509.Version.v3: + result['version'] = 3 - 1 + else: + result['version'] = "unknown" + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/luks_device.py b/plugins/modules/luks_device.py new file mode 100644 index 00000000..3d8172cc --- /dev/null +++ b/plugins/modules/luks_device.py @@ -0,0 +1,786 @@ +#!/usr/bin/python +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: luks_device + +short_description: Manage encrypted (LUKS) devices + + +description: + - "Module manages L(LUKS,https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup) + on given device. Supports creating, destroying, opening and closing of + LUKS container and adding or removing new keys and passphrases." + +options: + device: + description: + - "Device to work with (e.g. C(/dev/sda1)). Needed in most cases. + Can be omitted only when I(state=closed) together with I(name) + is provided." + type: str + state: + description: + - "Desired state of the LUKS container. Based on its value creates, + destroys, opens or closes the LUKS container on a given device." + - "I(present) will create LUKS container unless already present. + Requires I(device) and either I(keyfile) or I(passphrase) options + to be provided." + - "I(absent) will remove existing LUKS container if it exists. + Requires I(device) or I(name) to be specified." + - "I(opened) will unlock the LUKS container. If it does not exist + it will be created first. + Requires I(device) and either I(keyfile) or I(passphrase) + to be specified. Use the I(name) option to set the name of + the opened container. Otherwise the name will be + generated automatically and returned as a part of the + result." + - "I(closed) will lock the LUKS container. However if the container + does not exist it will be created. + Requires I(device) and either I(keyfile) or I(passphrase) + options to be provided. If container does already exist + I(device) or I(name) will suffice." + type: str + default: present + choices: [present, absent, opened, closed] + name: + description: + - "Sets container name when I(state=opened). Can be used + instead of I(device) when closing the existing container + (i.e. when I(state=closed))." + type: str + keyfile: + description: + - "Used to unlock the container. Either a I(keyfile) or a + I(passphrase) is needed for most of the operations. Parameter + value is the path to the keyfile with the passphrase." + - "BEWARE that working with keyfiles in plaintext is dangerous. + Make sure that they are protected." + type: path + passphrase: + description: + - "Used to unlock the container. Either a I(passphrase) or a + I(keyfile) is needed for most of the operations. Parameter + value is a string with the passphrase." + type: str + keysize: + description: + - "Sets the key size only if LUKS container does not exist." + type: int + new_keyfile: + description: + - "Adds additional key to given container on I(device). + Needs I(keyfile) or I(passphrase) option for authorization. + LUKS container supports up to 8 keyslots. Parameter value + is the path to the keyfile with the passphrase." + - "NOTE that adding additional keys is *not idempotent*. + A new keyslot will be used even if another keyslot already + exists for this keyfile." + - "BEWARE that working with keyfiles in plaintext is dangerous. + Make sure that they are protected." + type: path + new_passphrase: + description: + - "Adds additional passphrase to given container on I(device). + Needs I(keyfile) or I(passphrase) option for authorization. LUKS + container supports up to 8 keyslots. Parameter value is a string + with the new passphrase." + - "NOTE that adding additional passphrase is *not idempotent*. A + new keyslot will be used even if another keyslot already exists + for this passphrase." + type: str + remove_keyfile: + description: + - "Removes given key from the container on I(device). Does not + remove the keyfile from filesystem. + Parameter value is the path to the keyfile with the passphrase." + - "NOTE that removing keys is *not idempotent*. Trying to remove + a key which no longer exists results in an error." + - "NOTE that to remove the last key from a LUKS container, the + I(force_remove_last_key) option must be set to C(yes)." + - "BEWARE that working with keyfiles in plaintext is dangerous. + Make sure that they are protected." + type: path + remove_passphrase: + description: + - "Removes given passphrase from the container on I(device). + Parameter value is a string with the passphrase to remove." + - "NOTE that removing passphrases is I(not + idempotent). Trying to remove a passphrase which no longer + exists results in an error." + - "NOTE that to remove the last keyslot from a LUKS + container, the I(force_remove_last_key) option must be set + to C(yes)." + type: str + force_remove_last_key: + description: + - "If set to C(yes), allows removing the last key from a container." + - "BEWARE that when the last key has been removed from a container, + the container can no longer be opened!" + type: bool + default: no + label: + description: + - "This option allow the user to create a LUKS2 format container + with label support, respectively to identify the container by + label on later usages." + - "Will only be used on container creation, or when I(device) is + not specified." + - "This cannot be specified if I(type) is set to C(luks1)." + type: str + uuid: + description: + - "With this option user can identify the LUKS container by UUID." + - "Will only be used when I(device) and I(label) are not specified." + type: str + type: + description: + - "This option allow the user explicit define the format of LUKS + container that wants to work with. Options are C(luks1) or C(luks2)" + type: str + choices: [luks1, luks2] + + + +requirements: + - "cryptsetup" + - "wipefs (when I(state) is C(absent))" + - "lsblk" + - "blkid (when I(label) or I(uuid) options are used)" + +author: Jan Pokorny (@japokorn) +''' + +EXAMPLES = ''' + +- name: create LUKS container (remains unchanged if it already exists) + luks_device: + device: "/dev/loop0" + state: "present" + keyfile: "/vault/keyfile" + +- name: create LUKS container with a passphrase + luks_device: + device: "/dev/loop0" + state: "present" + passphrase: "foo" + +- name: (create and) open the LUKS container; name it "mycrypt" + luks_device: + device: "/dev/loop0" + state: "opened" + name: "mycrypt" + keyfile: "/vault/keyfile" + +- name: close the existing LUKS container "mycrypt" + luks_device: + state: "closed" + name: "mycrypt" + +- name: make sure LUKS container exists and is closed + luks_device: + device: "/dev/loop0" + state: "closed" + keyfile: "/vault/keyfile" + +- name: create container if it does not exist and add new key to it + luks_device: + device: "/dev/loop0" + state: "present" + keyfile: "/vault/keyfile" + new_keyfile: "/vault/keyfile2" + +- name: add new key to the LUKS container (container has to exist) + luks_device: + device: "/dev/loop0" + keyfile: "/vault/keyfile" + new_keyfile: "/vault/keyfile2" + +- name: add new passphrase to the LUKS container + luks_device: + device: "/dev/loop0" + keyfile: "/vault/keyfile" + new_passphrase: "foo" + +- name: remove existing keyfile from the LUKS container + luks_device: + device: "/dev/loop0" + remove_keyfile: "/vault/keyfile2" + +- name: remove existing passphrase from the LUKS container + luks_device: + device: "/dev/loop0" + remove_passphrase: "foo" + +- name: completely remove the LUKS container and its contents + luks_device: + device: "/dev/loop0" + state: "absent" + +- name: create a container with label + luks_device: + device: "/dev/loop0" + state: "present" + keyfile: "/vault/keyfile" + label: personalLabelName + +- name: open the LUKS container based on label without device; name it "mycrypt" + luks_device: + label: "personalLabelName" + state: "opened" + name: "mycrypt" + keyfile: "/vault/keyfile" + +- name: close container based on UUID + luks_device: + uuid: 03ecd578-fad4-4e6c-9348-842e3e8fa340 + state: "closed" + name: "mycrypt" + +- name: create a container using luks2 format + luks_device: + device: "/dev/loop0" + state: "present" + keyfile: "/vault/keyfile" + type: luks2 +''' + +RETURN = ''' +name: + description: + When I(state=opened) returns (generated or given) name + of LUKS container. Returns None if no name is supplied. + returned: success + type: str + sample: "luks-c1da9a58-2fde-4256-9d9f-6ab008b4dd1b" +''' + +import os +import re +import stat + +from ansible.module_utils.basic import AnsibleModule + +RETURN_CODE = 0 +STDOUT = 1 +STDERR = 2 + +# used to get out of lsblk output in format 'crypt ' +# regex takes care of any possible blank characters +LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*') +# used to get out of lsblk output +# in format 'device: ' +LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*') + + +class Handler(object): + + def __init__(self, module): + self._module = module + self._lsblk_bin = self._module.get_bin_path('lsblk', True) + + def _run_command(self, command, data=None): + return self._module.run_command(command, data=data) + + def get_device_by_uuid(self, uuid): + ''' Returns the device that holds UUID passed by user + ''' + self._blkid_bin = self._module.get_bin_path('blkid', True) + uuid = self._module.params['uuid'] + if uuid is None: + return None + result = self._run_command([self._blkid_bin, '--uuid', uuid]) + if result[RETURN_CODE] != 0: + return None + return result[STDOUT].strip() + + def get_device_by_label(self, label): + ''' Returns the device that holds label passed by user + ''' + self._blkid_bin = self._module.get_bin_path('blkid', True) + label = self._module.params['label'] + if label is None: + return None + result = self._run_command([self._blkid_bin, '--label', label]) + if result[RETURN_CODE] != 0: + return None + return result[STDOUT].strip() + + def generate_luks_name(self, device): + ''' Generate name for luks based on device UUID ('luks-'). + Raises ValueError when obtaining of UUID fails. + ''' + result = self._run_command([self._lsblk_bin, '-n', device, '-o', 'UUID']) + + if result[RETURN_CODE] != 0: + raise ValueError('Error while generating LUKS name for %s: %s' + % (device, result[STDERR])) + dev_uuid = result[STDOUT].strip() + return 'luks-%s' % dev_uuid + + +class CryptHandler(Handler): + + def __init__(self, module): + super(CryptHandler, self).__init__(module) + self._cryptsetup_bin = self._module.get_bin_path('cryptsetup', True) + + def get_container_name_by_device(self, device): + ''' obtain LUKS container name based on the device where it is located + return None if not found + raise ValueError if lsblk command fails + ''' + result = self._run_command([self._lsblk_bin, device, '-nlo', 'type,name']) + if result[RETURN_CODE] != 0: + raise ValueError('Error while obtaining LUKS name for %s: %s' + % (device, result[STDERR])) + + m = LUKS_NAME_REGEX.search(result[STDOUT]) + + try: + name = m.group(1) + except AttributeError: + name = None + return name + + def get_container_device_by_name(self, name): + ''' obtain device name based on the LUKS container name + return None if not found + raise ValueError if lsblk command fails + ''' + # apparently each device can have only one LUKS container on it + result = self._run_command([self._cryptsetup_bin, 'status', name]) + if result[RETURN_CODE] != 0: + return None + + m = LUKS_DEVICE_REGEX.search(result[STDOUT]) + device = m.group(1) + return device + + def is_luks(self, device): + ''' check if the LUKS container does exist + ''' + result = self._run_command([self._cryptsetup_bin, 'isLuks', device]) + return result[RETURN_CODE] == 0 + + def run_luks_create(self, device, keyfile, passphrase, keysize): + # create a new luks container; use batch mode to auto confirm + luks_type = self._module.params['type'] + label = self._module.params['label'] + + options = [] + if keysize is not None: + options.append('--key-size=' + str(keysize)) + if label is not None: + options.extend(['--label', label]) + luks_type = 'luks2' + if luks_type is not None: + options.extend(['--type', luks_type]) + + args = [self._cryptsetup_bin, 'luksFormat'] + args.extend(options) + args.extend(['-q', device]) + if keyfile: + args.append(keyfile) + + result = self._run_command(args, data=passphrase) + if result[RETURN_CODE] != 0: + raise ValueError('Error while creating LUKS on %s: %s' + % (device, result[STDERR])) + + def run_luks_open(self, device, keyfile, passphrase, name): + args = [self._cryptsetup_bin] + if keyfile: + args.extend(['--key-file', keyfile]) + args.extend(['open', '--type', 'luks', device, name]) + + result = self._run_command(args, data=passphrase) + if result[RETURN_CODE] != 0: + raise ValueError('Error while opening LUKS container on %s: %s' + % (device, result[STDERR])) + + def run_luks_close(self, name): + result = self._run_command([self._cryptsetup_bin, 'close', name]) + if result[RETURN_CODE] != 0: + raise ValueError('Error while closing LUKS container %s' % (name)) + + def run_luks_remove(self, device): + wipefs_bin = self._module.get_bin_path('wipefs', True) + + name = self.get_container_name_by_device(device) + if name is not None: + self.run_luks_close(name) + result = self._run_command([wipefs_bin, '--all', device]) + if result[RETURN_CODE] != 0: + raise ValueError('Error while wiping luks container %s: %s' + % (device, result[STDERR])) + + def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile, + new_passphrase): + ''' Add new key from a keyfile or passphrase to given 'device'; + authentication done using 'keyfile' or 'passphrase'. + Raises ValueError when command fails. + ''' + data = [] + args = [self._cryptsetup_bin, 'luksAddKey', device] + + if keyfile: + args.extend(['--key-file', keyfile]) + else: + data.append(passphrase) + + if new_keyfile: + args.append(new_keyfile) + else: + data.extend([new_passphrase, new_passphrase]) + + result = self._run_command(args, data='\n'.join(data) or None) + if result[RETURN_CODE] != 0: + raise ValueError('Error while adding new LUKS keyslot to %s: %s' + % (device, result[STDERR])) + + def run_luks_remove_key(self, device, keyfile, passphrase, + force_remove_last_key=False): + ''' Remove key from given device + Raises ValueError when command fails + ''' + if not force_remove_last_key: + result = self._run_command([self._cryptsetup_bin, 'luksDump', device]) + if result[RETURN_CODE] != 0: + raise ValueError('Error while dumping LUKS header from %s' + % (device, )) + keyslot_count = 0 + keyslot_area = False + keyslot_re = re.compile(r'^Key Slot [0-9]+: ENABLED') + for line in result[STDOUT].splitlines(): + if line.startswith('Keyslots:'): + keyslot_area = True + elif line.startswith(' '): + # LUKS2 header dumps use human-readable indented output. + # Thus we have to look out for 'Keyslots:' and count the + # number of indented keyslot numbers. + if keyslot_area and line[2] in '0123456789': + keyslot_count += 1 + elif line.startswith('\t'): + pass + elif keyslot_re.match(line): + # LUKS1 header dumps have one line per keyslot with ENABLED + # or DISABLED in them. We count such lines with ENABLED. + keyslot_count += 1 + else: + keyslot_area = False + if keyslot_count < 2: + self._module.fail_json(msg="LUKS device %s has less than two active keyslots. " + "To be able to remove a key, please set " + "`force_remove_last_key` to `yes`." % device) + + args = [self._cryptsetup_bin, 'luksRemoveKey', device, '-q'] + if keyfile: + args.extend(['--key-file', keyfile]) + result = self._run_command(args, data=passphrase) + if result[RETURN_CODE] != 0: + raise ValueError('Error while removing LUKS key from %s: %s' + % (device, result[STDERR])) + + +class ConditionsHandler(Handler): + + def __init__(self, module, crypthandler): + super(ConditionsHandler, self).__init__(module) + self._crypthandler = crypthandler + self.device = self.get_device_name() + + def get_device_name(self): + device = self._module.params.get('device') + label = self._module.params.get('label') + uuid = self._module.params.get('uuid') + name = self._module.params.get('name') + + if device is None and label is not None: + device = self.get_device_by_label(label) + elif device is None and uuid is not None: + device = self.get_device_by_uuid(uuid) + elif device is None and name is not None: + device = self._crypthandler.get_container_device_by_name(name) + + return device + + def luks_create(self): + return (self.device is not None and + (self._module.params['keyfile'] is not None or + self._module.params['passphrase'] is not None) and + self._module.params['state'] in ('present', + 'opened', + 'closed') and + not self._crypthandler.is_luks(self.device)) + + def opened_luks_name(self): + ''' If luks is already opened, return its name. + If 'name' parameter is specified and differs + from obtained value, fail. + Return None otherwise + ''' + if self._module.params['state'] != 'opened': + return None + + # try to obtain luks name - it may be already opened + name = self._crypthandler.get_container_name_by_device(self.device) + + if name is None: + # container is not open + return None + + if self._module.params['name'] is None: + # container is already opened + return name + + if name != self._module.params['name']: + # the container is already open but with different name: + # suspicious. back off + self._module.fail_json(msg="LUKS container is already opened " + "under different name '%s'." % name) + + # container is opened and the names match + return name + + def luks_open(self): + if ((self._module.params['keyfile'] is None and + self._module.params['passphrase'] is None) or + self.device is None or + self._module.params['state'] != 'opened'): + # conditions for open not fulfilled + return False + + name = self.opened_luks_name() + + if name is None: + return True + return False + + def luks_close(self): + if ((self._module.params['name'] is None and self.device is None) or + self._module.params['state'] != 'closed'): + # conditions for close not fulfilled + return False + + if self.device is not None: + name = self._crypthandler.get_container_name_by_device(self.device) + # successfully getting name based on device means that luks is open + luks_is_open = name is not None + + if self._module.params['name'] is not None: + self.device = self._crypthandler.get_container_device_by_name( + self._module.params['name']) + # successfully getting device based on name means that luks is open + luks_is_open = self.device is not None + + return luks_is_open + + def luks_add_key(self): + if (self.device is None or + (self._module.params['keyfile'] is None and + self._module.params['passphrase'] is None) or + (self._module.params['new_keyfile'] is None and + self._module.params['new_passphrase'] is None)): + # conditions for adding a key not fulfilled + return False + + if self._module.params['state'] == 'absent': + self._module.fail_json(msg="Contradiction in setup: Asking to " + "add a key to absent LUKS.") + + return True + + def luks_remove_key(self): + if (self.device is None or + (self._module.params['remove_keyfile'] is None and + self._module.params['remove_passphrase'] is None)): + # conditions for removing a key not fulfilled + return False + + if self._module.params['state'] == 'absent': + self._module.fail_json(msg="Contradiction in setup: Asking to " + "remove a key from absent LUKS.") + + return True + + def luks_remove(self): + return (self.device is not None and + self._module.params['state'] == 'absent' and + self._crypthandler.is_luks(self.device)) + + +def run_module(): + # available arguments/parameters that a user can pass + module_args = dict( + state=dict(type='str', default='present', choices=['present', 'absent', 'opened', 'closed']), + device=dict(type='str'), + name=dict(type='str'), + keyfile=dict(type='path'), + new_keyfile=dict(type='path'), + remove_keyfile=dict(type='path'), + passphrase=dict(type='str', no_log=True), + new_passphrase=dict(type='str', no_log=True), + remove_passphrase=dict(type='str', no_log=True), + force_remove_last_key=dict(type='bool', default=False), + keysize=dict(type='int'), + label=dict(type='str'), + uuid=dict(type='str'), + type=dict(type='str', choices=['luks1', 'luks2']), + ) + + mutually_exclusive = [ + ('keyfile', 'passphrase'), + ('new_keyfile', 'new_passphrase'), + ('remove_keyfile', 'remove_passphrase') + ] + + # seed the result dict in the object + result = dict( + changed=False, + name=None + ) + + module = AnsibleModule(argument_spec=module_args, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive) + + if module.params['device'] is not None: + try: + statinfo = os.stat(module.params['device']) + mode = statinfo.st_mode + if not stat.S_ISBLK(mode) and not stat.S_ISCHR(mode): + raise Exception('{0} is not a device'.format(module.params['device'])) + except Exception as e: + module.fail_json(msg=str(e)) + + crypt = CryptHandler(module) + conditions = ConditionsHandler(module, crypt) + + # conditions not allowed to run + if module.params['label'] is not None and module.params['type'] == 'luks1': + module.fail_json(msg='You cannot combine type luks1 with the label option.') + + # The conditions are in order to allow more operations in one run. + # (e.g. create luks and add a key to it) + + # luks create + if conditions.luks_create(): + if not module.check_mode: + try: + crypt.run_luks_create(conditions.device, + module.params['keyfile'], + module.params['passphrase'], + module.params['keysize']) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + result['changed'] = True + if module.check_mode: + module.exit_json(**result) + + # luks open + + name = conditions.opened_luks_name() + if name is not None: + result['name'] = name + + if conditions.luks_open(): + name = module.params['name'] + if name is None: + try: + name = crypt.generate_luks_name(conditions.device) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + if not module.check_mode: + try: + crypt.run_luks_open(conditions.device, + module.params['keyfile'], + module.params['passphrase'], + name) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + result['name'] = name + result['changed'] = True + if module.check_mode: + module.exit_json(**result) + + # luks close + if conditions.luks_close(): + if conditions.device is not None: + try: + name = crypt.get_container_name_by_device( + conditions.device) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + else: + name = module.params['name'] + if not module.check_mode: + try: + crypt.run_luks_close(name) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + result['name'] = name + result['changed'] = True + if module.check_mode: + module.exit_json(**result) + + # luks add key + if conditions.luks_add_key(): + if not module.check_mode: + try: + crypt.run_luks_add_key(conditions.device, + module.params['keyfile'], + module.params['passphrase'], + module.params['new_keyfile'], + module.params['new_passphrase']) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + result['changed'] = True + if module.check_mode: + module.exit_json(**result) + + # luks remove key + if conditions.luks_remove_key(): + if not module.check_mode: + try: + last_key = module.params['force_remove_last_key'] + crypt.run_luks_remove_key(conditions.device, + module.params['remove_keyfile'], + module.params['remove_passphrase'], + force_remove_last_key=last_key) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + result['changed'] = True + if module.check_mode: + module.exit_json(**result) + + # luks remove + if conditions.luks_remove(): + if not module.check_mode: + try: + crypt.run_luks_remove(conditions.device) + except ValueError as e: + module.fail_json(msg="luks_device error: %s" % e) + result['changed'] = True + if module.check_mode: + module.exit_json(**result) + + # Success - return result + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/openssh_cert.py b/plugins/modules/openssh_cert.py new file mode 100644 index 00000000..bcc43fa7 --- /dev/null +++ b/plugins/modules/openssh_cert.py @@ -0,0 +1,589 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, David Kainz +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: openssh_cert +author: "David Kainz (@lolcube)" +short_description: Generate OpenSSH host or user certificates. +description: + - Generate and regenerate OpenSSH host or user certificates. +requirements: + - "ssh-keygen" +options: + state: + description: + - Whether the host or user certificate should exist or not, taking action if the state is different from what is stated. + type: str + default: "present" + choices: [ 'present', 'absent' ] + type: + description: + - Whether the module should generate a host or a user certificate. + - Required if I(state) is C(present). + type: str + choices: ['host', 'user'] + force: + description: + - Should the certificate be regenerated even if it already exists and is valid. + type: bool + default: false + path: + description: + - Path of the file containing the certificate. + type: path + required: true + signing_key: + description: + - The path to the private openssh key that is used for signing the public key in order to generate the certificate. + - Required if I(state) is C(present). + type: path + public_key: + description: + - The path to the public key that will be signed with the signing key in order to generate the certificate. + - Required if I(state) is C(present). + type: path + valid_from: + description: + - "The point in time the certificate is valid from. Time can be specified either as relative time or as absolute timestamp. + Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | always) + where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + Note that if using relative time this module is NOT idempotent." + - Required if I(state) is C(present). + type: str + valid_to: + description: + - "The point in time the certificate is valid to. Time can be specified either as relative time or as absolute timestamp. + Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | forever) + where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + Note that if using relative time this module is NOT idempotent." + - Required if I(state) is C(present). + type: str + valid_at: + description: + - "Check if the certificate is valid at a certain point in time. If it is not the certificate will be regenerated. + Time will always be interpreted as UTC. Mainly to be used with relative timespec for I(valid_from) and / or I(valid_to). + Note that if using relative time this module is NOT idempotent." + type: str + principals: + description: + - "Certificates may be limited to be valid for a set of principal (user/host) names. + By default, generated certificates are valid for all users or hosts." + type: list + elements: str + options: + description: + - "Specify certificate options when signing a key. The option that are valid for user certificates are:" + - "C(clear): Clear all enabled permissions. This is useful for clearing the default set of permissions so permissions may be added individually." + - "C(force-command=command): Forces the execution of command instead of any shell or + command specified by the user when the certificate is used for authentication." + - "C(no-agent-forwarding): Disable ssh-agent forwarding (permitted by default)." + - "C(no-port-forwarding): Disable port forwarding (permitted by default)." + - "C(no-pty Disable): PTY allocation (permitted by default)." + - "C(no-user-rc): Disable execution of C(~/.ssh/rc) by sshd (permitted by default)." + - "C(no-x11-forwarding): Disable X11 forwarding (permitted by default)" + - "C(permit-agent-forwarding): Allows ssh-agent forwarding." + - "C(permit-port-forwarding): Allows port forwarding." + - "C(permit-pty): Allows PTY allocation." + - "C(permit-user-rc): Allows execution of C(~/.ssh/rc) by sshd." + - "C(permit-x11-forwarding): Allows X11 forwarding." + - "C(source-address=address_list): Restrict the source addresses from which the certificate is considered valid. + The C(address_list) is a comma-separated list of one or more address/netmask pairs in CIDR format." + - "At present, no options are valid for host keys." + type: list + elements: str + identifier: + description: + - Specify the key identity when signing a public key. The identifier that is logged by the server when the certificate is used for authentication. + type: str + serial_number: + description: + - "Specify the certificate serial number. + The serial number is logged by the server when the certificate is used for authentication. + The certificate serial number may be used in a KeyRevocationList. + The serial number may be omitted for checks, but must be specified again for a new certificate. + Note: The default value set by ssh-keygen is 0." + type: int + +extends_documentation_fragment: files +''' + +EXAMPLES = ''' +# Generate an OpenSSH user certificate that is valid forever and for all users +- openssh_cert: + type: user + signing_key: /path/to/private_key + public_key: /path/to/public_key.pub + path: /path/to/certificate + valid_from: always + valid_to: forever + +# Generate an OpenSSH host certificate that is valid for 32 weeks from now and will be regenerated +# if it is valid for less than 2 weeks from the time the module is being run +- openssh_cert: + type: host + signing_key: /path/to/private_key + public_key: /path/to/public_key.pub + path: /path/to/certificate + valid_from: +0s + valid_to: +32w + valid_at: +2w + +# Generate an OpenSSH host certificate that is valid forever and only for example.com and examplehost +- openssh_cert: + type: host + signing_key: /path/to/private_key + public_key: /path/to/public_key.pub + path: /path/to/certificate + valid_from: always + valid_to: forever + principals: + - example.com + - examplehost + +# Generate an OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 +- openssh_cert: + type: host + signing_key: /path/to/private_key + public_key: /path/to/public_key.pub + path: /path/to/certificate + valid_from: "2001-01-21" + valid_to: "2019-01-21" + +# Generate an OpenSSH user Certificate with clear and force-command option: +- openssh_cert: + type: user + signing_key: /path/to/private_key + public_key: /path/to/public_key.pub + path: /path/to/certificate + valid_from: always + valid_to: forever + options: + - "clear" + - "force-command=/tmp/bla/foo" + +''' + +RETURN = ''' +type: + description: type of the certificate (host or user) + returned: changed or success + type: str + sample: host +filename: + description: path to the certificate + returned: changed or success + type: str + sample: /tmp/certificate-cert.pub +info: + description: Information about the certificate. Output of C(ssh-keygen -L -f). + returned: change or success + type: list + elements: str + +''' + +import os +import errno +import re +import tempfile + +from datetime import datetime +from datetime import MINYEAR, MAXYEAR +from shutil import copy2 +from shutil import rmtree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.crypto.plugins.module_utils.crypto import convert_relative_to_datetime +from ansible.module_utils._text import to_native + + +class CertificateError(Exception): + pass + + +class Certificate(object): + + def __init__(self, module): + self.state = module.params['state'] + self.force = module.params['force'] + self.type = module.params['type'] + self.signing_key = module.params['signing_key'] + self.public_key = module.params['public_key'] + self.path = module.params['path'] + self.identifier = module.params['identifier'] + self.serial_number = module.params['serial_number'] + self.valid_from = module.params['valid_from'] + self.valid_to = module.params['valid_to'] + self.valid_at = module.params['valid_at'] + self.principals = module.params['principals'] + self.options = module.params['options'] + self.changed = False + self.check_mode = module.check_mode + self.cert_info = {} + + if self.state == 'present': + + if self.options and self.type == "host": + module.fail_json(msg="Options can only be used with user certificates.") + + if self.valid_at: + self.valid_at = self.valid_at.lstrip() + + self.valid_from = self.valid_from.lstrip() + self.valid_to = self.valid_to.lstrip() + + self.ssh_keygen = module.get_bin_path('ssh-keygen', True) + + def generate(self, module): + + if not self.is_valid(module, perms_required=False) or self.force: + args = [ + self.ssh_keygen, + '-s', self.signing_key + ] + + validity = "" + + if not (self.valid_from == "always" and self.valid_to == "forever"): + + if not self.valid_from == "always": + timeobj = self.convert_to_datetime(module, self.valid_from) + validity += ( + str(timeobj.year).zfill(4) + + str(timeobj.month).zfill(2) + + str(timeobj.day).zfill(2) + + str(timeobj.hour).zfill(2) + + str(timeobj.minute).zfill(2) + + str(timeobj.second).zfill(2) + ) + else: + validity += "19700101010101" + + validity += ":" + + if self.valid_to == "forever": + # on ssh-keygen versions that have the year 2038 bug this will cause the datetime to be 2038-01-19T04:14:07 + timeobj = datetime(MAXYEAR, 12, 31) + else: + timeobj = self.convert_to_datetime(module, self.valid_to) + + validity += ( + str(timeobj.year).zfill(4) + + str(timeobj.month).zfill(2) + + str(timeobj.day).zfill(2) + + str(timeobj.hour).zfill(2) + + str(timeobj.minute).zfill(2) + + str(timeobj.second).zfill(2) + ) + + args.extend(["-V", validity]) + + if self.type == 'host': + args.extend(['-h']) + + if self.identifier: + args.extend(['-I', self.identifier]) + else: + args.extend(['-I', ""]) + + if self.serial_number is not None: + args.extend(['-z', str(self.serial_number)]) + + if self.principals: + args.extend(['-n', ','.join(self.principals)]) + + if self.options: + for option in self.options: + args.extend(['-O']) + args.extend([option]) + + args.extend(['-P', '']) + + try: + temp_directory = tempfile.mkdtemp() + copy2(self.public_key, temp_directory) + args.extend([temp_directory + "/" + os.path.basename(self.public_key)]) + module.run_command(args, environ_update=dict(TZ="UTC"), check_rc=True) + copy2(temp_directory + "/" + os.path.splitext(os.path.basename(self.public_key))[0] + "-cert.pub", self.path) + rmtree(temp_directory, ignore_errors=True) + proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path]) + self.cert_info = proc[1].split() + self.changed = True + except Exception as e: + try: + self.remove() + rmtree(temp_directory, ignore_errors=True) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise CertificateError(exc) + else: + pass + module.fail_json(msg="%s" % to_native(e)) + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def convert_to_datetime(self, module, timestring): + + if self.is_relative(timestring): + result = convert_relative_to_datetime(timestring) + if result is None: + module.fail_json( + msg="'%s' is not a valid time format." % timestring) + else: + return result + else: + formats = ["%Y-%m-%d", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%dT%H:%M:%S", + ] + for fmt in formats: + try: + return datetime.strptime(timestring, fmt) + except ValueError: + pass + module.fail_json(msg="'%s' is not a valid time format" % timestring) + + def is_relative(self, timestr): + if timestr.startswith("+") or timestr.startswith("-"): + return True + return False + + def is_same_datetime(self, datetime_one, datetime_two): + + # This function is for backwards compatibility only because .total_seconds() is new in python2.7 + def timedelta_total_seconds(time_delta): + return (time_delta.microseconds + 0.0 + (time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6 + # try to use .total_ seconds() from python2.7 + try: + return (datetime_one - datetime_two).total_seconds() == 0.0 + except AttributeError: + return timedelta_total_seconds(datetime_one - datetime_two) == 0.0 + + def is_valid(self, module, perms_required=True): + + def _check_state(): + return os.path.exists(self.path) + + if _check_state(): + proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path], environ_update=dict(TZ="UTC"), check_rc=False) + if proc[0] != 0: + return False + self.cert_info = proc[1].split() + principals = re.findall("(?<=Principals:)(.*)(?=Critical)", proc[1], re.S)[0].split() + principals = list(map(str.strip, principals)) + if principals == ["(none)"]: + principals = None + cert_type = re.findall("( user | host )", proc[1])[0].strip() + serial_number = re.search(r"Serial: (\d+)", proc[1]).group(1) + validity = re.findall("(from (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}) to (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}))", proc[1]) + if validity: + if validity[0][1]: + cert_valid_from = self.convert_to_datetime(module, validity[0][1]) + if self.is_same_datetime(cert_valid_from, self.convert_to_datetime(module, "1970-01-01 01:01:01")): + cert_valid_from = datetime(MINYEAR, 1, 1) + else: + cert_valid_from = datetime(MINYEAR, 1, 1) + + if validity[0][3]: + cert_valid_to = self.convert_to_datetime(module, validity[0][3]) + if self.is_same_datetime(cert_valid_to, self.convert_to_datetime(module, "2038-01-19 03:14:07")): + cert_valid_to = datetime(MAXYEAR, 12, 31) + else: + cert_valid_to = datetime(MAXYEAR, 12, 31) + else: + cert_valid_from = datetime(MINYEAR, 1, 1) + cert_valid_to = datetime(MAXYEAR, 12, 31) + else: + return False + + def _check_perms(module): + file_args = module.load_file_common_arguments(module.params) + return not module.set_fs_attributes_if_different(file_args, False) + + def _check_serial_number(): + if self.serial_number is None: + return True + return self.serial_number == int(serial_number) + + def _check_type(): + return self.type == cert_type + + def _check_principals(): + if not principals or not self.principals: + return self.principals == principals + return set(self.principals) == set(principals) + + def _check_validity(module): + if self.valid_from == "always": + earliest_time = datetime(MINYEAR, 1, 1) + elif self.is_relative(self.valid_from): + earliest_time = None + else: + earliest_time = self.convert_to_datetime(module, self.valid_from) + + if self.valid_to == "forever": + last_time = datetime(MAXYEAR, 12, 31) + elif self.is_relative(self.valid_to): + last_time = None + else: + last_time = self.convert_to_datetime(module, self.valid_to) + + if earliest_time: + if not self.is_same_datetime(earliest_time, cert_valid_from): + return False + if last_time: + if not self.is_same_datetime(last_time, cert_valid_to): + return False + + if self.valid_at: + if cert_valid_from <= self.convert_to_datetime(module, self.valid_at) <= cert_valid_to: + return True + + if earliest_time and last_time: + return True + + return False + + if perms_required and not _check_perms(module): + return False + + return _check_type() and _check_principals() and _check_validity(module) and _check_serial_number() + + def dump(self): + + """Serialize the object into a dictionary.""" + + def filter_keywords(arr, keywords): + concated = [] + string = "" + for word in arr: + if word in keywords: + concated.append(string) + string = word + else: + string += " " + word + concated.append(string) + # drop the certificate path + concated.pop(0) + return concated + + def format_cert_info(): + return filter_keywords(self.cert_info, [ + "Type:", + "Public", + "Signing", + "Key", + "Serial:", + "Valid:", + "Principals:", + "Critical", + "Extensions:"]) + + if self.state == 'present': + result = { + 'changed': self.changed, + 'type': self.type, + 'filename': self.path, + 'info': format_cert_info(), + } + else: + result = { + 'changed': self.changed, + } + + return result + + def remove(self): + """Remove the resource from the filesystem.""" + + try: + os.remove(self.path) + self.changed = True + except OSError as exc: + if exc.errno != errno.ENOENT: + raise CertificateError(exc) + else: + pass + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['absent', 'present']), + force=dict(type='bool', default=False), + type=dict(type='str', choices=['host', 'user']), + signing_key=dict(type='path'), + public_key=dict(type='path'), + path=dict(type='path', required=True), + identifier=dict(type='str'), + serial_number=dict(type='int'), + valid_from=dict(type='str'), + valid_to=dict(type='str'), + valid_at=dict(type='str'), + principals=dict(type='list', elements='str'), + options=dict(type='list', elements='str'), + ), + supports_check_mode=True, + add_file_common_args=True, + required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])], + ) + + def isBaseDir(path): + base_dir = os.path.dirname(path) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + if module.params['state'] == "present": + isBaseDir(module.params['signing_key']) + isBaseDir(module.params['public_key']) + + isBaseDir(module.params['path']) + + certificate = Certificate(module) + + if certificate.state == 'present': + + if module.check_mode: + certificate.changed = module.params['force'] or not certificate.is_valid(module) + else: + try: + certificate.generate(module) + except Exception as exc: + module.fail_json(msg=to_native(exc)) + + else: + + if module.check_mode: + certificate.changed = os.path.exists(module.params['path']) + if certificate.changed: + certificate.cert_info = {} + else: + try: + certificate.remove() + except Exception as exc: + module.fail_json(msg=to_native(exc)) + + result = certificate.dump() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/openssh_keypair.py b/plugins/modules/openssh_keypair.py new file mode 100644 index 00000000..1c86a1ec --- /dev/null +++ b/plugins/modules/openssh_keypair.py @@ -0,0 +1,490 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, David Kainz +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: openssh_keypair +author: "David Kainz (@lolcube)" +short_description: Generate OpenSSH private and public keys. +description: + - "This module allows one to (re)generate OpenSSH private and public keys. It uses + ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519) + or C(ecdsa) private keys." +requirements: + - "ssh-keygen" +options: + state: + description: + - Whether the private and public keys should exist or not, taking action if the state is different from what is stated. + type: str + default: present + choices: [ present, absent ] + size: + description: + - "Specifies the number of bits in the private key to create. For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. + Generally, 2048 bits is considered sufficient. DSA keys must be exactly 1024 bits as specified by FIPS 186-2. + For ECDSA keys, size determines the key length by selecting from one of three elliptic curve sizes: 256, 384 or 521 bits. + Attempting to use bit lengths other than these three values for ECDSA keys will cause this module to fail. + Ed25519 keys have a fixed length and the size will be ignored." + type: int + type: + description: + - "The algorithm used to generate the SSH private key. C(rsa1) is for protocol version 1. + C(rsa1) is deprecated and may not be supported by every version of ssh-keygen." + type: str + default: rsa + choices: ['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519'] + force: + description: + - Should the key be regenerated even if it already exists + type: bool + default: false + path: + description: + - Name of the files containing the public and private key. The file containing the public key will have the extension C(.pub). + type: path + required: true + comment: + description: + - Provides a new comment to the public key. When checking if the key is in the correct state this will be ignored. + type: str + regenerate: + description: + - Allows to configure in which situations the module is allowed to regenerate private keys. + The module will always generate a new key if the destination file does not exist. + - By default, the key will be regenerated when it doesn't match the module's options, + except when the key cannot be read or the passphrase does not match. Please note that + this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence) + is specified. + - If set to C(never), the module will fail if the key cannot be read or the passphrase + isn't matching, and will never regenerate an existing key. + - If set to C(fail), the module will fail if the key does not correspond to the module's + options. + - If set to C(partial_idempotence), the key will be regenerated if it does not conform to + the module's options. The key is B(not) regenerated if it cannot be read (broken file), + the key is protected by an unknown passphrase, or when they key is not protected by a + passphrase, but a passphrase is specified. + - If set to C(full_idempotence), the key will be regenerated if it does not conform to the + module's options. This is also the case if the key cannot be read (broken file), the key + is protected by an unknown passphrase, or when they key is not protected by a passphrase, + but a passphrase is specified. Make sure you have a B(backup) when using this option! + - If set to C(always), the module will always regenerate the key. This is equivalent to + setting I(force) to C(yes). + - Note that adjusting the comment and the permissions can be changed without regeneration. + Therefore, even for C(never), the task can result in changed. + type: str + choices: + - never + - fail + - partial_idempotence + - full_idempotence + - always + default: partial_idempotence +notes: + - In case the ssh key is broken or password protected, the module will fail. Set the I(force) option to C(yes) if you want to regenerate the keypair. + +extends_documentation_fragment: files +''' + +EXAMPLES = ''' +# Generate an OpenSSH keypair with the default values (4096 bits, rsa) +- openssh_keypair: + path: /tmp/id_ssh_rsa + +# Generate an OpenSSH rsa keypair with a different size (2048 bits) +- openssh_keypair: + path: /tmp/id_ssh_rsa + size: 2048 + +# Force regenerate an OpenSSH keypair if it already exists +- openssh_keypair: + path: /tmp/id_ssh_rsa + force: True + +# Generate an OpenSSH keypair with a different algorithm (dsa) +- openssh_keypair: + path: /tmp/id_ssh_dsa + type: dsa +''' + +RETURN = ''' +size: + description: Size (in bits) of the SSH private key + returned: changed or success + type: int + sample: 4096 +type: + description: Algorithm used to generate the SSH private key + returned: changed or success + type: str + sample: rsa +filename: + description: Path to the generated SSH private key file + returned: changed or success + type: str + sample: /tmp/id_ssh_rsa +fingerprint: + description: The fingerprint of the key. + returned: changed or success + type: str + sample: SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM +public_key: + description: The public key of the generated SSH private key + returned: changed or success + type: str + sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw== test_key +comment: + description: The comment of the generated key + returned: changed or success + type: str + sample: test@comment +''' + +import os +import stat +import errno + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +class KeypairError(Exception): + pass + + +class Keypair(object): + + def __init__(self, module): + self.path = module.params['path'] + self.state = module.params['state'] + self.force = module.params['force'] + self.size = module.params['size'] + self.type = module.params['type'] + self.comment = module.params['comment'] + self.changed = False + self.check_mode = module.check_mode + self.privatekey = None + self.fingerprint = {} + self.public_key = {} + self.regenerate = module.params['regenerate'] + if self.regenerate == 'always': + self.force = True + + if self.type in ('rsa', 'rsa1'): + self.size = 4096 if self.size is None else self.size + if self.size < 1024: + module.fail_json(msg=('For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. ' + 'Attempting to use bit lengths under 1024 will cause the module to fail.')) + + if self.type == 'dsa': + self.size = 1024 if self.size is None else self.size + if self.size != 1024: + module.fail_json(msg=('DSA keys must be exactly 1024 bits as specified by FIPS 186-2.')) + + if self.type == 'ecdsa': + self.size = 256 if self.size is None else self.size + if self.size not in (256, 384, 521): + module.fail_json(msg=('For ECDSA keys, size determines the key length by selecting from ' + 'one of three elliptic curve sizes: 256, 384 or 521 bits. ' + 'Attempting to use bit lengths other than these three values for ' + 'ECDSA keys will cause this module to fail. ')) + if self.type == 'ed25519': + self.size = 256 + + def generate(self, module): + # generate a keypair + if self.force or not self.isPrivateKeyValid(module, perms_required=False): + args = [ + module.get_bin_path('ssh-keygen', True), + '-q', + '-N', '', + '-b', str(self.size), + '-t', self.type, + '-f', self.path, + ] + + if self.comment: + args.extend(['-C', self.comment]) + else: + args.extend(['-C', ""]) + + try: + if os.path.exists(self.path) and not os.access(self.path, os.W_OK): + os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR) + self.changed = True + stdin_data = None + if os.path.exists(self.path): + stdin_data = 'y' + module.run_command(args, data=stdin_data) + proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path]) + self.fingerprint = proc[1].split() + pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) + self.public_key = pubkey[1].strip('\n') + except Exception as e: + self.remove() + module.fail_json(msg="%s" % to_native(e)) + + elif not self.isPublicKeyValid(module, perms_required=False): + pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) + pubkey = pubkey[1].strip('\n') + try: + self.changed = True + with open(self.path + ".pub", "w") as pubkey_f: + pubkey_f.write(pubkey + '\n') + os.chmod(self.path + ".pub", stat.S_IWUSR + stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH) + except IOError: + module.fail_json( + msg='The public key is missing or does not match the private key. ' + 'Unable to regenerate the public key.') + self.public_key = pubkey + + if self.comment: + try: + if os.path.exists(self.path) and not os.access(self.path, os.W_OK): + os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR) + args = [module.get_bin_path('ssh-keygen', True), + '-q', '-o', '-c', '-C', self.comment, '-f', self.path] + module.run_command(args) + except IOError: + module.fail_json( + msg='Unable to update the comment for the public key.') + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + file_args['path'] = file_args['path'] + '.pub' + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def _check_pass_protected_or_broken_key(self, module): + key_state = module.run_command([module.get_bin_path('ssh-keygen', True), + '-P', '', '-yf', self.path], check_rc=False) + if key_state[0] == 255 or 'is not a public key file' in key_state[2]: + return True + if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]: + return True + return False + + def isPrivateKeyValid(self, module, perms_required=True): + + # check if the key is correct + def _check_state(): + return os.path.exists(self.path) + + if not _check_state(): + return False + + if self._check_pass_protected_or_broken_key(module): + if self.regenerate in ('full_idempotence', 'always'): + return False + module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `full_idempotence` or `always`, or with `force=yes`.') + + proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path], check_rc=False) + if not proc[0] == 0: + if os.path.isdir(self.path): + module.fail_json(msg='%s is a directory. Please specify a path to a file.' % (self.path)) + + if self.regenerate in ('full_idempotence', 'always'): + return False + module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `full_idempotence` or `always`, or with `force=yes`.') + + fingerprint = proc[1].split() + keysize = int(fingerprint[0]) + keytype = fingerprint[-1][1:-1].lower() + + self.fingerprint = fingerprint + + if self.regenerate == 'never': + return True + + def _check_type(): + return self.type == keytype + + def _check_size(): + return self.size == keysize + + if not (_check_type() and _check_size()): + if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): + return False + module.fail_json(msg='Key has wrong type and/or size.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.') + + def _check_perms(module): + file_args = module.load_file_common_arguments(module.params) + return not module.set_fs_attributes_if_different(file_args, False) + + return not perms_required or _check_perms(module) + + def isPublicKeyValid(self, module, perms_required=True): + + def _get_pubkey_content(): + if os.path.exists(self.path + ".pub"): + with open(self.path + ".pub", "r") as pubkey_f: + present_pubkey = pubkey_f.read().strip(' \n') + return present_pubkey + else: + return False + + def _parse_pubkey(pubkey_content): + if pubkey_content: + parts = pubkey_content.split(' ', 2) + if len(parts) < 2: + return False + return parts[0], parts[1], '' if len(parts) <= 2 else parts[2] + return False + + def _pubkey_valid(pubkey): + if pubkey_parts and _parse_pubkey(pubkey): + return pubkey_parts[:2] == _parse_pubkey(pubkey)[:2] + return False + + def _comment_valid(): + if pubkey_parts: + return pubkey_parts[2] == self.comment + return False + + def _check_perms(module): + file_args = module.load_file_common_arguments(module.params) + file_args['path'] = file_args['path'] + '.pub' + return not module.set_fs_attributes_if_different(file_args, False) + + pubkey_parts = _parse_pubkey(_get_pubkey_content()) + + pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) + pubkey = pubkey[1].strip('\n') + if _pubkey_valid(pubkey): + self.public_key = pubkey + else: + return False + + if self.comment: + if not _comment_valid(): + return False + + if perms_required: + if not _check_perms(module): + return False + + return True + + def dump(self): + # return result as a dict + + """Serialize the object into a dictionary.""" + result = { + 'changed': self.changed, + 'size': self.size, + 'type': self.type, + 'filename': self.path, + # On removal this has no value + 'fingerprint': self.fingerprint[1] if self.fingerprint else '', + 'public_key': self.public_key, + 'comment': self.comment if self.comment else '', + } + + return result + + def remove(self): + """Remove the resource from the filesystem.""" + + try: + os.remove(self.path) + self.changed = True + except OSError as exc: + if exc.errno != errno.ENOENT: + raise KeypairError(exc) + else: + pass + + if os.path.exists(self.path + ".pub"): + try: + os.remove(self.path + ".pub") + self.changed = True + except OSError as exc: + if exc.errno != errno.ENOENT: + raise KeypairError(exc) + else: + pass + + +def main(): + + # Define Ansible Module + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + size=dict(type='int'), + type=dict(type='str', default='rsa', choices=['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + comment=dict(type='str'), + regenerate=dict( + type='str', + default='partial_idempotence', + choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] + ), + ), + supports_check_mode=True, + add_file_common_args=True, + ) + + # Check if Path exists + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + keypair = Keypair(module) + + if keypair.state == 'present': + + if module.check_mode: + result = keypair.dump() + result['changed'] = keypair.force or not keypair.isPrivateKeyValid(module) or not keypair.isPublicKeyValid(module) + module.exit_json(**result) + + try: + keypair.generate(module) + except Exception as exc: + module.fail_json(msg=to_native(exc)) + else: + + if module.check_mode: + keypair.changed = os.path.exists(module.params['path']) + if keypair.changed: + keypair.fingerprint = {} + result = keypair.dump() + module.exit_json(**result) + + try: + keypair.remove() + except Exception as exc: + module.fail_json(msg=to_native(exc)) + + result = keypair.dump() + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/openssl_certificate.py b/plugins/modules/openssl_certificate.py new file mode 100644 index 00000000..09341ba8 --- /dev/null +++ b/plugins/modules/openssl_certificate.py @@ -0,0 +1,2723 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016-2017, Yanis Guenane +# Copyright: (c) 2017, Markus Teufelberger +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_certificate +short_description: Generate and/or check OpenSSL certificates +description: + - This module allows one to (re)generate OpenSSL certificates. + - It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly), C(entrust)) + for your certificate. + - The C(assertonly) provider is intended for use cases where one is only interested in + checking properties of a supplied certificate. Please note that this provider has been + deprecated in Ansible 2.9 and will be removed in Ansible 2.13. See the examples on how + to emulate C(assertonly) usage with M(openssl_certificate_info), M(openssl_csr_info), + M(openssl_privatekey_info) and M(assert). This also allows more flexible checks than + the ones offered by the C(assertonly) provider. + - The C(ownca) provider is intended for generating OpenSSL certificate signed with your own + CA (Certificate Authority) certificate (self-signed certificate). + - Many properties that can be specified in this module are for validation of an + existing or newly generated certificate. The proper place to specify them, if you + want to receive a certificate with these properties is a CSR (Certificate Signing Request). + - "Please note that the module regenerates existing certificate if it doesn't match the module's + options, or if it seems to be corrupt. If you are concerned that this could overwrite + your existing certificate, consider using the I(backup) option." + - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. + - If both the cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) + cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with C(select_crypto_backend)). + Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13. +requirements: + - PyOpenSSL >= 0.15 or cryptography >= 1.6 (if using C(selfsigned) or C(assertonly) provider) + - acme-tiny >= 4.0.0 (if using the C(acme) provider) +author: + - Yanis Guenane (@Spredzy) + - Markus Teufelberger (@MarkusTeufelberger) +options: + state: + description: + - Whether the certificate should exist or not, taking action if the state is different from what is stated. + type: str + default: present + choices: [ absent, present ] + + path: + description: + - Remote absolute path where the generated certificate file should be created or is already located. + type: path + required: true + + provider: + description: + - Name of the provider to use to generate/retrieve the OpenSSL certificate. + - The C(assertonly) provider will not generate files and fail if the certificate file is missing. + - The C(assertonly) provider has been deprecated in Ansible 2.9 and will be removed in Ansible 2.13. + Please see the examples on how to emulate it with M(openssl_certificate_info), M(openssl_csr_info), + M(openssl_privatekey_info) and M(assert). + - "The C(entrust) provider was added for Ansible 2.9 and requires credentials for the + L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API." + - Required if I(state) is C(present). + type: str + choices: [ acme, assertonly, entrust, ownca, selfsigned ] + + force: + description: + - Generate the certificate, even if it already exists. + type: bool + default: no + + csr_path: + description: + - Path to the Certificate Signing Request (CSR) used to generate this certificate. + - This is not required in C(assertonly) mode. + - This is mutually exclusive with I(csr_content). + type: path + csr_content: + description: + - Content of the Certificate Signing Request (CSR) used to generate this certificate. + - This is not required in C(assertonly) mode. + - This is mutually exclusive with I(csr_path). + type: str + + privatekey_path: + description: + - Path to the private key to use when signing the certificate. + - This is mutually exclusive with I(privatekey_content). + type: path + privatekey_content: + description: + - Path to the private key to use when signing the certificate. + - This is mutually exclusive with I(privatekey_path). + type: str + + privatekey_passphrase: + description: + - The passphrase for the I(privatekey_path) resp. I(privatekey_content). + - This is required if the private key is password protected. + type: str + + selfsigned_version: + description: + - Version of the C(selfsigned) certificate. + - Nowadays it should almost always be C(3). + - This is only used by the C(selfsigned) provider. + type: int + default: 3 + + selfsigned_digest: + description: + - Digest algorithm to be used when self-signing the certificate. + - This is only used by the C(selfsigned) provider. + type: str + default: sha256 + + selfsigned_not_before: + description: + - The point in time the certificate is valid from. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent. + - If this value is not specified, the certificate will start being valid from now. + - This is only used by the C(selfsigned) provider. + type: str + default: +0s + aliases: [ selfsigned_notBefore ] + + selfsigned_not_after: + description: + - The point in time at which the certificate stops being valid. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent. + - If this value is not specified, the certificate will stop being valid 10 years from now. + - This is only used by the C(selfsigned) provider. + type: str + default: +3650d + aliases: [ selfsigned_notAfter ] + + selfsigned_create_subject_key_identifier: + description: + - Whether to create the Subject Key Identifier (SKI) from the public key. + - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not + provide one. + - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is + ignored. + - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. + - This is only used by the C(selfsigned) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: str + choices: [create_if_not_provided, always_create, never_create] + default: create_if_not_provided + + ownca_path: + description: + - Remote absolute path of the CA (Certificate Authority) certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_content). + type: path + ownca_content: + description: + - Content of the CA (Certificate Authority) certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_path). + type: str + + ownca_privatekey_path: + description: + - Path to the CA (Certificate Authority) private key to use when signing the certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_privatekey_content). + type: path + ownca_privatekey_content: + description: + - Path to the CA (Certificate Authority) private key to use when signing the certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_privatekey_path). + type: str + + ownca_privatekey_passphrase: + description: + - The passphrase for the I(ownca_privatekey_path) resp. I(ownca_privatekey_content). + - This is only used by the C(ownca) provider. + type: str + + ownca_digest: + description: + - The digest algorithm to be used for the C(ownca) certificate. + - This is only used by the C(ownca) provider. + type: str + default: sha256 + + ownca_version: + description: + - The version of the C(ownca) certificate. + - Nowadays it should almost always be C(3). + - This is only used by the C(ownca) provider. + type: int + default: 3 + + ownca_not_before: + description: + - The point in time the certificate is valid from. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent. + - If this value is not specified, the certificate will start being valid from now. + - This is only used by the C(ownca) provider. + type: str + default: +0s + + ownca_not_after: + description: + - The point in time at which the certificate stops being valid. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent. + - If this value is not specified, the certificate will stop being valid 10 years from now. + - This is only used by the C(ownca) provider. + type: str + default: +3650d + + ownca_create_subject_key_identifier: + description: + - Whether to create the Subject Key Identifier (SKI) from the public key. + - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not + provide one. + - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is + ignored. + - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. + - This is only used by the C(ownca) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: str + choices: [create_if_not_provided, always_create, never_create] + default: create_if_not_provided + + ownca_create_authority_key_identifier: + description: + - Create a Authority Key Identifier from the CA's certificate. If the CSR provided + a authority key identifier, it is ignored. + - The Authority Key Identifier is generated from the CA certificate's Subject Key Identifier, + if available. If it is not available, the CA certificate's public key will be used. + - This is only used by the C(ownca) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: bool + default: yes + + acme_accountkey_path: + description: + - The path to the accountkey for the C(acme) provider. + - This is only used by the C(acme) provider. + type: path + + acme_challenge_path: + description: + - The path to the ACME challenge directory that is served on U(http://:80/.well-known/acme-challenge/) + - This is only used by the C(acme) provider. + type: path + + acme_chain: + description: + - Include the intermediate certificate to the generated certificate + - This is only used by the C(acme) provider. + - Note that this is only available for older versions of C(acme-tiny). + New versions include the chain automatically, and setting I(acme_chain) to C(yes) results in an error. + type: bool + default: no + + acme_directory: + description: + - "The ACME directory to use. You can use any directory that supports the ACME protocol, such as Buypass or Let's Encrypt." + - "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)." + type: str + default: https://acme-v02.api.letsencrypt.org/directory + + signature_algorithms: + description: + - A list of algorithms that you would accept the certificate to be signed with + (e.g. ['sha256WithRSAEncryption', 'sha512WithRSAEncryption']). + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: list + elements: str + + issuer: + description: + - The key/value pairs that must be present in the issuer name field of the certificate. + - If you need to specify more than one value with the same key, use a list as value. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: dict + + issuer_strict: + description: + - If set to C(yes), the I(issuer) field must contain only these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + + subject: + description: + - The key/value pairs that must be present in the subject name field of the certificate. + - If you need to specify more than one value with the same key, use a list as value. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: dict + + subject_strict: + description: + - If set to C(yes), the I(subject) field must contain only these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + + has_expired: + description: + - Checks if the certificate is expired/not expired at the time the module is executed. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + + version: + description: + - The version of the certificate. + - Nowadays it should almost always be 3. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: int + + valid_at: + description: + - The certificate must be valid at this point in time. + - The timestamp is formatted as an ASN.1 TIME. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: str + + invalid_at: + description: + - The certificate must be invalid at this point in time. + - The timestamp is formatted as an ASN.1 TIME. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: str + + not_before: + description: + - The certificate must start to become valid at this point in time. + - The timestamp is formatted as an ASN.1 TIME. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: str + aliases: [ notBefore ] + + not_after: + description: + - The certificate must expire at this point in time. + - The timestamp is formatted as an ASN.1 TIME. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: str + aliases: [ notAfter ] + + valid_in: + description: + - The certificate must still be valid at this relative time offset from now. + - Valid format is C([+-]timespec | number_of_seconds) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using this parameter, this module is NOT idempotent. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: str + + key_usage: + description: + - The I(key_usage) extension field must contain all these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: list + elements: str + aliases: [ keyUsage ] + + key_usage_strict: + description: + - If set to C(yes), the I(key_usage) extension field must contain only these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + aliases: [ keyUsage_strict ] + + extended_key_usage: + description: + - The I(extended_key_usage) extension field must contain all these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: list + elements: str + aliases: [ extendedKeyUsage ] + + extended_key_usage_strict: + description: + - If set to C(yes), the I(extended_key_usage) extension field must contain only these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + aliases: [ extendedKeyUsage_strict ] + + subject_alt_name: + description: + - The I(subject_alt_name) extension field must contain these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: list + elements: str + aliases: [ subjectAltName ] + + subject_alt_name_strict: + description: + - If set to C(yes), the I(subject_alt_name) extension field must contain only these values. + - This is only used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + aliases: [ subjectAltName_strict ] + + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. + From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + + backup: + description: + - Create a backup file including a timestamp so you can get the original + certificate back if you overwrote it with a new one by accident. + - This is not used by the C(assertonly) provider. + - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + + entrust_cert_type: + description: + - Specify the type of certificate requested. + - This is only used by the C(entrust) provider. + type: str + default: STANDARD_SSL + choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ] + + entrust_requester_email: + description: + - The email of the requester of the certificate (for tracking purposes). + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_requester_name: + description: + - The name of the requester of the certificate (for tracking purposes). + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_requester_phone: + description: + - The phone number of the requester of the certificate (for tracking purposes). + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_api_user: + description: + - The username for authentication to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_api_key: + description: + - The key (password) for authentication to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_api_client_cert_path: + description: + - The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: path + + entrust_api_client_cert_key_path: + description: + - The path to the private key of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: path + + entrust_not_after: + description: + - The point in time at which the certificate stops being valid. + - Time can be specified either as relative time or as an absolute timestamp. + - A valid absolute time format is C(ASN.1 TIME) such as C(2019-06-18). + - A valid relative time format is C([+-]timespec) where timespec can be an integer + C([w | d | h | m | s]), such as C(+365d) or C(+32w1d2h)). + - Time will always be interpreted as UTC. + - Note that only the date (day, month, year) is supported for specifying the expiry date of the issued certificate. + - The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day + earlier than expected if a relative time is used. + - The minimum certificate lifetime is 90 days, and maximum is three years. + - If this value is not specified, the certificate will stop being valid 365 days the date of issue. + - This is only used by the C(entrust) provider. + type: str + default: +365d + + entrust_api_specification_path: + description: + - The path to the specification file defining the Entrust Certificate Services (ECS) API configuration. + - You can use this to keep a local copy of the specification to avoid downloading it every time the module is used. + - This is only used by the C(entrust) provider. + type: path + default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml + + return_content: + description: + - If set to C(yes), will return the (current or generated) certificate's content as I(certificate). + type: bool + default: no + +extends_documentation_fragment: files +notes: + - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern. + - Date specified should be UTC. Minutes and seconds are mandatory. + - For security reason, when you use C(ownca) provider, you should NOT run M(openssl_certificate) on + a target machine, but on a dedicated CA machine. It is recommended not to store the CA private key + on the target machine. Once signed, the certificate can be moved to the target machine. +seealso: +- module: openssl_csr +- module: openssl_dhparam +- module: openssl_pkcs12 +- module: openssl_privatekey +- module: openssl_publickey +''' + +EXAMPLES = r''' +- name: Generate a Self Signed OpenSSL certificate + openssl_certificate: + path: /etc/ssl/crt/ansible.com.crt + privatekey_path: /etc/ssl/private/ansible.com.pem + csr_path: /etc/ssl/csr/ansible.com.csr + provider: selfsigned + +- name: Generate an OpenSSL certificate signed with your own CA certificate + openssl_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + ownca_path: /etc/ssl/crt/ansible_CA.crt + ownca_privatekey_path: /etc/ssl/private/ansible_CA.pem + provider: ownca + +- name: Generate a Let's Encrypt Certificate + openssl_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + provider: acme + acme_accountkey_path: /etc/ssl/private/ansible.com.pem + acme_challenge_path: /etc/ssl/challenges/ansible.com/ + +- name: Force (re-)generate a new Let's Encrypt Certificate + openssl_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + provider: acme + acme_accountkey_path: /etc/ssl/private/ansible.com.pem + acme_challenge_path: /etc/ssl/challenges/ansible.com/ + force: yes + +- name: Generate an Entrust certificate via the Entrust Certificate Services (ECS) API + openssl_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + provider: entrust + entrust_requester_name: Jo Doe + entrust_requester_email: jdoe@ansible.com + entrust_requester_phone: 555-555-5555 + entrust_cert_type: STANDARD_SSL + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt + entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml + +# The following example shows one assertonly usage using all existing options for +# assertonly, and shows how to emulate the behavior with the openssl_certificate_info, +# openssl_csr_info, openssl_privatekey_info and assert modules: + +- openssl_certificate: + provider: assertonly + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + privatekey_path: /etc/ssl/csr/ansible.com.key + signature_algorithms: + - sha256WithRSAEncryption + - sha512WithRSAEncryption + subject: + commonName: ansible.com + subject_strict: yes + issuer: + commonName: ansible.com + issuer_strict: yes + has_expired: no + version: 3 + key_usage: + - Data Encipherment + key_usage_strict: yes + extended_key_usage: + - DVCS + extended_key_usage_strict: yes + subject_alt_name: + - dns:ansible.com + subject_alt_name_strict: yes + not_before: 20190331202428Z + not_after: 20190413202428Z + valid_at: "+1d10h" + invalid_at: 20200331202428Z + valid_in: 10 # in ten seconds + +- openssl_certificate_info: + path: /etc/ssl/crt/ansible.com.crt + # for valid_at, invalid_at and valid_in + valid_at: + one_day_ten_hours: "+1d10h" + fixed_timestamp: 20200331202428Z + ten_seconds: "+10" + register: result + +- openssl_csr_info: + # Verifies that the CSR signature is valid; module will fail if not + path: /etc/ssl/csr/ansible.com.csr + register: result_csr + +- openssl_privatekey_info: + path: /etc/ssl/csr/ansible.com.key + register: result_privatekey + +- assert: + that: + # When private key is specified for assertonly, this will be checked: + - result.public_key == result_privatekey.public_key + # When CSR is specified for assertonly, this will be checked: + - result.public_key == result_csr.public_key + - result.subject_ordered == result_csr.subject_ordered + - result.extensions_by_oid == result_csr.extensions_by_oid + # signature_algorithms check + - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha512WithRSAEncryption'" + # subject and subject_strict + - "result.subject.commonName == 'ansible.com'" + - "result.subject | length == 1" # the number must be the number of entries you check for + # issuer and issuer_strict + - "result.issuer.commonName == 'ansible.com'" + - "result.issuer | length == 1" # the number must be the number of entries you check for + # has_expired + - not result.expired + # version + - result.version == 3 + # key_usage and key_usage_strict + - "'Data Encipherment' in result.key_usage" + - "result.key_usage | length == 1" # the number must be the number of entries you check for + # extended_key_usage and extended_key_usage_strict + - "'DVCS' in result.extended_key_usage" + - "result.extended_key_usage | length == 1" # the number must be the number of entries you check for + # subject_alt_name and subject_alt_name_strict + - "'dns:ansible.com' in result.subject_alt_name" + - "result.subject_alt_name | length == 1" # the number must be the number of entries you check for + # not_before and not_after + - "result.not_before == '20190331202428Z'" + - "result.not_after == '20190413202428Z'" + # valid_at, invalid_at and valid_in + - "result.valid_at.one_day_ten_hours" # for valid_at + - "not result.valid_at.fixed_timestamp" # for invalid_at + - "result.valid_at.ten_seconds" # for valid_in + +# Examples for some checks one could use the assertonly provider for: +# (Please note that assertonly has been deprecated!) + +# How to use the assertonly provider to implement and trigger your own custom certificate generation workflow: +- name: Check if a certificate is currently still valid, ignoring failures + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + has_expired: no + ignore_errors: yes + register: validity_check + +- name: Run custom task(s) to get a new, valid certificate in case the initial check failed + command: superspecialSSL recreate /etc/ssl/crt/example.com.crt + when: validity_check.failed + +- name: Check the new certificate again for validity with the same parameters, this time failing the play if it is still invalid + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + has_expired: no + when: validity_check.failed + +# Some other checks that assertonly could be used for: +- name: Verify that an existing certificate was issued by the Let's Encrypt CA and is currently still valid + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + issuer: + O: Let's Encrypt + has_expired: no + +- name: Ensure that a certificate uses a modern signature algorithm (no SHA1, MD5 or DSA) + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + signature_algorithms: + - sha224WithRSAEncryption + - sha256WithRSAEncryption + - sha384WithRSAEncryption + - sha512WithRSAEncryption + - sha224WithECDSAEncryption + - sha256WithECDSAEncryption + - sha384WithECDSAEncryption + - sha512WithECDSAEncryption + +- name: Ensure that the existing certificate belongs to the specified private key + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + privatekey_path: /etc/ssl/private/example.com.pem + provider: assertonly + +- name: Ensure that the existing certificate is still valid at the winter solstice 2017 + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + valid_at: 20171221162800Z + +- name: Ensure that the existing certificate is still valid 2 weeks (1209600 seconds) from now + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + valid_in: 1209600 + +- name: Ensure that the existing certificate is only used for digital signatures and encrypting other keys + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + key_usage: + - digitalSignature + - keyEncipherment + key_usage_strict: true + +- name: Ensure that the existing certificate can be used for client authentication + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + extended_key_usage: + - clientAuth + +- name: Ensure that the existing certificate can only be used for client authentication and time stamping + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + extended_key_usage: + - clientAuth + - 1.3.6.1.5.5.7.3.8 + extended_key_usage_strict: true + +- name: Ensure that the existing certificate has a certain domain in its subjectAltName + openssl_certificate: + path: /etc/ssl/crt/example.com.crt + provider: assertonly + subject_alt_name: + - www.example.com + - test.example.com +''' + +RETURN = r''' +filename: + description: Path to the generated certificate. + returned: changed or success + type: str + sample: /etc/ssl/crt/www.ansible.com.crt +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~ +certificate: + description: The (current or generated) certificate's content. + returned: if I(state) is C(present) and I(return_content) is C(yes) + type: str + version_added: "2.10" +''' + + +from random import randint +import abc +import datetime +import time +import os +import tempfile +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native, to_bytes, to_text +from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress as compat_ipaddress +from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' +MINIMAL_PYOPENSSL_VERSION = '0.15' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import Encoding + from cryptography.x509 import NameAttribute, Name + from cryptography.x509.oid import NameOID + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +class CertificateError(crypto_utils.OpenSSLObjectError): + pass + + +class Certificate(crypto_utils.OpenSSLObject): + + def __init__(self, module, backend): + super(Certificate, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + + self.provider = module.params['provider'] + self.privatekey_path = module.params['privatekey_path'] + self.privatekey_content = module.params['privatekey_content'] + if self.privatekey_content is not None: + self.privatekey_content = self.privatekey_content.encode('utf-8') + self.privatekey_passphrase = module.params['privatekey_passphrase'] + self.csr_path = module.params['csr_path'] + self.csr_content = module.params['csr_content'] + if self.csr_content is not None: + self.csr_content = self.csr_content.encode('utf-8') + self.cert = None + self.privatekey = None + self.csr = None + self.backend = backend + self.module = module + self.return_content = module.params['return_content'] + + # The following are default values which make sure check() works as + # before if providers do not explicitly change these properties. + self.create_subject_key_identifier = 'never_create' + self.create_authority_key_identifier = False + + self.backup = module.params['backup'] + self.backup_file = None + + def _validate_privatekey(self): + if self.backend == 'pyopenssl': + ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD) + ctx.use_privatekey(self.privatekey) + ctx.use_certificate(self.cert) + try: + ctx.check_privatekey() + return True + except OpenSSL.SSL.Error: + return False + elif self.backend == 'cryptography': + return crypto_utils.cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) + + def _validate_csr(self): + if self.backend == 'pyopenssl': + # Verify that CSR is signed by certificate's private key + try: + self.csr.verify(self.cert.get_pubkey()) + except OpenSSL.crypto.Error: + return False + # Check subject + if self.csr.get_subject() != self.cert.get_subject(): + return False + # Check extensions + csr_extensions = self.csr.get_extensions() + cert_extension_count = self.cert.get_extension_count() + if len(csr_extensions) != cert_extension_count: + return False + for extension_number in range(0, cert_extension_count): + cert_extension = self.cert.get_extension(extension_number) + csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions) + if cert_extension.get_data() != list(csr_extension)[0].get_data(): + return False + return True + elif self.backend == 'cryptography': + # Verify that CSR is signed by certificate's private key + if not self.csr.is_signature_valid: + return False + if not crypto_utils.cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()): + return False + # Check subject + if self.csr.subject != self.cert.subject: + return False + # Check extensions + cert_exts = list(self.cert.extensions) + csr_exts = list(self.csr.extensions) + if self.create_subject_key_identifier != 'never_create': + # Filter out SubjectKeyIdentifier extension before comparison + cert_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), cert_exts)) + csr_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), csr_exts)) + if self.create_authority_key_identifier: + # Filter out AuthorityKeyIdentifier extension before comparison + cert_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), cert_exts)) + csr_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), csr_exts)) + if len(cert_exts) != len(csr_exts): + return False + for cert_ext in cert_exts: + try: + csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid) + if cert_ext != csr_ext: + return False + except cryptography.x509.ExtensionNotFound as dummy: + return False + return True + + def remove(self, module): + if self.backup: + self.backup_file = module.backup_local(self.path) + super(Certificate, self).remove(module) + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + state_and_perms = super(Certificate, self).check(module, perms_required) + + if not state_and_perms: + return False + + try: + self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + except Exception as dummy: + return False + + if self.privatekey_path or self.privatekey_content: + try: + self.privatekey = crypto_utils.load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise CertificateError(exc) + if not self._validate_privatekey(): + return False + + if self.csr_path or self.csr_content: + self.csr = crypto_utils.load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend + ) + if not self._validate_csr(): + return False + + # Check SubjectKeyIdentifier + if self.backend == 'cryptography' and self.create_subject_key_identifier != 'never_create': + # Get hold of certificate's SKI + try: + ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + except cryptography.x509.ExtensionNotFound as dummy: + return False + # Get hold of CSR's SKI for 'create_if_not_provided' + csr_ext = None + if self.create_subject_key_identifier == 'create_if_not_provided': + try: + csr_ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + except cryptography.x509.ExtensionNotFound as dummy: + pass + if csr_ext is None: + # If CSR had no SKI, or we chose to ignore it ('always_create'), compare with created SKI + if ext.value.digest != x509.SubjectKeyIdentifier.from_public_key(self.cert.public_key()).digest: + return False + else: + # If CSR had SKI and we didn't ignore it ('create_if_not_provided'), compare SKIs + if ext.value.digest != csr_ext.value.digest: + return False + + return True + + +class CertificateAbsent(Certificate): + def __init__(self, module): + super(CertificateAbsent, self).__init__(module, 'cryptography') # backend doesn't matter + + def generate(self, module): + pass + + def dump(self, check_mode=False): + # Use only for absent + + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'csr': self.csr_path + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + result['certificate'] = None + + return result + + +class SelfSignedCertificateCryptography(Certificate): + """Generate the self-signed certificate, using the cryptography backend""" + def __init__(self, module): + super(SelfSignedCertificateCryptography, self).__init__(module, 'cryptography') + self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier'] + self.notBefore = crypto_utils.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) + self.notAfter = crypto_utils.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) + self.digest = crypto_utils.select_message_digest(module.params['selfsigned_digest']) + self.version = module.params['selfsigned_version'] + self.serial_number = x509.random_serial_number() + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + if self.privatekey_content is None and not os.path.exists(self.privatekey_path): + raise CertificateError( + 'The private key file {0} does not exist'.format(self.privatekey_path) + ) + + self.csr = crypto_utils.load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend + ) + self._module = module + + try: + self.privatekey = crypto_utils.load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + module.fail_json(msg=to_native(exc)) + + if crypto_utils.cryptography_key_needs_digest_for_signing(self.privatekey): + if self.digest is None: + raise CertificateError( + 'The digest %s is not supported with the cryptography backend' % module.params['selfsigned_digest'] + ) + else: + self.digest = None + + def generate(self, module): + if self.privatekey_content is None and not os.path.exists(self.privatekey_path): + raise CertificateError( + 'The private key %s does not exist' % self.privatekey_path + ) + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file %s does not exist' % self.csr_path + ) + if not self.check(module, perms_required=False) or self.force: + try: + cert_builder = x509.CertificateBuilder() + cert_builder = cert_builder.subject_name(self.csr.subject) + cert_builder = cert_builder.issuer_name(self.csr.subject) + cert_builder = cert_builder.serial_number(self.serial_number) + cert_builder = cert_builder.not_valid_before(self.notBefore) + cert_builder = cert_builder.not_valid_after(self.notAfter) + cert_builder = cert_builder.public_key(self.privatekey.public_key()) + has_ski = False + for extension in self.csr.extensions: + if isinstance(extension.value, x509.SubjectKeyIdentifier): + if self.create_subject_key_identifier == 'always_create': + continue + has_ski = True + cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical) + if not has_ski and self.create_subject_key_identifier != 'never_create': + cert_builder = cert_builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), + critical=False + ) + except ValueError as e: + raise CertificateError(str(e)) + + try: + certificate = cert_builder.sign( + private_key=self.privatekey, algorithm=self.digest, + backend=default_backend() + ) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: + module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + self.cert = certificate + + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, certificate.public_bytes(Encoding.PEM)) + self.changed = True + else: + self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def dump(self, check_mode=False): + + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'csr': self.csr_path + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['certificate'] = content.decode('utf-8') if content else None + + if check_mode: + result.update({ + 'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': self.serial_number, + }) + else: + result.update({ + 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': self.cert.serial_number, + }) + + return result + + +class SelfSignedCertificate(Certificate): + """Generate the self-signed certificate.""" + + def __init__(self, module): + super(SelfSignedCertificate, self).__init__(module, 'pyopenssl') + if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided': + module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!') + self.notBefore = crypto_utils.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) + self.notAfter = crypto_utils.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) + self.digest = module.params['selfsigned_digest'] + self.version = module.params['selfsigned_version'] + self.serial_number = randint(1000, 99999) + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + if self.privatekey_content is None and not os.path.exists(self.privatekey_path): + raise CertificateError( + 'The private key file {0} does not exist'.format(self.privatekey_path) + ) + + self.csr = crypto_utils.load_certificate_request( + path=self.csr_path, + content=self.csr_content, + ) + try: + self.privatekey = crypto_utils.load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + module.fail_json(msg=str(exc)) + + def generate(self, module): + + if self.privatekey_content is None and not os.path.exists(self.privatekey_path): + raise CertificateError( + 'The private key %s does not exist' % self.privatekey_path + ) + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file %s does not exist' % self.csr_path + ) + + if not self.check(module, perms_required=False) or self.force: + cert = crypto.X509() + cert.set_serial_number(self.serial_number) + cert.set_notBefore(to_bytes(self.notBefore)) + cert.set_notAfter(to_bytes(self.notAfter)) + cert.set_subject(self.csr.get_subject()) + cert.set_issuer(self.csr.get_subject()) + cert.set_version(self.version - 1) + cert.set_pubkey(self.csr.get_pubkey()) + cert.add_extensions(self.csr.get_extensions()) + + cert.sign(self.privatekey, self.digest) + self.cert = cert + + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def dump(self, check_mode=False): + + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'csr': self.csr_path + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['certificate'] = content.decode('utf-8') if content else None + + if check_mode: + result.update({ + 'notBefore': self.notBefore, + 'notAfter': self.notAfter, + 'serial_number': self.serial_number, + }) + else: + result.update({ + 'notBefore': self.cert.get_notBefore(), + 'notAfter': self.cert.get_notAfter(), + 'serial_number': self.cert.get_serial_number(), + }) + + return result + + +class OwnCACertificateCryptography(Certificate): + """Generate the own CA certificate. Using the cryptography backend""" + def __init__(self, module): + super(OwnCACertificateCryptography, self).__init__(module, 'cryptography') + self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier'] + self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier'] + self.notBefore = crypto_utils.get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) + self.notAfter = crypto_utils.get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) + self.digest = crypto_utils.select_message_digest(module.params['ownca_digest']) + self.version = module.params['ownca_version'] + self.serial_number = x509.random_serial_number() + self.ca_cert_path = module.params['ownca_path'] + self.ca_cert_content = module.params['ownca_content'] + if self.ca_cert_content is not None: + self.ca_cert_content = self.ca_cert_content.encode('utf-8') + self.ca_privatekey_path = module.params['ownca_privatekey_path'] + self.ca_privatekey_content = module.params['ownca_privatekey_content'] + if self.ca_privatekey_content is not None: + self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8') + self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase'] + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): + raise CertificateError( + 'The CA certificate file {0} does not exist'.format(self.ca_cert_path) + ) + if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): + raise CertificateError( + 'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) + ) + + self.csr = crypto_utils.load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend + ) + self.ca_cert = crypto_utils.load_certificate( + path=self.ca_cert_path, + content=self.ca_cert_content, + backend=self.backend + ) + try: + self.ca_private_key = crypto_utils.load_privatekey( + path=self.ca_privatekey_path, + content=self.ca_privatekey_content, + passphrase=self.ca_privatekey_passphrase, + backend=self.backend + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + module.fail_json(msg=str(exc)) + + if crypto_utils.cryptography_key_needs_digest_for_signing(self.ca_private_key): + if self.digest is None: + raise CertificateError( + 'The digest %s is not supported with the cryptography backend' % module.params['ownca_digest'] + ) + else: + self.digest = None + + def generate(self, module): + + if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): + raise CertificateError( + 'The CA certificate %s does not exist' % self.ca_cert_path + ) + + if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): + raise CertificateError( + 'The CA private key %s does not exist' % self.ca_privatekey_path + ) + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file %s does not exist' % self.csr_path + ) + + if not self.check(module, perms_required=False) or self.force: + cert_builder = x509.CertificateBuilder() + cert_builder = cert_builder.subject_name(self.csr.subject) + cert_builder = cert_builder.issuer_name(self.ca_cert.subject) + cert_builder = cert_builder.serial_number(self.serial_number) + cert_builder = cert_builder.not_valid_before(self.notBefore) + cert_builder = cert_builder.not_valid_after(self.notAfter) + cert_builder = cert_builder.public_key(self.csr.public_key()) + has_ski = False + for extension in self.csr.extensions: + if isinstance(extension.value, x509.SubjectKeyIdentifier): + if self.create_subject_key_identifier == 'always_create': + continue + has_ski = True + if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier): + continue + cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical) + if not has_ski and self.create_subject_key_identifier != 'never_create': + cert_builder = cert_builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()), + critical=False + ) + if self.create_authority_key_identifier: + try: + ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + cert_builder = cert_builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) + if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext), + critical=False + ) + except cryptography.x509.ExtensionNotFound: + cert_builder = cert_builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()), + critical=False + ) + + try: + certificate = cert_builder.sign( + private_key=self.ca_private_key, algorithm=self.digest, + backend=default_backend() + ) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: + module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + self.cert = certificate + + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, certificate.public_bytes(Encoding.PEM)) + self.changed = True + else: + self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + if not super(OwnCACertificateCryptography, self).check(module, perms_required): + return False + + # Check AuthorityKeyIdentifier + if self.create_authority_key_identifier: + try: + ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + expected_ext = ( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) + if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext) + ) + except cryptography.x509.ExtensionNotFound: + expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()) + try: + ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + if ext.value != expected_ext: + return False + except cryptography.x509.ExtensionNotFound as dummy: + return False + + return True + + def dump(self, check_mode=False): + + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'csr': self.csr_path, + 'ca_cert': self.ca_cert_path, + 'ca_privatekey': self.ca_privatekey_path + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['certificate'] = content.decode('utf-8') if content else None + + if check_mode: + result.update({ + 'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': self.serial_number, + }) + else: + result.update({ + 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': self.cert.serial_number, + }) + + return result + + +class OwnCACertificate(Certificate): + """Generate the own CA certificate.""" + + def __init__(self, module): + super(OwnCACertificate, self).__init__(module, 'pyopenssl') + self.notBefore = crypto_utils.get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) + self.notAfter = crypto_utils.get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) + self.digest = module.params['ownca_digest'] + self.version = module.params['ownca_version'] + self.serial_number = randint(1000, 99999) + if module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided': + module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!') + if module.params['ownca_create_authority_key_identifier']: + module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!') + self.ca_cert_path = module.params['ownca_path'] + self.ca_cert_content = module.params['ownca_content'] + if self.ca_cert_content is not None: + self.ca_cert_content = self.ca_cert_content.encode('utf-8') + self.ca_privatekey_path = module.params['ownca_privatekey_path'] + self.ca_privatekey_content = module.params['ownca_privatekey_content'] + if self.ca_privatekey_content is not None: + self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8') + self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase'] + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): + raise CertificateError( + 'The CA certificate file {0} does not exist'.format(self.ca_cert_path) + ) + if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): + raise CertificateError( + 'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) + ) + + self.csr = crypto_utils.load_certificate_request( + path=self.csr_path, + content=self.csr_content, + ) + self.ca_cert = crypto_utils.load_certificate( + path=self.ca_cert_path, + content=self.ca_cert_content, + ) + try: + self.ca_privatekey = crypto_utils.load_privatekey( + path=self.ca_privatekey_path, + content=self.ca_privatekey_content, + passphrase=self.ca_privatekey_passphrase + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + module.fail_json(msg=str(exc)) + + def generate(self, module): + + if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): + raise CertificateError( + 'The CA certificate %s does not exist' % self.ca_cert_path + ) + + if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): + raise CertificateError( + 'The CA private key %s does not exist' % self.ca_privatekey_path + ) + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file %s does not exist' % self.csr_path + ) + + if not self.check(module, perms_required=False) or self.force: + cert = crypto.X509() + cert.set_serial_number(self.serial_number) + cert.set_notBefore(to_bytes(self.notBefore)) + cert.set_notAfter(to_bytes(self.notAfter)) + cert.set_subject(self.csr.get_subject()) + cert.set_issuer(self.ca_cert.get_subject()) + cert.set_version(self.version - 1) + cert.set_pubkey(self.csr.get_pubkey()) + cert.add_extensions(self.csr.get_extensions()) + + cert.sign(self.ca_privatekey, self.digest) + self.cert = cert + + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def dump(self, check_mode=False): + + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'csr': self.csr_path, + 'ca_cert': self.ca_cert_path, + 'ca_privatekey': self.ca_privatekey_path + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['certificate'] = content.decode('utf-8') if content else None + + if check_mode: + result.update({ + 'notBefore': self.notBefore, + 'notAfter': self.notAfter, + 'serial_number': self.serial_number, + }) + else: + result.update({ + 'notBefore': self.cert.get_notBefore(), + 'notAfter': self.cert.get_notAfter(), + 'serial_number': self.cert.get_serial_number(), + }) + + return result + + +def compare_sets(subset, superset, equality=False): + if equality: + return set(subset) == set(superset) + else: + return all(x in superset for x in subset) + + +def compare_dicts(subset, superset, equality=False): + if equality: + return subset == superset + else: + return all(superset.get(x) == v for x, v in subset.items()) + + +NO_EXTENSION = 'no extension' + + +class AssertOnlyCertificateBase(Certificate): + + def __init__(self, module, backend): + super(AssertOnlyCertificateBase, self).__init__(module, backend) + + self.signature_algorithms = module.params['signature_algorithms'] + if module.params['subject']: + self.subject = crypto_utils.parse_name_field(module.params['subject']) + else: + self.subject = [] + self.subject_strict = module.params['subject_strict'] + if module.params['issuer']: + self.issuer = crypto_utils.parse_name_field(module.params['issuer']) + else: + self.issuer = [] + self.issuer_strict = module.params['issuer_strict'] + self.has_expired = module.params['has_expired'] + self.version = module.params['version'] + self.key_usage = module.params['key_usage'] + self.key_usage_strict = module.params['key_usage_strict'] + self.extended_key_usage = module.params['extended_key_usage'] + self.extended_key_usage_strict = module.params['extended_key_usage_strict'] + self.subject_alt_name = module.params['subject_alt_name'] + self.subject_alt_name_strict = module.params['subject_alt_name_strict'] + self.not_before = module.params['not_before'] + self.not_after = module.params['not_after'] + self.valid_at = module.params['valid_at'] + self.invalid_at = module.params['invalid_at'] + self.valid_in = module.params['valid_in'] + if self.valid_in and not self.valid_in.startswith("+") and not self.valid_in.startswith("-"): + try: + int(self.valid_in) + except ValueError: + module.fail_json(msg='The supplied value for "valid_in" (%s) is not an integer or a valid timespec' % self.valid_in) + self.valid_in = "+" + self.valid_in + "s" + + # Load objects + self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + if self.privatekey_path is not None or self.privatekey_content is not None: + try: + self.privatekey = crypto_utils.load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise CertificateError(exc) + if self.csr_path is not None or self.csr_content is not None: + self.csr = crypto_utils.load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend + ) + + @abc.abstractmethod + def _validate_privatekey(self): + pass + + @abc.abstractmethod + def _validate_csr_signature(self): + pass + + @abc.abstractmethod + def _validate_csr_subject(self): + pass + + @abc.abstractmethod + def _validate_csr_extensions(self): + pass + + @abc.abstractmethod + def _validate_signature_algorithms(self): + pass + + @abc.abstractmethod + def _validate_subject(self): + pass + + @abc.abstractmethod + def _validate_issuer(self): + pass + + @abc.abstractmethod + def _validate_has_expired(self): + pass + + @abc.abstractmethod + def _validate_version(self): + pass + + @abc.abstractmethod + def _validate_key_usage(self): + pass + + @abc.abstractmethod + def _validate_extended_key_usage(self): + pass + + @abc.abstractmethod + def _validate_subject_alt_name(self): + pass + + @abc.abstractmethod + def _validate_not_before(self): + pass + + @abc.abstractmethod + def _validate_not_after(self): + pass + + @abc.abstractmethod + def _validate_valid_at(self): + pass + + @abc.abstractmethod + def _validate_invalid_at(self): + pass + + @abc.abstractmethod + def _validate_valid_in(self): + pass + + def assertonly(self, module): + messages = [] + if self.privatekey_path is not None or self.privatekey_content is not None: + if not self._validate_privatekey(): + messages.append( + 'Certificate %s and private key %s do not match' % + (self.path, self.privatekey_path or '(provided in module options)') + ) + + if self.csr_path is not None or self.csr_content is not None: + if not self._validate_csr_signature(): + messages.append( + 'Certificate %s and CSR %s do not match: private key mismatch' % + (self.path, self.csr_path or '(provided in module options)') + ) + if not self._validate_csr_subject(): + messages.append( + 'Certificate %s and CSR %s do not match: subject mismatch' % + (self.path, self.csr_path or '(provided in module options)') + ) + if not self._validate_csr_extensions(): + messages.append( + 'Certificate %s and CSR %s do not match: extensions mismatch' % + (self.path, self.csr_path or '(provided in module options)') + ) + + if self.signature_algorithms is not None: + wrong_alg = self._validate_signature_algorithms() + if wrong_alg: + messages.append( + 'Invalid signature algorithm (got %s, expected one of %s)' % + (wrong_alg, self.signature_algorithms) + ) + + if self.subject is not None: + failure = self._validate_subject() + if failure: + dummy, cert_subject = failure + messages.append( + 'Invalid subject component (got %s, expected all of %s to be present)' % + (cert_subject, self.subject) + ) + + if self.issuer is not None: + failure = self._validate_issuer() + if failure: + dummy, cert_issuer = failure + messages.append( + 'Invalid issuer component (got %s, expected all of %s to be present)' % (cert_issuer, self.issuer) + ) + + if self.has_expired is not None: + cert_expired = self._validate_has_expired() + if cert_expired != self.has_expired: + messages.append( + 'Certificate expiration check failed (certificate expiration is %s, expected %s)' % + (cert_expired, self.has_expired) + ) + + if self.version is not None: + cert_version = self._validate_version() + if cert_version != self.version: + messages.append( + 'Invalid certificate version number (got %s, expected %s)' % + (cert_version, self.version) + ) + + if self.key_usage is not None: + failure = self._validate_key_usage() + if failure == NO_EXTENSION: + messages.append('Found no keyUsage extension') + elif failure: + dummy, cert_key_usage = failure + messages.append( + 'Invalid keyUsage components (got %s, expected all of %s to be present)' % + (cert_key_usage, self.key_usage) + ) + + if self.extended_key_usage is not None: + failure = self._validate_extended_key_usage() + if failure == NO_EXTENSION: + messages.append('Found no extendedKeyUsage extension') + elif failure: + dummy, ext_cert_key_usage = failure + messages.append( + 'Invalid extendedKeyUsage component (got %s, expected all of %s to be present)' % (ext_cert_key_usage, self.extended_key_usage) + ) + + if self.subject_alt_name is not None: + failure = self._validate_subject_alt_name() + if failure == NO_EXTENSION: + messages.append('Found no subjectAltName extension') + elif failure: + dummy, cert_san = failure + messages.append( + 'Invalid subjectAltName component (got %s, expected all of %s to be present)' % + (cert_san, self.subject_alt_name) + ) + + if self.not_before is not None: + cert_not_valid_before = self._validate_not_before() + if cert_not_valid_before != crypto_utils.get_relative_time_option(self.not_before, 'not_before', backend=self.backend): + messages.append( + 'Invalid not_before component (got %s, expected %s to be present)' % + (cert_not_valid_before, self.not_before) + ) + + if self.not_after is not None: + cert_not_valid_after = self._validate_not_after() + if cert_not_valid_after != crypto_utils.get_relative_time_option(self.not_after, 'not_after', backend=self.backend): + messages.append( + 'Invalid not_after component (got %s, expected %s to be present)' % + (cert_not_valid_after, self.not_after) + ) + + if self.valid_at is not None: + not_before, valid_at, not_after = self._validate_valid_at() + if not (not_before <= valid_at <= not_after): + messages.append( + 'Certificate is not valid for the specified date (%s) - not_before: %s - not_after: %s' % + (self.valid_at, not_before, not_after) + ) + + if self.invalid_at is not None: + not_before, invalid_at, not_after = self._validate_invalid_at() + if not_before <= invalid_at <= not_after: + messages.append( + 'Certificate is not invalid for the specified date (%s) - not_before: %s - not_after: %s' % + (self.invalid_at, not_before, not_after) + ) + + if self.valid_in is not None: + not_before, valid_in, not_after = self._validate_valid_in() + if not not_before <= valid_in <= not_after: + messages.append( + 'Certificate is not valid in %s from now (that would be %s) - not_before: %s - not_after: %s' % + (self.valid_in, valid_in, not_before, not_after) + ) + return messages + + def generate(self, module): + """Don't generate anything - only assert""" + messages = self.assertonly(module) + if messages: + module.fail_json(msg=' | '.join(messages)) + + def check(self, module, perms_required=False): + """Ensure the resource is in its desired state.""" + messages = self.assertonly(module) + return len(messages) == 0 + + def dump(self, check_mode=False): + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'csr': self.csr_path, + } + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['certificate'] = content.decode('utf-8') if content else None + return result + + +class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): + """Validate the supplied cert, using the cryptography backend""" + def __init__(self, module): + super(AssertOnlyCertificateCryptography, self).__init__(module, 'cryptography') + + def _validate_privatekey(self): + return crypto_utils.cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) + + def _validate_csr_signature(self): + if not self.csr.is_signature_valid: + return False + return crypto_utils.cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()) + + def _validate_csr_subject(self): + return self.csr.subject == self.cert.subject + + def _validate_csr_extensions(self): + cert_exts = self.cert.extensions + csr_exts = self.csr.extensions + if len(cert_exts) != len(csr_exts): + return False + for cert_ext in cert_exts: + try: + csr_ext = csr_exts.get_extension_for_oid(cert_ext.oid) + if cert_ext != csr_ext: + return False + except cryptography.x509.ExtensionNotFound as dummy: + return False + return True + + def _validate_signature_algorithms(self): + if self.cert.signature_algorithm_oid._name not in self.signature_algorithms: + return self.cert.signature_algorithm_oid._name + + def _validate_subject(self): + expected_subject = Name([NameAttribute(oid=crypto_utils.cryptography_name_to_oid(sub[0]), value=to_text(sub[1])) + for sub in self.subject]) + cert_subject = self.cert.subject + if not compare_sets(expected_subject, cert_subject, self.subject_strict): + return expected_subject, cert_subject + + def _validate_issuer(self): + expected_issuer = Name([NameAttribute(oid=crypto_utils.cryptography_name_to_oid(iss[0]), value=to_text(iss[1])) + for iss in self.issuer]) + cert_issuer = self.cert.issuer + if not compare_sets(expected_issuer, cert_issuer, self.issuer_strict): + return self.issuer, cert_issuer + + def _validate_has_expired(self): + cert_not_after = self.cert.not_valid_after + cert_expired = cert_not_after < datetime.datetime.utcnow() + return cert_expired + + def _validate_version(self): + if self.cert.version == x509.Version.v1: + return 1 + if self.cert.version == x509.Version.v3: + return 3 + return "unknown" + + def _validate_key_usage(self): + try: + current_key_usage = self.cert.extensions.get_extension_for_class(x509.KeyUsage).value + test_key_usage = dict( + digital_signature=current_key_usage.digital_signature, + content_commitment=current_key_usage.content_commitment, + key_encipherment=current_key_usage.key_encipherment, + data_encipherment=current_key_usage.data_encipherment, + key_agreement=current_key_usage.key_agreement, + key_cert_sign=current_key_usage.key_cert_sign, + crl_sign=current_key_usage.crl_sign, + encipher_only=False, + decipher_only=False + ) + if test_key_usage['key_agreement']: + test_key_usage.update(dict( + encipher_only=current_key_usage.encipher_only, + decipher_only=current_key_usage.decipher_only + )) + + key_usages = crypto_utils.cryptography_parse_key_usage_params(self.key_usage) + if not compare_dicts(key_usages, test_key_usage, self.key_usage_strict): + return self.key_usage, [k for k, v in test_key_usage.items() if v is True] + + except cryptography.x509.ExtensionNotFound: + # This is only bad if the user specified a non-empty list + if self.key_usage: + return NO_EXTENSION + + def _validate_extended_key_usage(self): + try: + current_ext_keyusage = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value + usages = [crypto_utils.cryptography_name_to_oid(usage) for usage in self.extended_key_usage] + expected_ext_keyusage = x509.ExtendedKeyUsage(usages) + if not compare_sets(expected_ext_keyusage, current_ext_keyusage, self.extended_key_usage_strict): + return [eku.value for eku in expected_ext_keyusage], [eku.value for eku in current_ext_keyusage] + + except cryptography.x509.ExtensionNotFound: + # This is only bad if the user specified a non-empty list + if self.extended_key_usage: + return NO_EXTENSION + + def _validate_subject_alt_name(self): + try: + current_san = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + expected_san = [crypto_utils.cryptography_get_name(san) for san in self.subject_alt_name] + if not compare_sets(expected_san, current_san, self.subject_alt_name_strict): + return self.subject_alt_name, current_san + except cryptography.x509.ExtensionNotFound: + # This is only bad if the user specified a non-empty list + if self.subject_alt_name: + return NO_EXTENSION + + def _validate_not_before(self): + return self.cert.not_valid_before + + def _validate_not_after(self): + return self.cert.not_valid_after + + def _validate_valid_at(self): + rt = crypto_utils.get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend) + return self.cert.not_valid_before, rt, self.cert.not_valid_after + + def _validate_invalid_at(self): + rt = crypto_utils.get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend) + return self.cert.not_valid_before, rt, self.cert.not_valid_after + + def _validate_valid_in(self): + valid_in_date = crypto_utils.get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) + return self.cert.not_valid_before, valid_in_date, self.cert.not_valid_after + + +class AssertOnlyCertificate(AssertOnlyCertificateBase): + """validate the supplied certificate.""" + + def __init__(self, module): + super(AssertOnlyCertificate, self).__init__(module, 'pyopenssl') + + # Ensure inputs are properly sanitized before comparison. + for param in ['signature_algorithms', 'key_usage', 'extended_key_usage', + 'subject_alt_name', 'subject', 'issuer', 'not_before', + 'not_after', 'valid_at', 'invalid_at']: + attr = getattr(self, param) + if isinstance(attr, list) and attr: + if isinstance(attr[0], str): + setattr(self, param, [to_bytes(item) for item in attr]) + elif isinstance(attr[0], tuple): + setattr(self, param, [(to_bytes(item[0]), to_bytes(item[1])) for item in attr]) + elif isinstance(attr, tuple): + setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items())) + elif isinstance(attr, dict): + setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items())) + elif isinstance(attr, str): + setattr(self, param, to_bytes(attr)) + + def _validate_privatekey(self): + ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD) + ctx.use_privatekey(self.privatekey) + ctx.use_certificate(self.cert) + try: + ctx.check_privatekey() + return True + except OpenSSL.SSL.Error: + return False + + def _validate_csr_signature(self): + try: + self.csr.verify(self.cert.get_pubkey()) + except OpenSSL.crypto.Error: + return False + + def _validate_csr_subject(self): + if self.csr.get_subject() != self.cert.get_subject(): + return False + + def _validate_csr_extensions(self): + csr_extensions = self.csr.get_extensions() + cert_extension_count = self.cert.get_extension_count() + if len(csr_extensions) != cert_extension_count: + return False + for extension_number in range(0, cert_extension_count): + cert_extension = self.cert.get_extension(extension_number) + csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions) + if cert_extension.get_data() != list(csr_extension)[0].get_data(): + return False + return True + + def _validate_signature_algorithms(self): + if self.cert.get_signature_algorithm() not in self.signature_algorithms: + return self.cert.get_signature_algorithm() + + def _validate_subject(self): + expected_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in self.subject] + cert_subject = self.cert.get_subject().get_components() + current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in cert_subject] + if not compare_sets(expected_subject, current_subject, self.subject_strict): + return expected_subject, current_subject + + def _validate_issuer(self): + expected_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in self.issuer] + cert_issuer = self.cert.get_issuer().get_components() + current_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in cert_issuer] + if not compare_sets(expected_issuer, current_issuer, self.issuer_strict): + return self.issuer, cert_issuer + + def _validate_has_expired(self): + # The following 3 lines are the same as the current PyOpenSSL code for cert.has_expired(). + # Older version of PyOpenSSL have a buggy implementation, + # to avoid issues with those we added the code from a more recent release here. + + time_string = to_native(self.cert.get_notAfter()) + not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") + cert_expired = not_after < datetime.datetime.utcnow() + return cert_expired + + def _validate_version(self): + # Version numbers in certs are off by one: + # v1: 0, v2: 1, v3: 2 ... + return self.cert.get_version() + 1 + + def _validate_key_usage(self): + found = False + for extension_idx in range(0, self.cert.get_extension_count()): + extension = self.cert.get_extension(extension_idx) + if extension.get_short_name() == b'keyUsage': + found = True + expected_extension = crypto.X509Extension(b"keyUsage", False, b', '.join(self.key_usage)) + key_usage = [usage.strip() for usage in to_text(expected_extension, errors='surrogate_or_strict').split(',')] + current_ku = [usage.strip() for usage in to_text(extension, errors='surrogate_or_strict').split(',')] + if not compare_sets(key_usage, current_ku, self.key_usage_strict): + return self.key_usage, str(extension).split(', ') + if not found: + # This is only bad if the user specified a non-empty list + if self.key_usage: + return NO_EXTENSION + + def _validate_extended_key_usage(self): + found = False + for extension_idx in range(0, self.cert.get_extension_count()): + extension = self.cert.get_extension(extension_idx) + if extension.get_short_name() == b'extendedKeyUsage': + found = True + extKeyUsage = [OpenSSL._util.lib.OBJ_txt2nid(keyUsage) for keyUsage in self.extended_key_usage] + current_xku = [OpenSSL._util.lib.OBJ_txt2nid(usage.strip()) for usage in + to_bytes(extension, errors='surrogate_or_strict').split(b',')] + if not compare_sets(extKeyUsage, current_xku, self.extended_key_usage_strict): + return self.extended_key_usage, str(extension).split(', ') + if not found: + # This is only bad if the user specified a non-empty list + if self.extended_key_usage: + return NO_EXTENSION + + def _normalize_san(self, san): + # Apparently OpenSSL returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string + # although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004) + if san.startswith('IP Address:'): + san = 'IP:' + san[len('IP Address:'):] + if san.startswith('IP:'): + ip = compat_ipaddress.ip_address(san[3:]) + san = 'IP:{0}'.format(ip.compressed) + return san + + def _validate_subject_alt_name(self): + found = False + for extension_idx in range(0, self.cert.get_extension_count()): + extension = self.cert.get_extension(extension_idx) + if extension.get_short_name() == b'subjectAltName': + found = True + l_altnames = [self._normalize_san(altname.strip()) for altname in + to_text(extension, errors='surrogate_or_strict').split(', ')] + sans = [self._normalize_san(to_text(san, errors='surrogate_or_strict')) for san in self.subject_alt_name] + if not compare_sets(sans, l_altnames, self.subject_alt_name_strict): + return self.subject_alt_name, l_altnames + if not found: + # This is only bad if the user specified a non-empty list + if self.subject_alt_name: + return NO_EXTENSION + + def _validate_not_before(self): + return self.cert.get_notBefore() + + def _validate_not_after(self): + return self.cert.get_notAfter() + + def _validate_valid_at(self): + rt = crypto_utils.get_relative_time_option(self.valid_at, "valid_at", backend=self.backend) + rt = to_bytes(rt, errors='surrogate_or_strict') + return self.cert.get_notBefore(), rt, self.cert.get_notAfter() + + def _validate_invalid_at(self): + rt = crypto_utils.get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend) + rt = to_bytes(rt, errors='surrogate_or_strict') + return self.cert.get_notBefore(), rt, self.cert.get_notAfter() + + def _validate_valid_in(self): + valid_in_asn1 = crypto_utils.get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) + valid_in_date = to_bytes(valid_in_asn1, errors='surrogate_or_strict') + return self.cert.get_notBefore(), valid_in_date, self.cert.get_notAfter() + + +class EntrustCertificate(Certificate): + """Retrieve a certificate using Entrust (ECS).""" + + def __init__(self, module, backend): + super(EntrustCertificate, self).__init__(module, backend) + self.trackingId = None + self.notAfter = crypto_utils.get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after', backend=self.backend) + + if self.csr_content is None or not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + + self.csr = crypto_utils.load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend, + ) + + # ECS API defaults to using the validated organization tied to the account. + # We want to always force behavior of trying to use the organization provided in the CSR. + # To that end we need to parse out the organization from the CSR. + self.csr_org = None + if self.backend == 'pyopenssl': + csr_subject = self.csr.get_subject() + csr_subject_components = csr_subject.get_components() + for k, v in csr_subject_components: + if k.upper() == 'O': + # Entrust does not support multiple validated organizations in a single certificate + if self.csr_org is not None: + module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in " + "Subject DN: '{0}'. ".format(csr_subject))) + else: + self.csr_org = v + elif self.backend == 'cryptography': + csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME) + if len(csr_subject_orgs) == 1: + self.csr_org = csr_subject_orgs[0].value + elif len(csr_subject_orgs) > 1: + module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in " + "Subject DN: '{0}'. ".format(self.csr.subject))) + # If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to + # organization tied to the account. + if self.csr_org is None: + self.csr_org = '' + + try: + self.ecs_client = ECSClient( + entrust_api_user=module.params.get('entrust_api_user'), + entrust_api_key=module.params.get('entrust_api_key'), + entrust_api_cert=module.params.get('entrust_api_client_cert_path'), + entrust_api_cert_key=module.params.get('entrust_api_client_cert_key_path'), + entrust_api_specification_path=module.params.get('entrust_api_specification_path') + ) + except SessionConfigurationException as e: + module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e.message))) + + def generate(self, module): + + if not self.check(module, perms_required=False) or self.force: + # Read the CSR that was generated for us + body = {} + if self.csr_content is not None: + body['csr'] = self.csr_content + else: + with open(self.csr_path, 'r') as csr_file: + body['csr'] = csr_file.read() + + body['certType'] = module.params['entrust_cert_type'] + + # Handle expiration (30 days if not specified) + expiry = self.notAfter + if not expiry: + gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())) + expiry = gmt_now + datetime.timedelta(days=365) + + expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") + body['certExpiryDate'] = expiry_iso3339 + body['org'] = self.csr_org + body['tracking'] = { + 'requesterName': module.params['entrust_requester_name'], + 'requesterEmail': module.params['entrust_requester_email'], + 'requesterPhone': module.params['entrust_requester_phone'], + } + + try: + result = self.ecs_client.NewCertRequest(Body=body) + self.trackingId = result.get('trackingId') + except RestOperationException as e: + module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message))) + + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, to_bytes(result.get('endEntityCert'))) + self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + self.changed = True + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + parent_check = super(EntrustCertificate, self).check(module, perms_required) + + try: + cert_details = self._get_cert_details() + except RestOperationException as e: + module.fail_json(msg='Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'.format(to_native(e.message))) + + # Always issue a new certificate if the certificate is expired, suspended or revoked + status = cert_details.get('status', False) + if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED': + return False + + # If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed + if module.params['entrust_cert_type'] and cert_details.get('certType') and module.params['entrust_cert_type'] != cert_details.get('certType'): + return False + + return parent_check + + def _get_cert_details(self): + cert_details = {} + if self.cert: + serial_number = None + expiry = None + if self.backend == 'pyopenssl': + serial_number = "{0:X}".format(self.cert.get_serial_number()) + time_string = to_native(self.cert.get_notAfter()) + expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") + elif self.backend == 'cryptography': + serial_number = "{0:X}".format(self.cert.serial_number) + expiry = self.cert.not_valid_after + + # get some information about the expiry of this certificate + expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") + cert_details['expiresAfter'] = expiry_iso3339 + + # If a trackingId is not already defined (from the result of a generate) + # use the serial number to identify the tracking Id + if self.trackingId is None and serial_number is not None: + cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {}) + + # Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks + # on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is + # still checked as it is in the rest of the module. + if len(cert_results) == 1: + self.trackingId = cert_results[0].get('trackingId') + + if self.trackingId is not None: + cert_details.update(self.ecs_client.GetCertificate(trackingId=self.trackingId)) + + return cert_details + + def dump(self, check_mode=False): + + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'csr': self.csr_path, + } + + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['certificate'] = content.decode('utf-8') if content else None + + result.update(self._get_cert_details()) + + return result + + +class AcmeCertificate(Certificate): + """Retrieve a certificate using the ACME protocol.""" + + # Since there's no real use of the backend, + # other than the 'self.check' function, we just pass the backend to the constructor + + def __init__(self, module, backend): + super(AcmeCertificate, self).__init__(module, backend) + self.accountkey_path = module.params['acme_accountkey_path'] + self.challenge_path = module.params['acme_challenge_path'] + self.use_chain = module.params['acme_chain'] + self.acme_directory = module.params['acme_directory'] + + def generate(self, module): + + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file %s does not exist' % self.csr_path + ) + + if not os.path.exists(self.accountkey_path): + raise CertificateError( + 'The account key %s does not exist' % self.accountkey_path + ) + + if not os.path.exists(self.challenge_path): + raise CertificateError( + 'The challenge path %s does not exist' % self.challenge_path + ) + + if not self.check(module, perms_required=False) or self.force: + acme_tiny_path = self.module.get_bin_path('acme-tiny', required=True) + command = [acme_tiny_path] + if self.use_chain: + command.append('--chain') + command.extend(['--account-key', self.accountkey_path]) + if self.csr_content is not None: + # We need to temporarily write the CSR to disk + fd, tmpsrc = tempfile.mkstemp() + module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit + f = os.fdopen(fd, 'wb') + try: + f.write(self.csr_content) + except Exception as err: + try: + f.close() + except Exception as dummy: + pass + module.fail_json( + msg="failed to create temporary CSR file: %s" % to_native(err), + exception=traceback.format_exc() + ) + f.close() + command.extend(['--csr', tmpsrc]) + else: + command.extend(['--csr', self.csr_path]) + command.extend(['--acme-dir', self.challenge_path]) + command.extend(['--directory-url', self.acme_directory]) + + try: + crt = module.run_command(command, check_rc=True)[1] + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, to_bytes(crt)) + self.changed = True + except OSError as exc: + raise CertificateError(exc) + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def dump(self, check_mode=False): + + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'accountkey': self.accountkey_path, + 'csr': self.csr_path, + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['certificate'] = content.decode('utf-8') if content else None + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + path=dict(type='path', required=True), + provider=dict(type='str', choices=['acme', 'assertonly', 'entrust', 'ownca', 'selfsigned']), + force=dict(type='bool', default=False,), + csr_path=dict(type='path'), + csr_content=dict(type='str'), + backup=dict(type='bool', default=False), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), + return_content=dict(type='bool', default=False), + + # General properties of a certificate + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str'), + privatekey_passphrase=dict(type='str', no_log=True), + + # provider: assertonly + signature_algorithms=dict(type='list', elements='str', removed_in_version='2.13'), + subject=dict(type='dict', removed_in_version='2.13'), + subject_strict=dict(type='bool', default=False, removed_in_version='2.13'), + issuer=dict(type='dict', removed_in_version='2.13'), + issuer_strict=dict(type='bool', default=False, removed_in_version='2.13'), + has_expired=dict(type='bool', default=False, removed_in_version='2.13'), + version=dict(type='int', removed_in_version='2.13'), + key_usage=dict(type='list', elements='str', aliases=['keyUsage'], removed_in_version='2.13'), + key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'], removed_in_version='2.13'), + extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'], removed_in_version='2.13'), + extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'], removed_in_version='2.13'), + subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'], removed_in_version='2.13'), + subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'], removed_in_version='2.13'), + not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.13'), + not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.13'), + valid_at=dict(type='str', removed_in_version='2.13'), + invalid_at=dict(type='str', removed_in_version='2.13'), + valid_in=dict(type='str', removed_in_version='2.13'), + + # provider: selfsigned + selfsigned_version=dict(type='int', default=3), + selfsigned_digest=dict(type='str', default='sha256'), + selfsigned_not_before=dict(type='str', default='+0s', aliases=['selfsigned_notBefore']), + selfsigned_not_after=dict(type='str', default='+3650d', aliases=['selfsigned_notAfter']), + selfsigned_create_subject_key_identifier=dict( + type='str', + default='create_if_not_provided', + choices=['create_if_not_provided', 'always_create', 'never_create'] + ), + + # provider: ownca + ownca_path=dict(type='path'), + ownca_content=dict(type='str'), + ownca_privatekey_path=dict(type='path'), + ownca_privatekey_content=dict(type='str'), + ownca_privatekey_passphrase=dict(type='str', no_log=True), + ownca_digest=dict(type='str', default='sha256'), + ownca_version=dict(type='int', default=3), + ownca_not_before=dict(type='str', default='+0s'), + ownca_not_after=dict(type='str', default='+3650d'), + ownca_create_subject_key_identifier=dict( + type='str', + default='create_if_not_provided', + choices=['create_if_not_provided', 'always_create', 'never_create'] + ), + ownca_create_authority_key_identifier=dict(type='bool', default=True), + + # provider: acme + acme_accountkey_path=dict(type='path'), + acme_challenge_path=dict(type='path'), + acme_chain=dict(type='bool', default=False), + acme_directory=dict(type='str', default="https://acme-v02.api.letsencrypt.org/directory"), + + # provider: entrust + entrust_cert_type=dict(type='str', default='STANDARD_SSL', + choices=['STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', + 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT']), + entrust_requester_email=dict(type='str'), + entrust_requester_name=dict(type='str'), + entrust_requester_phone=dict(type='str'), + entrust_api_user=dict(type='str'), + entrust_api_key=dict(type='str', no_log=True), + entrust_api_client_cert_path=dict(type='path'), + entrust_api_client_cert_key_path=dict(type='path', no_log=True), + entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'), + entrust_not_after=dict(type='str', default='+365d'), + ), + supports_check_mode=True, + add_file_common_args=True, + required_if=[ + ['state', 'present', ['provider']], + ['provider', 'entrust', ['entrust_requester_email', 'entrust_requester_name', 'entrust_requester_phone', + 'entrust_api_user', 'entrust_api_key', 'entrust_api_client_cert_path', + 'entrust_api_client_cert_key_path']], + ], + mutually_exclusive=[ + ['csr_path', 'csr_content'], + ['privatekey_path', 'privatekey_content'], + ['ownca_path', 'ownca_content'], + ['ownca_privatekey_path', 'ownca_privatekey_content'], + ], + ) + + try: + if module.params['state'] == 'absent': + certificate = CertificateAbsent(module) + + else: + if module.params['provider'] != 'assertonly' and module.params['csr_path'] is None and module.params['csr_content'] is None: + module.fail_json(msg='csr_path or csr_content is required when provider is not assertonly') + + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + provider = module.params['provider'] + if provider == 'assertonly': + module.deprecate("The 'assertonly' provider is deprecated; please see the examples of " + "the 'openssl_certificate' module on how to replace it with other modules", + version='2.13') + elif provider == 'selfsigned': + if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None: + module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.') + elif provider == 'acme': + if module.params['acme_accountkey_path'] is None: + module.fail_json(msg='The acme_accountkey_path option must be specified for the acme provider.') + if module.params['acme_challenge_path'] is None: + module.fail_json(msg='The acme_challenge_path option must be specified for the acme provider.') + elif provider == 'ownca': + if module.params['ownca_path'] is None and module.params['ownca_content'] is None: + module.fail_json(msg='One of ownca_path and ownca_content must be specified for the ownca provider.') + if module.params['ownca_privatekey_path'] is None and module.params['ownca_privatekey_content'] is None: + module.fail_json(msg='One of ownca_privatekey_path and ownca_privatekey_content must be specified for the ownca provider.') + + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detect what backend we can use + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # If cryptography is available we'll use it + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + if module.params['selfsigned_version'] == 2 or module.params['ownca_version'] == 2: + module.warn('crypto backend forced to pyopenssl. The cryptography library does not support v2 certificates') + backend = 'pyopenssl' + + # Fail if no backend has been found + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + if module.params['provider'] in ['selfsigned', 'ownca', 'assertonly']: + try: + getattr(crypto.X509Req, 'get_extensions') + except AttributeError: + module.fail_json(msg='You need to have PyOpenSSL>=0.15') + + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + if provider == 'selfsigned': + certificate = SelfSignedCertificate(module) + elif provider == 'acme': + certificate = AcmeCertificate(module, 'pyopenssl') + elif provider == 'ownca': + certificate = OwnCACertificate(module) + elif provider == 'entrust': + certificate = EntrustCertificate(module, 'pyopenssl') + else: + certificate = AssertOnlyCertificate(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + if module.params['selfsigned_version'] == 2 or module.params['ownca_version'] == 2: + module.fail_json(msg='The cryptography backend does not support v2 certificates, ' + 'use select_crypto_backend=pyopenssl for v2 certificates') + if provider == 'selfsigned': + certificate = SelfSignedCertificateCryptography(module) + elif provider == 'acme': + certificate = AcmeCertificate(module, 'cryptography') + elif provider == 'ownca': + certificate = OwnCACertificateCryptography(module) + elif provider == 'entrust': + certificate = EntrustCertificate(module, 'cryptography') + else: + certificate = AssertOnlyCertificateCryptography(module) + + if module.params['state'] == 'present': + if module.check_mode: + result = certificate.dump(check_mode=True) + result['changed'] = module.params['force'] or not certificate.check(module) + module.exit_json(**result) + + certificate.generate(module) + else: + if module.check_mode: + result = certificate.dump(check_mode=True) + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + certificate.remove(module) + + result = certificate.dump() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/openssl_certificate_info.py b/plugins/modules/openssl_certificate_info.py new file mode 100644 index 00000000..f4605780 --- /dev/null +++ b/plugins/modules/openssl_certificate_info.py @@ -0,0 +1,861 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016-2017, Yanis Guenane +# Copyright: (c) 2017, Markus Teufelberger +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_certificate_info +short_description: Provide information of OpenSSL X.509 certificates +description: + - This module allows one to query information on OpenSSL certificates. + - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the + cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) + cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with + C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 + and will be removed in Ansible 2.13. +requirements: + - PyOpenSSL >= 0.15 or cryptography >= 1.6 +author: + - Felix Fontein (@felixfontein) + - Yanis Guenane (@Spredzy) + - Markus Teufelberger (@MarkusTeufelberger) +options: + path: + description: + - Remote absolute path where the certificate file is loaded from. + - Either I(path) or I(content) must be specified, but not both. + type: path + content: + description: + - Content of the X.509 certificate in PEM format. + - Either I(path) or I(content) must be specified, but not both. + type: str + valid_at: + description: + - A dict of names mapping to time specifications. Every time specified here + will be checked whether the certificate is valid at this point. See the + C(valid_at) return value for informations on the result. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h), and ASN.1 TIME (i.e. pattern C(YYYYMMDDHHMMSSZ)). + Note that all timestamps will be treated as being in UTC. + type: dict + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. + From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + +notes: + - All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern. + They are all in UTC. +seealso: +- module: openssl_certificate +''' + +EXAMPLES = r''' +- name: Generate a Self Signed OpenSSL certificate + openssl_certificate: + path: /etc/ssl/crt/ansible.com.crt + privatekey_path: /etc/ssl/private/ansible.com.pem + csr_path: /etc/ssl/csr/ansible.com.csr + provider: selfsigned + + +# Get information on the certificate + +- name: Get information on generated certificate + openssl_certificate_info: + path: /etc/ssl/crt/ansible.com.crt + register: result + +- name: Dump information + debug: + var: result + + +# Check whether the certificate is valid or not valid at certain times, fail +# if this is not the case. The first task (openssl_certificate_info) collects +# the information, and the second task (assert) validates the result and +# makes the playbook fail in case something is not as expected. + +- name: Test whether that certificate is valid tomorrow and/or in three weeks + openssl_certificate_info: + path: /etc/ssl/crt/ansible.com.crt + valid_at: + point_1: "+1d" + point_2: "+3w" + register: result + +- name: Validate that certificate is valid tomorrow, but not in three weeks + assert: + that: + - result.valid_at.point_1 # valid in one day + - not result.valid_at.point_2 # not valid in three weeks +''' + +RETURN = r''' +expired: + description: Whether the certificate is expired (i.e. C(notAfter) is in the past) + returned: success + type: bool +basic_constraints: + description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present. + returned: success + type: list + elements: str + sample: "[CA:TRUE, pathlen:1]" +basic_constraints_critical: + description: Whether the C(basic_constraints) extension is critical. + returned: success + type: bool +extended_key_usage: + description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present. + returned: success + type: list + elements: str + sample: "[Biometric Info, DVCS, Time Stamping]" +extended_key_usage_critical: + description: Whether the C(extended_key_usage) extension is critical. + returned: success + type: bool +extensions_by_oid: + description: Returns a dictionary for every extension OID + returned: success + type: dict + contains: + critical: + description: Whether the extension is critical. + returned: success + type: bool + value: + description: The Base64 encoded value (in DER format) of the extension + returned: success + type: str + sample: "MAMCAQU=" + sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}' +key_usage: + description: Entries in the C(key_usage) extension, or C(none) if extension is not present. + returned: success + type: str + sample: "[Key Agreement, Data Encipherment]" +key_usage_critical: + description: Whether the C(key_usage) extension is critical. + returned: success + type: bool +subject_alt_name: + description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present. + returned: success + type: list + elements: str + sample: "[DNS:www.ansible.com, IP:1.2.3.4]" +subject_alt_name_critical: + description: Whether the C(subject_alt_name) extension is critical. + returned: success + type: bool +ocsp_must_staple: + description: C(yes) if the OCSP Must Staple extension is present, C(none) otherwise. + returned: success + type: bool +ocsp_must_staple_critical: + description: Whether the C(ocsp_must_staple) extension is critical. + returned: success + type: bool +issuer: + description: + - The certificate's issuer. + - Note that for repeated values, only the last one will be returned. + returned: success + type: dict + sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}' +issuer_ordered: + description: The certificate's issuer as an ordered list of tuples. + returned: success + type: list + elements: list + sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]' + version_added: "2.9" +subject: + description: + - The certificate's subject as a dictionary. + - Note that for repeated values, only the last one will be returned. + returned: success + type: dict + sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}' +subject_ordered: + description: The certificate's subject as an ordered list of tuples. + returned: success + type: list + elements: list + sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]' + version_added: "2.9" +not_after: + description: C(notAfter) date as ASN.1 TIME + returned: success + type: str + sample: 20190413202428Z +not_before: + description: C(notBefore) date as ASN.1 TIME + returned: success + type: str + sample: 20190331202428Z +public_key: + description: Certificate's public key in PEM format + returned: success + type: str + sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." +public_key_fingerprints: + description: + - Fingerprints of certificate's public key. + - For every hash algorithm available, the fingerprint is computed. + returned: success + type: dict + sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', + 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." +signature_algorithm: + description: The signature algorithm used to sign the certificate. + returned: success + type: str + sample: sha256WithRSAEncryption +serial_number: + description: The certificate's serial number. + returned: success + type: int + sample: 1234 +version: + description: The certificate version. + returned: success + type: int + sample: 3 +valid_at: + description: For every time stamp provided in the I(valid_at) option, a + boolean whether the certificate is valid at that point in time + or not. + returned: success + type: dict +subject_key_identifier: + description: + - The certificate's subject key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(SubjectKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_key_identifier: + description: + - The certificate's authority key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_cert_issuer: + description: + - The certificate's authority cert issuer as a list of general names. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: list + elements: str + sample: "[DNS:www.ansible.com, IP:1.2.3.4]" + version_added: "2.9" +authority_cert_serial_number: + description: + - The certificate's authority cert serial number. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: int + sample: '12345' + version_added: "2.9" +ocsp_uri: + description: The OCSP responder URI, if included in the certificate. Will be + C(none) if no OCSP responder URI is included. + returned: success + type: str + version_added: "2.9" +''' + + +import abc +import binascii +import datetime +import os +import re +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_native, to_text, to_bytes +from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress as compat_ipaddress + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' +MINIMAL_PYOPENSSL_VERSION = '0.15' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: + # OpenSSL 1.1.0 or newer + OPENSSL_MUST_STAPLE_NAME = b"tlsfeature" + OPENSSL_MUST_STAPLE_VALUE = b"status_request" + else: + # OpenSSL 1.0.x or older + OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24" + OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05" +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.primitives import serialization + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" + + +class CertificateInfo(crypto_utils.OpenSSLObject): + def __init__(self, module, backend): + super(CertificateInfo, self).__init__( + module.params['path'] or '', + 'present', + False, + module.check_mode, + ) + self.backend = backend + self.module = module + self.content = module.params['content'] + if self.content is not None: + self.content = self.content.encode('utf-8') + + self.valid_at = module.params['valid_at'] + if self.valid_at: + for k, v in self.valid_at.items(): + if not isinstance(v, string_types): + self.module.fail_json( + msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v)) + ) + self.valid_at[k] = crypto_utils.get_relative_time_option(v, 'valid_at.{0}'.format(k)) + + def generate(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + def dump(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + @abc.abstractmethod + def _get_signature_algorithm(self): + pass + + @abc.abstractmethod + def _get_subject_ordered(self): + pass + + @abc.abstractmethod + def _get_issuer_ordered(self): + pass + + @abc.abstractmethod + def _get_version(self): + pass + + @abc.abstractmethod + def _get_key_usage(self): + pass + + @abc.abstractmethod + def _get_extended_key_usage(self): + pass + + @abc.abstractmethod + def _get_basic_constraints(self): + pass + + @abc.abstractmethod + def _get_ocsp_must_staple(self): + pass + + @abc.abstractmethod + def _get_subject_alt_name(self): + pass + + @abc.abstractmethod + def _get_not_before(self): + pass + + @abc.abstractmethod + def _get_not_after(self): + pass + + @abc.abstractmethod + def _get_public_key(self, binary): + pass + + @abc.abstractmethod + def _get_subject_key_identifier(self): + pass + + @abc.abstractmethod + def _get_authority_key_identifier(self): + pass + + @abc.abstractmethod + def _get_serial_number(self): + pass + + @abc.abstractmethod + def _get_all_extensions(self): + pass + + @abc.abstractmethod + def _get_ocsp_uri(self): + pass + + def get_info(self): + result = dict() + self.cert = crypto_utils.load_certificate(self.path, content=self.content, backend=self.backend) + + result['signature_algorithm'] = self._get_signature_algorithm() + subject = self._get_subject_ordered() + issuer = self._get_issuer_ordered() + result['subject'] = dict() + for k, v in subject: + result['subject'][k] = v + result['subject_ordered'] = subject + result['issuer'] = dict() + for k, v in issuer: + result['issuer'][k] = v + result['issuer_ordered'] = issuer + result['version'] = self._get_version() + result['key_usage'], result['key_usage_critical'] = self._get_key_usage() + result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage() + result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints() + result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple() + result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name() + + not_before = self._get_not_before() + not_after = self._get_not_after() + result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT) + result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT) + result['expired'] = not_after < datetime.datetime.utcnow() + + result['valid_at'] = dict() + if self.valid_at: + for k, v in self.valid_at.items(): + result['valid_at'][k] = not_before <= v <= not_after + + result['public_key'] = self._get_public_key(binary=False) + pk = self._get_public_key(binary=True) + result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + + if self.backend != 'pyopenssl': + ski = self._get_subject_key_identifier() + if ski is not None: + ski = to_native(binascii.hexlify(ski)) + ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) + result['subject_key_identifier'] = ski + + aki, aci, acsn = self._get_authority_key_identifier() + if aki is not None: + aki = to_native(binascii.hexlify(aki)) + aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) + result['authority_key_identifier'] = aki + result['authority_cert_issuer'] = aci + result['authority_cert_serial_number'] = acsn + + result['serial_number'] = self._get_serial_number() + result['extensions_by_oid'] = self._get_all_extensions() + result['ocsp_uri'] = self._get_ocsp_uri() + + return result + + +class CertificateInfoCryptography(CertificateInfo): + """Validate the supplied cert, using the cryptography backend""" + def __init__(self, module): + super(CertificateInfoCryptography, self).__init__(module, 'cryptography') + + def _get_signature_algorithm(self): + return crypto_utils.cryptography_oid_to_name(self.cert.signature_algorithm_oid) + + def _get_subject_ordered(self): + result = [] + for attribute in self.cert.subject: + result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + return result + + def _get_issuer_ordered(self): + result = [] + for attribute in self.cert.issuer: + result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + return result + + def _get_version(self): + if self.cert.version == x509.Version.v1: + return 1 + if self.cert.version == x509.Version.v3: + return 3 + return "unknown" + + def _get_key_usage(self): + try: + current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage) + current_key_usage = current_key_ext.value + key_usage = dict( + digital_signature=current_key_usage.digital_signature, + content_commitment=current_key_usage.content_commitment, + key_encipherment=current_key_usage.key_encipherment, + data_encipherment=current_key_usage.data_encipherment, + key_agreement=current_key_usage.key_agreement, + key_cert_sign=current_key_usage.key_cert_sign, + crl_sign=current_key_usage.crl_sign, + encipher_only=False, + decipher_only=False, + ) + if key_usage['key_agreement']: + key_usage.update(dict( + encipher_only=current_key_usage.encipher_only, + decipher_only=current_key_usage.decipher_only + )) + + key_usage_names = dict( + digital_signature='Digital Signature', + content_commitment='Non Repudiation', + key_encipherment='Key Encipherment', + data_encipherment='Data Encipherment', + key_agreement='Key Agreement', + key_cert_sign='Certificate Sign', + crl_sign='CRL Sign', + encipher_only='Encipher Only', + decipher_only='Decipher Only', + ) + return sorted([ + key_usage_names[name] for name, value in key_usage.items() if value + ]), current_key_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_extended_key_usage(self): + try: + ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) + return sorted([ + crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value + ]), ext_keyusage_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_basic_constraints(self): + try: + ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints) + result = [] + result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')) + if ext_keyusage_ext.value.path_length is not None: + result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length)) + return sorted(result), ext_keyusage_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_ocsp_must_staple(self): + try: + try: + # This only works with cryptography >= 2.1 + tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature) + value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value + except AttributeError as dummy: + # Fallback for cryptography < 2.1 + oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") + tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid) + value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05" + return value, tlsfeature_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_subject_alt_name(self): + try: + san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) + result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value] + return result, san_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_not_before(self): + return self.cert.not_valid_before + + def _get_not_after(self): + return self.cert.not_valid_after + + def _get_public_key(self, binary): + return self.cert.public_key().public_bytes( + serialization.Encoding.DER if binary else serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + + def _get_subject_key_identifier(self): + try: + ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + return ext.value.digest + except cryptography.x509.ExtensionNotFound: + return None + + def _get_authority_key_identifier(self): + try: + ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + issuer = None + if ext.value.authority_cert_issuer is not None: + issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] + return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number + except cryptography.x509.ExtensionNotFound: + return None, None, None + + def _get_serial_number(self): + return self.cert.serial_number + + def _get_all_extensions(self): + return crypto_utils.cryptography_get_extensions_from_cert(self.cert) + + def _get_ocsp_uri(self): + try: + ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess) + for desc in ext.value: + if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP: + if isinstance(desc.access_location, x509.UniformResourceIdentifier): + return desc.access_location.value + except x509.ExtensionNotFound as dummy: + pass + return None + + +class CertificateInfoPyOpenSSL(CertificateInfo): + """validate the supplied certificate.""" + + def __init__(self, module): + super(CertificateInfoPyOpenSSL, self).__init__(module, 'pyopenssl') + + def _get_signature_algorithm(self): + return to_text(self.cert.get_signature_algorithm()) + + def __get_name(self, name): + result = [] + for sub in name.get_components(): + result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) + return result + + def _get_subject_ordered(self): + return self.__get_name(self.cert.get_subject()) + + def _get_issuer_ordered(self): + return self.__get_name(self.cert.get_issuer()) + + def _get_version(self): + # Version numbers in certs are off by one: + # v1: 0, v2: 1, v3: 2 ... + return self.cert.get_version() + 1 + + def _get_extension(self, short_name): + for extension_idx in range(0, self.cert.get_extension_count()): + extension = self.cert.get_extension(extension_idx) + if extension.get_short_name() == short_name: + result = [ + crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') + ] + return sorted(result), bool(extension.get_critical()) + return None, False + + def _get_key_usage(self): + return self._get_extension(b'keyUsage') + + def _get_extended_key_usage(self): + return self._get_extension(b'extendedKeyUsage') + + def _get_basic_constraints(self): + return self._get_extension(b'basicConstraints') + + def _get_ocsp_must_staple(self): + extensions = [self.cert.get_extension(i) for i in range(0, self.cert.get_extension_count())] + oms_ext = [ + ext for ext in extensions + if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE + ] + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000: + # Older versions of libssl don't know about OCSP Must Staple + oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05']) + if oms_ext: + return True, bool(oms_ext[0].get_critical()) + else: + return None, False + + def _normalize_san(self, san): + if san.startswith('IP Address:'): + san = 'IP:' + san[len('IP Address:'):] + if san.startswith('IP:'): + ip = compat_ipaddress.ip_address(san[3:]) + san = 'IP:{0}'.format(ip.compressed) + return san + + def _get_subject_alt_name(self): + for extension_idx in range(0, self.cert.get_extension_count()): + extension = self.cert.get_extension(extension_idx) + if extension.get_short_name() == b'subjectAltName': + result = [self._normalize_san(altname.strip()) for altname in + to_text(extension, errors='surrogate_or_strict').split(', ')] + return result, bool(extension.get_critical()) + return None, False + + def _get_not_before(self): + time_string = to_native(self.cert.get_notBefore()) + return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") + + def _get_not_after(self): + time_string = to_native(self.cert.get_notAfter()) + return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") + + def _get_public_key(self, binary): + try: + return crypto.dump_publickey( + crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM, + self.cert.get_pubkey() + ) + except AttributeError: + try: + # pyOpenSSL < 16.0: + bio = crypto._new_mem_buf() + if binary: + rc = crypto._lib.i2d_PUBKEY_bio(bio, self.cert.get_pubkey()._pkey) + else: + rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.cert.get_pubkey()._pkey) + if rc != 1: + crypto._raise_current_error() + return crypto._bio_to_string(bio) + except AttributeError: + self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' + 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') + + def _get_subject_key_identifier(self): + # Won't be implemented + return None + + def _get_authority_key_identifier(self): + # Won't be implemented + return None, None, None + + def _get_serial_number(self): + return self.cert.get_serial_number() + + def _get_all_extensions(self): + return crypto_utils.pyopenssl_get_extensions_from_cert(self.cert) + + def _get_ocsp_uri(self): + for i in range(self.cert.get_extension_count()): + ext = self.cert.get_extension(i) + if ext.get_short_name() == b'authorityInfoAccess': + v = str(ext) + m = re.search('^OCSP - URI:(.*)$', v, flags=re.MULTILINE) + if m: + return m.group(1) + return None + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str'), + valid_at=dict(type='dict'), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + try: + if module.params['path'] is not None: + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detect what backend we can use + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # If cryptography is available we'll use it + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Fail if no backend has been found + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + try: + getattr(crypto.X509Req, 'get_extensions') + except AttributeError: + module.fail_json(msg='You need to have PyOpenSSL>=0.15') + + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + certificate = CertificateInfoPyOpenSSL(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + certificate = CertificateInfoCryptography(module) + + result = certificate.get_info() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/openssl_csr.py b/plugins/modules/openssl_csr.py new file mode 100644 index 00000000..c23c66dd --- /dev/null +++ b/plugins/modules/openssl_csr.py @@ -0,0 +1,1143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyrigt: (c) 2017, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_csr +short_description: Generate OpenSSL Certificate Signing Request (CSR) +description: + - This module allows one to (re)generate OpenSSL certificate signing requests. + - It uses the pyOpenSSL python library to interact with openssl. This module supports + the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple + extensions. + - "Please note that the module regenerates existing CSR if it doesn't match the module's + options, or if it seems to be corrupt. If you are concerned that this could overwrite + your existing CSR, consider using the I(backup) option." + - The module can use the cryptography Python library, or the pyOpenSSL Python + library. By default, it tries to detect which one is available. This can be + overridden with the I(select_crypto_backend) option. Please note that the + PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." +requirements: + - Either cryptography >= 1.3 + - Or pyOpenSSL >= 0.15 +author: +- Yanis Guenane (@Spredzy) +options: + state: + description: + - Whether the certificate signing request should exist or not, taking action if the state is different from what is stated. + type: str + default: present + choices: [ absent, present ] + digest: + description: + - The digest used when signing the certificate signing request with the private key. + type: str + default: sha256 + privatekey_path: + description: + - The path to the private key to use when signing the certificate signing request. + - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. + type: path + privatekey_content: + description: + - The content of the private key to use when signing the certificate signing request. + - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. + type: str + privatekey_passphrase: + description: + - The passphrase for the private key. + - This is required if the private key is password protected. + type: str + version: + description: + - The version of the certificate signing request. + - "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1) + is 1." + - This option will no longer accept unsupported values from Ansible 2.14 on. + type: int + default: 1 + force: + description: + - Should the certificate signing request be forced regenerated by this ansible module. + type: bool + default: no + path: + description: + - The name of the file into which the generated OpenSSL certificate signing request will be written. + type: path + required: true + subject: + description: + - Key/value pairs that will be present in the subject name field of the certificate signing request. + - If you need to specify more than one value with the same key, use a list as value. + type: dict + country_name: + description: + - The countryName field of the certificate signing request subject. + type: str + aliases: [ C, countryName ] + state_or_province_name: + description: + - The stateOrProvinceName field of the certificate signing request subject. + type: str + aliases: [ ST, stateOrProvinceName ] + locality_name: + description: + - The localityName field of the certificate signing request subject. + type: str + aliases: [ L, localityName ] + organization_name: + description: + - The organizationName field of the certificate signing request subject. + type: str + aliases: [ O, organizationName ] + organizational_unit_name: + description: + - The organizationalUnitName field of the certificate signing request subject. + type: str + aliases: [ OU, organizationalUnitName ] + common_name: + description: + - The commonName field of the certificate signing request subject. + type: str + aliases: [ CN, commonName ] + email_address: + description: + - The emailAddress field of the certificate signing request subject. + type: str + aliases: [ E, emailAddress ] + subject_alt_name: + description: + - SAN extension to attach to the certificate signing request. + - This can either be a 'comma separated string' or a YAML list. + - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), + C(otherName) and the ones specific to your CA) + - Note that if no SAN is specified, but a common name, the common + name will be added as a SAN except if C(useCommonNameForSAN) is + set to I(false). + - More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6). + type: list + elements: str + aliases: [ subjectAltName ] + subject_alt_name_critical: + description: + - Should the subjectAltName extension be considered as critical. + type: bool + aliases: [ subjectAltName_critical ] + use_common_name_for_san: + description: + - If set to C(yes), the module will fill the common name in for + C(subject_alt_name) with C(DNS:) prefix if no SAN is specified. + type: bool + default: yes + aliases: [ useCommonNameForSAN ] + key_usage: + description: + - This defines the purpose (e.g. encipherment, signature, certificate signing) + of the key contained in the certificate. + type: list + elements: str + aliases: [ keyUsage ] + key_usage_critical: + description: + - Should the keyUsage extension be considered as critical. + type: bool + aliases: [ keyUsage_critical ] + extended_key_usage: + description: + - Additional restrictions (e.g. client authentication, server authentication) + on the allowed purposes for which the public key may be used. + type: list + elements: str + aliases: [ extKeyUsage, extendedKeyUsage ] + extended_key_usage_critical: + description: + - Should the extkeyUsage extension be considered as critical. + type: bool + aliases: [ extKeyUsage_critical, extendedKeyUsage_critical ] + basic_constraints: + description: + - Indicates basic constraints, such as if the certificate is a CA. + type: list + elements: str + aliases: [ basicConstraints ] + basic_constraints_critical: + description: + - Should the basicConstraints extension be considered as critical. + type: bool + aliases: [ basicConstraints_critical ] + ocsp_must_staple: + description: + - Indicates that the certificate should contain the OCSP Must Staple + extension (U(https://tools.ietf.org/html/rfc7633)). + type: bool + aliases: [ ocspMustStaple ] + ocsp_must_staple_critical: + description: + - Should the OCSP Must Staple extension be considered as critical + - Note that according to the RFC, this extension should not be marked + as critical, as old clients not knowing about OCSP Must Staple + are required to reject such certificates + (see U(https://tools.ietf.org/html/rfc7633#section-4)). + type: bool + aliases: [ ocspMustStaple_critical ] + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. + From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + backup: + description: + - Create a backup file including a timestamp so you can get the original + CSR back if you overwrote it with a new one by accident. + type: bool + default: no + create_subject_key_identifier: + description: + - Create the Subject Key Identifier from the public key. + - "Please note that commercial CAs can ignore the value, respectively use a value of + their own choice instead. Specifying this option is mostly useful for self-signed + certificates or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + type: bool + default: no + subject_key_identifier: + description: + - The subject key identifier as a hex string, where two bytes are separated by colons. + - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this option can only be used if I(create_subject_key_identifier) is C(no). + - Note that this is only supported if the C(cryptography) backend is used! + type: str + authority_key_identifier: + description: + - The authority key identifier as a hex string, where two bytes are separated by colons. + - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" + - If specified, I(authority_cert_issuer) must also be specified. + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: str + authority_cert_issuer: + description: + - Names that will be present in the authority cert issuer field of the certificate signing request. + - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), + C(otherName) and the ones specific to your CA) + - "Example: C(DNS:ca.example.org)" + - If specified, I(authority_key_identifier) must also be specified. + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: list + elements: str + authority_cert_serial_number: + description: + - The authority cert serial number. + - Note that this is only supported if the C(cryptography) backend is used! + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: int + return_content: + description: + - If set to C(yes), will return the (current or generated) CSR's content as I(csr). + type: bool + default: no +extends_documentation_fragment: +- files +notes: + - If the certificate signing request already exists it will be checked whether subjectAltName, + keyUsage, extendedKeyUsage and basicConstraints only contain the requested values, whether + OCSP Must Staple is as requested, and if the request was signed by the given private key. +seealso: +- module: openssl_certificate +- module: openssl_dhparam +- module: openssl_pkcs12 +- module: openssl_privatekey +- module: openssl_publickey +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL Certificate Signing Request + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with an inline key + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_content: "{{ private_key_content }}" + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with a passphrase protected private key + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + privatekey_passphrase: ansible + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with Subject information + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + country_name: FR + organization_name: Ansible + email_address: jdoe@ansible.com + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with subjectAltName extension + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + subject_alt_name: 'DNS:www.ansible.com,DNS:m.ansible.com' + +- name: Generate an OpenSSL CSR with subjectAltName extension with dynamic list + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + subject_alt_name: "{{ item.value | map('regex_replace', '^', 'DNS:') | list }}" + with_dict: + dns_server: + - www.ansible.com + - m.ansible.com + +- name: Force regenerate an OpenSSL Certificate Signing Request + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + force: yes + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with special key usages + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + key_usage: + - digitalSignature + - keyAgreement + extended_key_usage: + - clientAuth + +- name: Generate an OpenSSL Certificate Signing Request with OCSP Must Staple + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + ocsp_must_staple: yes +''' + +RETURN = r''' +privatekey: + description: + - Path to the TLS/SSL private key the CSR was generated for + - Will be C(none) if the private key has been provided in I(privatekey_content). + returned: changed or success + type: str + sample: /etc/ssl/private/ansible.com.pem +filename: + description: Path to the generated Certificate Signing Request + returned: changed or success + type: str + sample: /etc/ssl/csr/www.ansible.com.csr +subject: + description: A list of the subject tuples attached to the CSR + returned: changed or success + type: list + elements: list + sample: "[('CN', 'www.ansible.com'), ('O', 'Ansible')]" +subjectAltName: + description: The alternative names this CSR is valid for + returned: changed or success + type: list + elements: str + sample: [ 'DNS:www.ansible.com', 'DNS:m.ansible.com' ] +keyUsage: + description: Purpose for which the public key may be used + returned: changed or success + type: list + elements: str + sample: [ 'digitalSignature', 'keyAgreement' ] +extendedKeyUsage: + description: Additional restriction on the public key purposes + returned: changed or success + type: list + elements: str + sample: [ 'clientAuth' ] +basicConstraints: + description: Indicates if the certificate belongs to a CA + returned: changed or success + type: list + elements: str + sample: ['CA:TRUE', 'pathLenConstraint:0'] +ocsp_must_staple: + description: Indicates whether the certificate has the OCSP + Must Staple feature enabled + returned: changed or success + type: bool + sample: false +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/www.ansible.com.csr.2019-03-09@11:22~ +csr: + description: The (current or generated) CSR's content. + returned: if I(state) is C(present) and I(return_content) is C(yes) + type: str + version_added: "2.10" +''' + +import abc +import binascii +import os +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native, to_bytes, to_text +from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress as compat_ipaddress + +MINIMAL_PYOPENSSL_VERSION = '0.15' +MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: + # OpenSSL 1.1.0 or newer + OPENSSL_MUST_STAPLE_NAME = b"tlsfeature" + OPENSSL_MUST_STAPLE_VALUE = b"status_request" + else: + # OpenSSL 1.0.x or older + OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24" + OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05" + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.x509 + import cryptography.x509.oid + import cryptography.exceptions + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization + import cryptography.hazmat.primitives.hashes + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + CRYPTOGRAPHY_MUST_STAPLE_NAME = cryptography.x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") + CRYPTOGRAPHY_MUST_STAPLE_VALUE = b"\x30\x03\x02\x01\x05" + + +class CertificateSigningRequestError(crypto_utils.OpenSSLObjectError): + pass + + +class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): + + def __init__(self, module): + super(CertificateSigningRequestBase, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.digest = module.params['digest'] + self.privatekey_path = module.params['privatekey_path'] + self.privatekey_content = module.params['privatekey_content'] + if self.privatekey_content is not None: + self.privatekey_content = self.privatekey_content.encode('utf-8') + self.privatekey_passphrase = module.params['privatekey_passphrase'] + self.version = module.params['version'] + self.subjectAltName = module.params['subject_alt_name'] + self.subjectAltName_critical = module.params['subject_alt_name_critical'] + self.keyUsage = module.params['key_usage'] + self.keyUsage_critical = module.params['key_usage_critical'] + self.extendedKeyUsage = module.params['extended_key_usage'] + self.extendedKeyUsage_critical = module.params['extended_key_usage_critical'] + self.basicConstraints = module.params['basic_constraints'] + self.basicConstraints_critical = module.params['basic_constraints_critical'] + self.ocspMustStaple = module.params['ocsp_must_staple'] + self.ocspMustStaple_critical = module.params['ocsp_must_staple_critical'] + self.create_subject_key_identifier = module.params['create_subject_key_identifier'] + self.subject_key_identifier = module.params['subject_key_identifier'] + self.authority_key_identifier = module.params['authority_key_identifier'] + self.authority_cert_issuer = module.params['authority_cert_issuer'] + self.authority_cert_serial_number = module.params['authority_cert_serial_number'] + self.request = None + self.privatekey = None + self.csr_bytes = None + self.return_content = module.params['return_content'] + + if self.create_subject_key_identifier and self.subject_key_identifier is not None: + module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true') + + self.backup = module.params['backup'] + self.backup_file = None + + self.subject = [ + ('C', module.params['country_name']), + ('ST', module.params['state_or_province_name']), + ('L', module.params['locality_name']), + ('O', module.params['organization_name']), + ('OU', module.params['organizational_unit_name']), + ('CN', module.params['common_name']), + ('emailAddress', module.params['email_address']), + ] + + if module.params['subject']: + self.subject = self.subject + crypto_utils.parse_name_field(module.params['subject']) + self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]] + + if not self.subjectAltName and module.params['use_common_name_for_san']: + for sub in self.subject: + if sub[0] in ('commonName', 'CN'): + self.subjectAltName = ['DNS:%s' % sub[1]] + break + + if self.subject_key_identifier is not None: + try: + self.subject_key_identifier = binascii.unhexlify(self.subject_key_identifier.replace(':', '')) + except Exception as e: + raise CertificateSigningRequestError('Cannot parse subject_key_identifier: {0}'.format(e)) + + if self.authority_key_identifier is not None: + try: + self.authority_key_identifier = binascii.unhexlify(self.authority_key_identifier.replace(':', '')) + except Exception as e: + raise CertificateSigningRequestError('Cannot parse authority_key_identifier: {0}'.format(e)) + + @abc.abstractmethod + def _generate_csr(self): + pass + + def generate(self, module): + '''Generate the certificate signing request.''' + if not self.check(module, perms_required=False) or self.force: + result = self._generate_csr() + if self.backup: + self.backup_file = module.backup_local(self.path) + if self.return_content: + self.csr_bytes = result + crypto_utils.write_file(module, result) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + @abc.abstractmethod + def _load_private_key(self): + pass + + @abc.abstractmethod + def _check_csr(self): + pass + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + state_and_perms = super(CertificateSigningRequestBase, self).check(module, perms_required) + + self._load_private_key() + + if not state_and_perms: + return False + + return self._check_csr() + + def remove(self, module): + if self.backup: + self.backup_file = module.backup_local(self.path) + super(CertificateSigningRequestBase, self).remove(module) + + def dump(self): + '''Serialize the object into a dictionary.''' + + result = { + 'privatekey': self.privatekey_path, + 'filename': self.path, + 'subject': self.subject, + 'subjectAltName': self.subjectAltName, + 'keyUsage': self.keyUsage, + 'extendedKeyUsage': self.extendedKeyUsage, + 'basicConstraints': self.basicConstraints, + 'ocspMustStaple': self.ocspMustStaple, + 'changed': self.changed + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + if self.csr_bytes is None: + self.csr_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['csr'] = self.csr_bytes.decode('utf-8') if self.csr_bytes else None + + return result + + +class CertificateSigningRequestPyOpenSSL(CertificateSigningRequestBase): + + def __init__(self, module): + if module.params['create_subject_key_identifier']: + module.fail_json(msg='You cannot use create_subject_key_identifier with the pyOpenSSL backend!') + for o in ('subject_key_identifier', 'authority_key_identifier', 'authority_cert_issuer', 'authority_cert_serial_number'): + if module.params[o] is not None: + module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o)) + super(CertificateSigningRequestPyOpenSSL, self).__init__(module) + + def _generate_csr(self): + req = crypto.X509Req() + req.set_version(self.version - 1) + subject = req.get_subject() + for entry in self.subject: + if entry[1] is not None: + # Workaround for https://github.com/pyca/pyopenssl/issues/165 + nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(entry[0])) + if nid == 0: + raise CertificateSigningRequestError('Unknown subject field identifier "{0}"'.format(entry[0])) + res = OpenSSL._util.lib.X509_NAME_add_entry_by_NID(subject._name, nid, OpenSSL._util.lib.MBSTRING_UTF8, to_bytes(entry[1]), -1, -1, 0) + if res == 0: + raise CertificateSigningRequestError('Invalid value for subject field identifier "{0}": {1}'.format(entry[0], entry[1])) + + extensions = [] + if self.subjectAltName: + altnames = ', '.join(self.subjectAltName) + try: + extensions.append(crypto.X509Extension(b"subjectAltName", self.subjectAltName_critical, altnames.encode('ascii'))) + except OpenSSL.crypto.Error as e: + raise CertificateSigningRequestError( + 'Error while parsing Subject Alternative Names {0} (check for missing type prefix, such as "DNS:"!): {1}'.format( + ', '.join(["{0}".format(san) for san in self.subjectAltName]), str(e) + ) + ) + + if self.keyUsage: + usages = ', '.join(self.keyUsage) + extensions.append(crypto.X509Extension(b"keyUsage", self.keyUsage_critical, usages.encode('ascii'))) + + if self.extendedKeyUsage: + usages = ', '.join(self.extendedKeyUsage) + extensions.append(crypto.X509Extension(b"extendedKeyUsage", self.extendedKeyUsage_critical, usages.encode('ascii'))) + + if self.basicConstraints: + usages = ', '.join(self.basicConstraints) + extensions.append(crypto.X509Extension(b"basicConstraints", self.basicConstraints_critical, usages.encode('ascii'))) + + if self.ocspMustStaple: + extensions.append(crypto.X509Extension(OPENSSL_MUST_STAPLE_NAME, self.ocspMustStaple_critical, OPENSSL_MUST_STAPLE_VALUE)) + + if extensions: + req.add_extensions(extensions) + + req.set_pubkey(self.privatekey) + req.sign(self.privatekey, self.digest) + self.request = req + + return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.request) + + def _load_private_key(self): + try: + self.privatekey = crypto_utils.load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise CertificateSigningRequestError(exc) + + def _normalize_san(self, san): + # Apparently OpenSSL returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string + # although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004) + if san.startswith('IP Address:'): + san = 'IP:' + san[len('IP Address:'):] + if san.startswith('IP:'): + ip = compat_ipaddress.ip_address(san[3:]) + san = 'IP:{0}'.format(ip.compressed) + return san + + def _check_csr(self): + def _check_subject(csr): + subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in self.subject] + current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in csr.get_subject().get_components()] + if not set(subject) == set(current_subject): + return False + + return True + + def _check_subjectAltName(extensions): + altnames_ext = next((ext for ext in extensions if ext.get_short_name() == b'subjectAltName'), '') + altnames = [self._normalize_san(altname.strip()) for altname in + to_text(altnames_ext, errors='surrogate_or_strict').split(',') if altname.strip()] + if self.subjectAltName: + if (set(altnames) != set([self._normalize_san(to_text(name)) for name in self.subjectAltName]) or + altnames_ext.get_critical() != self.subjectAltName_critical): + return False + else: + if altnames: + return False + + return True + + def _check_keyUsage_(extensions, extName, expected, critical): + usages_ext = [ext for ext in extensions if ext.get_short_name() == extName] + if (not usages_ext and expected) or (usages_ext and not expected): + return False + elif not usages_ext and not expected: + return True + else: + current = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage.strip())) for usage in str(usages_ext[0]).split(',')] + expected = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage)) for usage in expected] + return set(current) == set(expected) and usages_ext[0].get_critical() == critical + + def _check_keyUsage(extensions): + usages_ext = [ext for ext in extensions if ext.get_short_name() == b'keyUsage'] + if (not usages_ext and self.keyUsage) or (usages_ext and not self.keyUsage): + return False + elif not usages_ext and not self.keyUsage: + return True + else: + # OpenSSL._util.lib.OBJ_txt2nid() always returns 0 for all keyUsage values + # (since keyUsage has a fixed bitfield for these values and is not extensible). + # Therefore, we create an extension for the wanted values, and compare the + # data of the extensions (which is the serialized bitfield). + expected_ext = crypto.X509Extension(b"keyUsage", False, ', '.join(self.keyUsage).encode('ascii')) + return usages_ext[0].get_data() == expected_ext.get_data() and usages_ext[0].get_critical() == self.keyUsage_critical + + def _check_extenededKeyUsage(extensions): + return _check_keyUsage_(extensions, b'extendedKeyUsage', self.extendedKeyUsage, self.extendedKeyUsage_critical) + + def _check_basicConstraints(extensions): + return _check_keyUsage_(extensions, b'basicConstraints', self.basicConstraints, self.basicConstraints_critical) + + def _check_ocspMustStaple(extensions): + oms_ext = [ext for ext in extensions if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE] + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000: + # Older versions of libssl don't know about OCSP Must Staple + oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05']) + if self.ocspMustStaple: + return len(oms_ext) > 0 and oms_ext[0].get_critical() == self.ocspMustStaple_critical + else: + return len(oms_ext) == 0 + + def _check_extensions(csr): + extensions = csr.get_extensions() + return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and + _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and + _check_ocspMustStaple(extensions)) + + def _check_signature(csr): + try: + return csr.verify(self.privatekey) + except crypto.Error: + return False + + try: + csr = crypto_utils.load_certificate_request(self.path, backend='pyopenssl') + except Exception as dummy: + return False + + return _check_subject(csr) and _check_extensions(csr) and _check_signature(csr) + + +class CertificateSigningRequestCryptography(CertificateSigningRequestBase): + + def __init__(self, module): + super(CertificateSigningRequestCryptography, self).__init__(module) + self.cryptography_backend = cryptography.hazmat.backends.default_backend() + self.module = module + if self.version != 1: + module.warn('The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)') + + def _generate_csr(self): + csr = cryptography.x509.CertificateSigningRequestBuilder() + try: + csr = csr.subject_name(cryptography.x509.Name([ + cryptography.x509.NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject + ])) + except ValueError as e: + raise CertificateSigningRequestError(e) + + if self.subjectAltName: + csr = csr.add_extension(cryptography.x509.SubjectAlternativeName([ + crypto_utils.cryptography_get_name(name) for name in self.subjectAltName + ]), critical=self.subjectAltName_critical) + + if self.keyUsage: + params = crypto_utils.cryptography_parse_key_usage_params(self.keyUsage) + csr = csr.add_extension(cryptography.x509.KeyUsage(**params), critical=self.keyUsage_critical) + + if self.extendedKeyUsage: + usages = [crypto_utils.cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage] + csr = csr.add_extension(cryptography.x509.ExtendedKeyUsage(usages), critical=self.extendedKeyUsage_critical) + + if self.basicConstraints: + params = {} + ca, path_length = crypto_utils.cryptography_get_basic_constraints(self.basicConstraints) + csr = csr.add_extension(cryptography.x509.BasicConstraints(ca, path_length), critical=self.basicConstraints_critical) + + if self.ocspMustStaple: + try: + # This only works with cryptography >= 2.1 + csr = csr.add_extension(cryptography.x509.TLSFeature([cryptography.x509.TLSFeatureType.status_request]), critical=self.ocspMustStaple_critical) + except AttributeError as dummy: + csr = csr.add_extension( + cryptography.x509.UnrecognizedExtension(CRYPTOGRAPHY_MUST_STAPLE_NAME, CRYPTOGRAPHY_MUST_STAPLE_VALUE), + critical=self.ocspMustStaple_critical + ) + + if self.create_subject_key_identifier: + csr = csr.add_extension( + cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), + critical=False + ) + elif self.subject_key_identifier is not None: + csr = csr.add_extension(cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier), critical=False) + + if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: + issuers = None + if self.authority_cert_issuer is not None: + issuers = [crypto_utils.cryptography_get_name(n) for n in self.authority_cert_issuer] + csr = csr.add_extension( + cryptography.x509.AuthorityKeyIdentifier(self.authority_key_identifier, issuers, self.authority_cert_serial_number), + critical=False + ) + + digest = None + if crypto_utils.cryptography_key_needs_digest_for_signing(self.privatekey): + if self.digest == 'sha256': + digest = cryptography.hazmat.primitives.hashes.SHA256() + elif self.digest == 'sha384': + digest = cryptography.hazmat.primitives.hashes.SHA384() + elif self.digest == 'sha512': + digest = cryptography.hazmat.primitives.hashes.SHA512() + elif self.digest == 'sha1': + digest = cryptography.hazmat.primitives.hashes.SHA1() + elif self.digest == 'md5': + digest = cryptography.hazmat.primitives.hashes.MD5() + # FIXME + else: + raise CertificateSigningRequestError('Unsupported digest "{0}"'.format(self.digest)) + try: + self.request = csr.sign(self.privatekey, digest, self.cryptography_backend) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + return self.request.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM) + + def _load_private_key(self): + try: + if self.privatekey_content is not None: + content = self.privatekey_content + else: + with open(self.privatekey_path, 'rb') as f: + content = f.read() + self.privatekey = cryptography.hazmat.primitives.serialization.load_pem_private_key( + content, + None if self.privatekey_passphrase is None else to_bytes(self.privatekey_passphrase), + backend=self.cryptography_backend + ) + except Exception as e: + raise CertificateSigningRequestError(e) + + def _check_csr(self): + def _check_subject(csr): + subject = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.subject] + current_subject = [(sub.oid, sub.value) for sub in csr.subject] + return set(subject) == set(current_subject) + + def _find_extension(extensions, exttype): + return next( + (ext for ext in extensions if isinstance(ext.value, exttype)), + None + ) + + def _check_subjectAltName(extensions): + current_altnames_ext = _find_extension(extensions, cryptography.x509.SubjectAlternativeName) + current_altnames = [str(altname) for altname in current_altnames_ext.value] if current_altnames_ext else [] + altnames = [str(crypto_utils.cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else [] + if set(altnames) != set(current_altnames): + return False + if altnames: + if current_altnames_ext.critical != self.subjectAltName_critical: + return False + return True + + def _check_keyUsage(extensions): + current_keyusage_ext = _find_extension(extensions, cryptography.x509.KeyUsage) + if not self.keyUsage: + return current_keyusage_ext is None + elif current_keyusage_ext is None: + return False + params = crypto_utils.cryptography_parse_key_usage_params(self.keyUsage) + for param in params: + if getattr(current_keyusage_ext.value, '_' + param) != params[param]: + return False + if current_keyusage_ext.critical != self.keyUsage_critical: + return False + return True + + def _check_extenededKeyUsage(extensions): + current_usages_ext = _find_extension(extensions, cryptography.x509.ExtendedKeyUsage) + current_usages = [str(usage) for usage in current_usages_ext.value] if current_usages_ext else [] + usages = [str(crypto_utils.cryptography_name_to_oid(usage)) for usage in self.extendedKeyUsage] if self.extendedKeyUsage else [] + if set(current_usages) != set(usages): + return False + if usages: + if current_usages_ext.critical != self.extendedKeyUsage_critical: + return False + return True + + def _check_basicConstraints(extensions): + bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints) + current_ca = bc_ext.value.ca if bc_ext else False + current_path_length = bc_ext.value.path_length if bc_ext else None + ca, path_length = crypto_utils.cryptography_get_basic_constraints(self.basicConstraints) + # Check CA flag + if ca != current_ca: + return False + # Check path length + if path_length != current_path_length: + return False + # Check criticality + if self.basicConstraints: + if bc_ext.critical != self.basicConstraints_critical: + return False + return True + + def _check_ocspMustStaple(extensions): + try: + # This only works with cryptography >= 2.1 + tlsfeature_ext = _find_extension(extensions, cryptography.x509.TLSFeature) + has_tlsfeature = True + except AttributeError as dummy: + tlsfeature_ext = next( + (ext for ext in extensions if ext.value.oid == CRYPTOGRAPHY_MUST_STAPLE_NAME), + None + ) + has_tlsfeature = False + if self.ocspMustStaple: + if not tlsfeature_ext or tlsfeature_ext.critical != self.ocspMustStaple_critical: + return False + if has_tlsfeature: + return cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value + else: + return tlsfeature_ext.value.value == CRYPTOGRAPHY_MUST_STAPLE_VALUE + else: + return tlsfeature_ext is None + + def _check_subject_key_identifier(extensions): + ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier) + if self.create_subject_key_identifier or self.subject_key_identifier is not None: + if not ext or ext.critical: + return False + if self.create_subject_key_identifier: + digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()).digest + return ext.value.digest == digest + else: + return ext.value.digest == self.subject_key_identifier + else: + return ext is None + + def _check_authority_key_identifier(extensions): + ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier) + if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: + if not ext or ext.critical: + return False + aci = None + csr_aci = None + if self.authority_cert_issuer is not None: + aci = [str(crypto_utils.cryptography_get_name(n)) for n in self.authority_cert_issuer] + if ext.value.authority_cert_issuer is not None: + csr_aci = [str(n) for n in ext.value.authority_cert_issuer] + return (ext.value.key_identifier == self.authority_key_identifier + and csr_aci == aci + and ext.value.authority_cert_serial_number == self.authority_cert_serial_number) + else: + return ext is None + + def _check_extensions(csr): + extensions = csr.extensions + return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and + _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and + _check_ocspMustStaple(extensions) and _check_subject_key_identifier(extensions) and + _check_authority_key_identifier(extensions)) + + def _check_signature(csr): + if not csr.is_signature_valid: + return False + # To check whether public key of CSR belongs to private key, + # encode both public keys and compare PEMs. + key_a = csr.public_key().public_bytes( + cryptography.hazmat.primitives.serialization.Encoding.PEM, + cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo + ) + key_b = self.privatekey.public_key().public_bytes( + cryptography.hazmat.primitives.serialization.Encoding.PEM, + cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo + ) + return key_a == key_b + + try: + csr = crypto_utils.load_certificate_request(self.path, backend='cryptography') + except Exception as dummy: + return False + + return _check_subject(csr) and _check_extensions(csr) and _check_signature(csr) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['absent', 'present']), + digest=dict(type='str', default='sha256'), + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str'), + privatekey_passphrase=dict(type='str', no_log=True), + version=dict(type='int', default=1), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + subject=dict(type='dict'), + country_name=dict(type='str', aliases=['C', 'countryName']), + state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']), + locality_name=dict(type='str', aliases=['L', 'localityName']), + organization_name=dict(type='str', aliases=['O', 'organizationName']), + organizational_unit_name=dict(type='str', aliases=['OU', 'organizationalUnitName']), + common_name=dict(type='str', aliases=['CN', 'commonName']), + email_address=dict(type='str', aliases=['E', 'emailAddress']), + subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName']), + subject_alt_name_critical=dict(type='bool', default=False, aliases=['subjectAltName_critical']), + use_common_name_for_san=dict(type='bool', default=True, aliases=['useCommonNameForSAN']), + key_usage=dict(type='list', elements='str', aliases=['keyUsage']), + key_usage_critical=dict(type='bool', default=False, aliases=['keyUsage_critical']), + extended_key_usage=dict(type='list', elements='str', aliases=['extKeyUsage', 'extendedKeyUsage']), + extended_key_usage_critical=dict(type='bool', default=False, aliases=['extKeyUsage_critical', 'extendedKeyUsage_critical']), + basic_constraints=dict(type='list', elements='str', aliases=['basicConstraints']), + basic_constraints_critical=dict(type='bool', default=False, aliases=['basicConstraints_critical']), + ocsp_must_staple=dict(type='bool', default=False, aliases=['ocspMustStaple']), + ocsp_must_staple_critical=dict(type='bool', default=False, aliases=['ocspMustStaple_critical']), + backup=dict(type='bool', default=False), + create_subject_key_identifier=dict(type='bool', default=False), + subject_key_identifier=dict(type='str'), + authority_key_identifier=dict(type='str'), + authority_cert_issuer=dict(type='list', elements='str'), + authority_cert_serial_number=dict(type='int'), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), + return_content=dict(type='bool', default=False), + ), + required_together=[('authority_cert_issuer', 'authority_cert_serial_number')], + required_if=[('state', 'present', ['privatekey_path', 'privatekey_content'], True)], + mutually_exclusive=( + ['privatekey_path', 'privatekey_content'], + ), + add_file_common_args=True, + supports_check_mode=True, + ) + + if module.params['version'] != 1: + module.deprecate('The version option will only support allowed values from Ansible 2.14 on. ' + 'Currently, only the value 1 is allowed by RFC 2986', version='2.14') + + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir) + + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # First try cryptography, then pyOpenSSL + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + try: + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + try: + getattr(crypto.X509Req, 'get_extensions') + except AttributeError: + module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs') + + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + csr = CertificateSigningRequestPyOpenSSL(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + csr = CertificateSigningRequestCryptography(module) + + if module.params['state'] == 'present': + if module.check_mode: + result = csr.dump() + result['changed'] = module.params['force'] or not csr.check(module) + module.exit_json(**result) + + csr.generate(module) + + else: + if module.check_mode: + result = csr.dump() + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + csr.remove(module) + + result = csr.dump() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/openssl_csr_info.py b/plugins/modules/openssl_csr_info.py new file mode 100644 index 00000000..6c03bf18 --- /dev/null +++ b/plugins/modules/openssl_csr_info.py @@ -0,0 +1,665 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016-2017, Yanis Guenane +# Copyright: (c) 2017, Markus Teufelberger +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_csr_info +short_description: Provide information of OpenSSL Certificate Signing Requests (CSR) +description: + - This module allows one to query information on OpenSSL Certificate Signing Requests (CSR). + - In case the CSR signature cannot be validated, the module will fail. In this case, all return + variables are still returned. + - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the + cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) + cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with + C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 + and will be removed in Ansible 2.13. +requirements: + - PyOpenSSL >= 0.15 or cryptography >= 1.3 +author: + - Felix Fontein (@felixfontein) + - Yanis Guenane (@Spredzy) +options: + path: + description: + - Remote absolute path where the CSR file is loaded from. + - Either I(path) or I(content) must be specified, but not both. + type: path + content: + description: + - Content of the CSR file. + - Either I(path) or I(content) must be specified, but not both. + type: str + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. + From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + +seealso: +- module: openssl_csr +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL Certificate Signing Request + openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + +- name: Get information on the CSR + openssl_csr_info: + path: /etc/ssl/csr/www.ansible.com.csr + register: result + +- name: Dump information + debug: + var: result +''' + +RETURN = r''' +signature_valid: + description: + - Whether the CSR's signature is valid. + - In case the check returns C(no), the module will fail. + returned: success + type: bool +basic_constraints: + description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present. + returned: success + type: list + elements: str + sample: "[CA:TRUE, pathlen:1]" +basic_constraints_critical: + description: Whether the C(basic_constraints) extension is critical. + returned: success + type: bool +extended_key_usage: + description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present. + returned: success + type: list + elements: str + sample: "[Biometric Info, DVCS, Time Stamping]" +extended_key_usage_critical: + description: Whether the C(extended_key_usage) extension is critical. + returned: success + type: bool +extensions_by_oid: + description: Returns a dictionary for every extension OID + returned: success + type: dict + contains: + critical: + description: Whether the extension is critical. + returned: success + type: bool + value: + description: The Base64 encoded value (in DER format) of the extension + returned: success + type: str + sample: "MAMCAQU=" + sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}' +key_usage: + description: Entries in the C(key_usage) extension, or C(none) if extension is not present. + returned: success + type: str + sample: "[Key Agreement, Data Encipherment]" +key_usage_critical: + description: Whether the C(key_usage) extension is critical. + returned: success + type: bool +subject_alt_name: + description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present. + returned: success + type: list + elements: str + sample: "[DNS:www.ansible.com, IP:1.2.3.4]" +subject_alt_name_critical: + description: Whether the C(subject_alt_name) extension is critical. + returned: success + type: bool +ocsp_must_staple: + description: C(yes) if the OCSP Must Staple extension is present, C(none) otherwise. + returned: success + type: bool +ocsp_must_staple_critical: + description: Whether the C(ocsp_must_staple) extension is critical. + returned: success + type: bool +subject: + description: + - The CSR's subject as a dictionary. + - Note that for repeated values, only the last one will be returned. + returned: success + type: dict + sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}' +subject_ordered: + description: The CSR's subject as an ordered list of tuples. + returned: success + type: list + elements: list + sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]' + version_added: "2.9" +public_key: + description: CSR's public key in PEM format + returned: success + type: str + sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." +public_key_fingerprints: + description: + - Fingerprints of CSR's public key. + - For every hash algorithm available, the fingerprint is computed. + returned: success + type: dict + sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', + 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." +subject_key_identifier: + description: + - The CSR's subject key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(SubjectKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_key_identifier: + description: + - The CSR's authority key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_cert_issuer: + description: + - The CSR's authority cert issuer as a list of general names. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: list + elements: str + sample: "[DNS:www.ansible.com, IP:1.2.3.4]" + version_added: "2.9" +authority_cert_serial_number: + description: + - The CSR's authority cert serial number. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: int + sample: '12345' + version_added: "2.9" +''' + + +import abc +import binascii +import os +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native, to_text, to_bytes +from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ipaddress as compat_ipaddress + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' +MINIMAL_PYOPENSSL_VERSION = '0.15' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: + # OpenSSL 1.1.0 or newer + OPENSSL_MUST_STAPLE_NAME = b"tlsfeature" + OPENSSL_MUST_STAPLE_VALUE = b"status_request" + else: + # OpenSSL 1.0.x or older + OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24" + OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05" +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.primitives import serialization + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" + + +class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): + def __init__(self, module, backend): + super(CertificateSigningRequestInfo, self).__init__( + module.params['path'] or '', + 'present', + False, + module.check_mode, + ) + self.backend = backend + self.module = module + self.content = module.params['content'] + if self.content is not None: + self.content = self.content.encode('utf-8') + + def generate(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + def dump(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + @abc.abstractmethod + def _get_subject_ordered(self): + pass + + @abc.abstractmethod + def _get_key_usage(self): + pass + + @abc.abstractmethod + def _get_extended_key_usage(self): + pass + + @abc.abstractmethod + def _get_basic_constraints(self): + pass + + @abc.abstractmethod + def _get_ocsp_must_staple(self): + pass + + @abc.abstractmethod + def _get_subject_alt_name(self): + pass + + @abc.abstractmethod + def _get_public_key(self, binary): + pass + + @abc.abstractmethod + def _get_subject_key_identifier(self): + pass + + @abc.abstractmethod + def _get_authority_key_identifier(self): + pass + + @abc.abstractmethod + def _get_all_extensions(self): + pass + + @abc.abstractmethod + def _is_signature_valid(self): + pass + + def get_info(self): + result = dict() + self.csr = crypto_utils.load_certificate_request(self.path, content=self.content, backend=self.backend) + + subject = self._get_subject_ordered() + result['subject'] = dict() + for k, v in subject: + result['subject'][k] = v + result['subject_ordered'] = subject + result['key_usage'], result['key_usage_critical'] = self._get_key_usage() + result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage() + result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints() + result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple() + result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name() + + result['public_key'] = self._get_public_key(binary=False) + pk = self._get_public_key(binary=True) + result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + + if self.backend != 'pyopenssl': + ski = self._get_subject_key_identifier() + if ski is not None: + ski = to_native(binascii.hexlify(ski)) + ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) + result['subject_key_identifier'] = ski + + aki, aci, acsn = self._get_authority_key_identifier() + if aki is not None: + aki = to_native(binascii.hexlify(aki)) + aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) + result['authority_key_identifier'] = aki + result['authority_cert_issuer'] = aci + result['authority_cert_serial_number'] = acsn + + result['extensions_by_oid'] = self._get_all_extensions() + + result['signature_valid'] = self._is_signature_valid() + if not result['signature_valid']: + self.module.fail_json( + msg='CSR signature is invalid!', + **result + ) + return result + + +class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo): + """Validate the supplied CSR, using the cryptography backend""" + def __init__(self, module): + super(CertificateSigningRequestInfoCryptography, self).__init__(module, 'cryptography') + + def _get_subject_ordered(self): + result = [] + for attribute in self.csr.subject: + result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + return result + + def _get_key_usage(self): + try: + current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage) + current_key_usage = current_key_ext.value + key_usage = dict( + digital_signature=current_key_usage.digital_signature, + content_commitment=current_key_usage.content_commitment, + key_encipherment=current_key_usage.key_encipherment, + data_encipherment=current_key_usage.data_encipherment, + key_agreement=current_key_usage.key_agreement, + key_cert_sign=current_key_usage.key_cert_sign, + crl_sign=current_key_usage.crl_sign, + encipher_only=False, + decipher_only=False, + ) + if key_usage['key_agreement']: + key_usage.update(dict( + encipher_only=current_key_usage.encipher_only, + decipher_only=current_key_usage.decipher_only + )) + + key_usage_names = dict( + digital_signature='Digital Signature', + content_commitment='Non Repudiation', + key_encipherment='Key Encipherment', + data_encipherment='Data Encipherment', + key_agreement='Key Agreement', + key_cert_sign='Certificate Sign', + crl_sign='CRL Sign', + encipher_only='Encipher Only', + decipher_only='Decipher Only', + ) + return sorted([ + key_usage_names[name] for name, value in key_usage.items() if value + ]), current_key_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_extended_key_usage(self): + try: + ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage) + return sorted([ + crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value + ]), ext_keyusage_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_basic_constraints(self): + try: + ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints) + result = [] + result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')) + if ext_keyusage_ext.value.path_length is not None: + result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length)) + return sorted(result), ext_keyusage_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_ocsp_must_staple(self): + try: + try: + # This only works with cryptography >= 2.1 + tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature) + value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value + except AttributeError as dummy: + # Fallback for cryptography < 2.1 + oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") + tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid) + value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05" + return value, tlsfeature_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_subject_alt_name(self): + try: + san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName) + result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value] + return result, san_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_public_key(self, binary): + return self.csr.public_key().public_bytes( + serialization.Encoding.DER if binary else serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + + def _get_subject_key_identifier(self): + try: + ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + return ext.value.digest + except cryptography.x509.ExtensionNotFound: + return None + + def _get_authority_key_identifier(self): + try: + ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + issuer = None + if ext.value.authority_cert_issuer is not None: + issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] + return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number + except cryptography.x509.ExtensionNotFound: + return None, None, None + + def _get_all_extensions(self): + return crypto_utils.cryptography_get_extensions_from_csr(self.csr) + + def _is_signature_valid(self): + return self.csr.is_signature_valid + + +class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo): + """validate the supplied CSR.""" + + def __init__(self, module): + super(CertificateSigningRequestInfoPyOpenSSL, self).__init__(module, 'pyopenssl') + + def __get_name(self, name): + result = [] + for sub in name.get_components(): + result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) + return result + + def _get_subject_ordered(self): + return self.__get_name(self.csr.get_subject()) + + def _get_extension(self, short_name): + for extension in self.csr.get_extensions(): + if extension.get_short_name() == short_name: + result = [ + crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') + ] + return sorted(result), bool(extension.get_critical()) + return None, False + + def _get_key_usage(self): + return self._get_extension(b'keyUsage') + + def _get_extended_key_usage(self): + return self._get_extension(b'extendedKeyUsage') + + def _get_basic_constraints(self): + return self._get_extension(b'basicConstraints') + + def _get_ocsp_must_staple(self): + extensions = self.csr.get_extensions() + oms_ext = [ + ext for ext in extensions + if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE + ] + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000: + # Older versions of libssl don't know about OCSP Must Staple + oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05']) + if oms_ext: + return True, bool(oms_ext[0].get_critical()) + else: + return None, False + + def _normalize_san(self, san): + # apparently openssl returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string + # although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004) + if san.startswith('IP Address:'): + san = 'IP:' + san[len('IP Address:'):] + if san.startswith('IP:'): + ip = compat_ipaddress.ip_address(san[3:]) + san = 'IP:{0}'.format(ip.compressed) + return san + + def _get_subject_alt_name(self): + for extension in self.csr.get_extensions(): + if extension.get_short_name() == b'subjectAltName': + result = [self._normalize_san(altname.strip()) for altname in + to_text(extension, errors='surrogate_or_strict').split(', ')] + return result, bool(extension.get_critical()) + return None, False + + def _get_public_key(self, binary): + try: + return crypto.dump_publickey( + crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM, + self.csr.get_pubkey() + ) + except AttributeError: + try: + bio = crypto._new_mem_buf() + if binary: + rc = crypto._lib.i2d_PUBKEY_bio(bio, self.csr.get_pubkey()._pkey) + else: + rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.csr.get_pubkey()._pkey) + if rc != 1: + crypto._raise_current_error() + return crypto._bio_to_string(bio) + except AttributeError: + self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' + 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') + + def _get_subject_key_identifier(self): + # Won't be implemented + return None + + def _get_authority_key_identifier(self): + # Won't be implemented + return None, None, None + + def _get_all_extensions(self): + return crypto_utils.pyopenssl_get_extensions_from_csr(self.csr) + + def _is_signature_valid(self): + try: + return bool(self.csr.verify(self.csr.get_pubkey())) + except crypto.Error: + # OpenSSL error means that key is not consistent + return False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str'), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + try: + if module.params['path'] is not None: + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detect what backend we can use + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # If cryptography is available we'll use it + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Fail if no backend has been found + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + try: + getattr(crypto.X509Req, 'get_extensions') + except AttributeError: + module.fail_json(msg='You need to have PyOpenSSL>=0.15') + + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + certificate = CertificateSigningRequestInfoPyOpenSSL(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + certificate = CertificateSigningRequestInfoCryptography(module) + + result = certificate.get_info() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/openssl_dhparam.py b/plugins/modules/openssl_dhparam.py new file mode 100644 index 00000000..5dbd395f --- /dev/null +++ b/plugins/modules/openssl_dhparam.py @@ -0,0 +1,414 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Thom Wiggers +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_dhparam +short_description: Generate OpenSSL Diffie-Hellman Parameters +description: + - This module allows one to (re)generate OpenSSL DH-params. + - This module uses file common arguments to specify generated file permissions. + - "Please note that the module regenerates existing DH params if they don't + match the module's options. If you are concerned that this could overwrite + your existing DH params, consider using the I(backup) option." + - The module can use the cryptography Python library, or the C(openssl) executable. + By default, it tries to detect which one is available. This can be overridden + with the I(select_crypto_backend) option. +requirements: + - Either cryptography >= 2.0 + - Or OpenSSL binary C(openssl) +author: + - Thom Wiggers (@thomwiggers) +options: + state: + description: + - Whether the parameters should exist or not, + taking action if the state is different from what is stated. + type: str + default: present + choices: [ absent, present ] + size: + description: + - Size (in bits) of the generated DH-params. + type: int + default: 4096 + force: + description: + - Should the parameters be regenerated even it it already exists. + type: bool + default: no + path: + description: + - Name of the file in which the generated parameters will be saved. + type: path + required: true + backup: + description: + - Create a backup file including a timestamp so you can get the original + DH params back if you overwrote them with new ones by accident. + type: bool + default: no + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(openssl). + - If set to C(openssl), will try to use the OpenSSL C(openssl) executable. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography, openssl ] + return_content: + description: + - If set to C(yes), will return the (current or generated) DH params' content as I(dhparams). + type: bool + default: no +extends_documentation_fragment: +- files +seealso: +- module: openssl_certificate +- module: openssl_csr +- module: openssl_pkcs12 +- module: openssl_privatekey +- module: openssl_publickey +''' + +EXAMPLES = r''' +- name: Generate Diffie-Hellman parameters with the default size (4096 bits) + openssl_dhparam: + path: /etc/ssl/dhparams.pem + +- name: Generate DH Parameters with a different size (2048 bits) + openssl_dhparam: + path: /etc/ssl/dhparams.pem + size: 2048 + +- name: Force regenerate an DH parameters if they already exist + openssl_dhparam: + path: /etc/ssl/dhparams.pem + force: yes +''' + +RETURN = r''' +size: + description: Size (in bits) of the Diffie-Hellman parameters. + returned: changed or success + type: int + sample: 4096 +filename: + description: Path to the generated Diffie-Hellman parameters. + returned: changed or success + type: str + sample: /etc/ssl/dhparams.pem +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/dhparams.pem.2019-03-09@11:22~ +dhparams: + description: The (current or generated) DH params' content. + returned: if I(state) is C(present) and I(return_content) is C(yes) + type: str + version_added: "2.10" +''' + +import abc +import os +import re +import tempfile +import traceback +from distutils.version import LooseVersion + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils + + +MINIMAL_CRYPTOGRAPHY_VERSION = '2.0' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.exceptions + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.asymmetric.dh + import cryptography.hazmat.primitives.serialization + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +class DHParameterError(Exception): + pass + + +class DHParameterBase(object): + + def __init__(self, module): + self.state = module.params['state'] + self.path = module.params['path'] + self.size = module.params['size'] + self.force = module.params['force'] + self.changed = False + self.return_content = module.params['return_content'] + + self.backup = module.params['backup'] + self.backup_file = None + + @abc.abstractmethod + def _do_generate(self, module): + """Actually generate the DH params.""" + pass + + def generate(self, module): + """Generate DH params.""" + changed = False + + # ony generate when necessary + if self.force or not self._check_params_valid(module): + self._do_generate(module) + changed = True + + # fix permissions (checking force not necessary as done above) + if not self._check_fs_attributes(module): + # Fix done implicitly by + # AnsibleModule.set_fs_attributes_if_different + changed = True + + self.changed = changed + + def remove(self, module): + if self.backup: + self.backup_file = module.backup_local(self.path) + try: + os.remove(self.path) + self.changed = True + except OSError as exc: + module.fail_json(msg=to_native(exc)) + + def check(self, module): + """Ensure the resource is in its desired state.""" + if self.force: + return False + return self._check_params_valid(module) and self._check_fs_attributes(module) + + @abc.abstractmethod + def _check_params_valid(self, module): + """Check if the params are in the correct state""" + pass + + def _check_fs_attributes(self, module): + """Checks (and changes if not in check mode!) fs attributes""" + file_args = module.load_file_common_arguments(module.params) + attrs_changed = module.set_fs_attributes_if_different(file_args, False) + + return not attrs_changed + + def dump(self): + """Serialize the object into a dictionary.""" + + result = { + 'size': self.size, + 'filename': self.path, + 'changed': self.changed, + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['dhparams'] = content.decode('utf-8') if content else None + + return result + + +class DHParameterAbsent(DHParameterBase): + + def __init__(self, module): + super(DHParameterAbsent, self).__init__(module) + + def _do_generate(self, module): + """Actually generate the DH params.""" + pass + + def _check_params_valid(self, module): + """Check if the params are in the correct state""" + pass + + +class DHParameterOpenSSL(DHParameterBase): + + def __init__(self, module): + super(DHParameterOpenSSL, self).__init__(module) + self.openssl_bin = module.get_bin_path('openssl', True) + + def _do_generate(self, module): + """Actually generate the DH params.""" + # create a tempfile + fd, tmpsrc = tempfile.mkstemp() + os.close(fd) + module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit + # openssl dhparam -out + command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)] + rc, dummy, err = module.run_command(command, check_rc=False) + if rc != 0: + raise DHParameterError(to_native(err)) + if self.backup: + self.backup_file = module.backup_local(self.path) + try: + module.atomic_move(tmpsrc, self.path) + except Exception as e: + module.fail_json(msg="Failed to write to file %s: %s" % (self.path, str(e))) + + def _check_params_valid(self, module): + """Check if the params are in the correct state""" + command = [self.openssl_bin, 'dhparam', '-check', '-text', '-noout', '-in', self.path] + rc, out, err = module.run_command(command, check_rc=False) + result = to_native(out) + if rc != 0: + # If the call failed the file probably doesn't exist or is + # unreadable + return False + # output contains "(xxxx bit)" + match = re.search(r"Parameters:\s+\((\d+) bit\).*", result) + if not match: + return False # No "xxxx bit" in output + + bits = int(match.group(1)) + + # if output contains "WARNING" we've got a problem + if "WARNING" in result or "WARNING" in to_native(err): + return False + + return bits == self.size + + +class DHParameterCryptography(DHParameterBase): + + def __init__(self, module): + super(DHParameterCryptography, self).__init__(module) + self.crypto_backend = cryptography.hazmat.backends.default_backend() + + def _do_generate(self, module): + """Actually generate the DH params.""" + # Generate parameters + params = cryptography.hazmat.primitives.asymmetric.dh.generate_parameters( + generator=2, + key_size=self.size, + backend=self.crypto_backend, + ) + # Serialize parameters + result = params.parameter_bytes( + encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM, + format=cryptography.hazmat.primitives.serialization.ParameterFormat.PKCS3, + ) + # Write result + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, result) + + def _check_params_valid(self, module): + """Check if the params are in the correct state""" + # Load parameters + try: + with open(self.path, 'rb') as f: + data = f.read() + params = self.crypto_backend.load_pem_parameters(data) + except Exception as dummy: + return False + # Check parameters + bits = crypto_utils.count_bits(params.parameter_numbers().p) + return bits == self.size + + +def main(): + """Main function""" + + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['absent', 'present']), + size=dict(type='int', default=4096), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + backup=dict(type='bool', default=False), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'openssl']), + return_content=dict(type='bool', default=False), + ), + supports_check_mode=True, + add_file_common_args=True, + ) + + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg="The directory '%s' does not exist or the file is not a directory" % base_dir + ) + + if module.params['state'] == 'present': + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_openssl = module.get_bin_path('openssl', False) is not None + + # First try cryptography, then OpenSSL + if can_use_cryptography: + backend = 'cryptography' + elif can_use_openssl: + backend = 'openssl' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Can't detect either the required Python library cryptography (>= {0}) " + "or the OpenSSL binary openssl").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if backend == 'openssl': + dhparam = DHParameterOpenSSL(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + dhparam = DHParameterCryptography(module) + + if module.check_mode: + result = dhparam.dump() + result['changed'] = module.params['force'] or not dhparam.check(module) + module.exit_json(**result) + + try: + dhparam.generate(module) + except DHParameterError as exc: + module.fail_json(msg=to_native(exc)) + else: + dhparam = DHParameterAbsent(module) + + if module.check_mode: + result = dhparam.dump() + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + if os.path.exists(module.params['path']): + try: + dhparam.remove(module) + except Exception as exc: + module.fail_json(msg=to_native(exc)) + + result = dhparam.dump() + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/openssl_pkcs12.py b/plugins/modules/openssl_pkcs12.py new file mode 100644 index 00000000..97512b93 --- /dev/null +++ b/plugins/modules/openssl_pkcs12.py @@ -0,0 +1,467 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Guillaume Delpierre +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_pkcs12 +author: +- Guillaume Delpierre (@gdelpierre) +short_description: Generate OpenSSL PKCS#12 archive +description: + - This module allows one to (re-)generate PKCS#12. +requirements: + - python-pyOpenSSL +options: + action: + description: + - C(export) or C(parse) a PKCS#12. + type: str + default: export + choices: [ export, parse ] + other_certificates: + description: + - List of other certificates to include. Pre 2.8 this parameter was called C(ca_certificates) + type: list + elements: path + aliases: [ ca_certificates ] + certificate_path: + description: + - The path to read certificates and private keys from. + - Must be in PEM format. + type: path + force: + description: + - Should the file be regenerated even if it already exists. + type: bool + default: no + friendly_name: + description: + - Specifies the friendly name for the certificate and private key. + type: str + aliases: [ name ] + iter_size: + description: + - Number of times to repeat the encryption step. + type: int + default: 2048 + maciter_size: + description: + - Number of times to repeat the MAC step. + type: int + default: 1 + passphrase: + description: + - The PKCS#12 password. + type: str + path: + description: + - Filename to write the PKCS#12 file to. + type: path + required: true + privatekey_passphrase: + description: + - Passphrase source to decrypt any input private keys with. + type: str + privatekey_path: + description: + - File to read private key from. + type: path + state: + description: + - Whether the file should exist or not. + All parameters except C(path) are ignored when state is C(absent). + choices: [ absent, present ] + default: present + type: str + src: + description: + - PKCS#12 file path to parse. + type: path + backup: + description: + - Create a backup file including a timestamp so you can get the original + output file back if you overwrote it with a new one by accident. + type: bool + default: no + return_content: + description: + - If set to C(yes), will return the (current or generated) PKCS#12's content as I(pkcs12). + type: bool + default: no +extends_documentation_fragment: + - files +seealso: +- module: openssl_certificate +- module: openssl_csr +- module: openssl_dhparam +- module: openssl_privatekey +- module: openssl_publickey +''' + +EXAMPLES = r''' +- name: Generate PKCS#12 file + openssl_pkcs12: + action: export + path: /opt/certs/ansible.p12 + friendly_name: raclette + privatekey_path: /opt/certs/keys/key.pem + certificate_path: /opt/certs/cert.pem + other_certificates: /opt/certs/ca.pem + state: present + +- name: Change PKCS#12 file permission + openssl_pkcs12: + action: export + path: /opt/certs/ansible.p12 + friendly_name: raclette + privatekey_path: /opt/certs/keys/key.pem + certificate_path: /opt/certs/cert.pem + other_certificates: /opt/certs/ca.pem + state: present + mode: '0600' + +- name: Regen PKCS#12 file + openssl_pkcs12: + action: export + src: /opt/certs/ansible.p12 + path: /opt/certs/ansible.p12 + friendly_name: raclette + privatekey_path: /opt/certs/keys/key.pem + certificate_path: /opt/certs/cert.pem + other_certificates: /opt/certs/ca.pem + state: present + mode: '0600' + force: yes + +- name: Dump/Parse PKCS#12 file + openssl_pkcs12: + action: parse + src: /opt/certs/ansible.p12 + path: /opt/certs/ansible.pem + state: present + +- name: Remove PKCS#12 file + openssl_pkcs12: + path: /opt/certs/ansible.p12 + state: absent +''' + +RETURN = r''' +filename: + description: Path to the generate PKCS#12 file. + returned: changed or success + type: str + sample: /opt/certs/ansible.p12 +privatekey: + description: Path to the TLS/SSL private key the public key was generated from. + returned: changed or success + type: str + sample: /etc/ssl/private/ansible.com.pem +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/ansible.com.pem.2019-03-09@11:22~ +pkcs12: + description: The (current or generated) PKCS#12's content Base64 encoded. + returned: if I(state) is C(present) and I(return_content) is C(yes) + type: str + version_added: "2.10" +''' + +import base64 +import stat +import os +import traceback + +PYOPENSSL_IMP_ERR = None +try: + from OpenSSL import crypto +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + pyopenssl_found = False +else: + pyopenssl_found = True + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils._text import to_bytes, to_native + + +class PkcsError(crypto_utils.OpenSSLObjectError): + pass + + +class Pkcs(crypto_utils.OpenSSLObject): + + def __init__(self, module): + super(Pkcs, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.action = module.params['action'] + self.other_certificates = module.params['other_certificates'] + self.certificate_path = module.params['certificate_path'] + self.friendly_name = module.params['friendly_name'] + self.iter_size = module.params['iter_size'] + self.maciter_size = module.params['maciter_size'] + self.passphrase = module.params['passphrase'] + self.pkcs12 = None + self.privatekey_passphrase = module.params['privatekey_passphrase'] + self.privatekey_path = module.params['privatekey_path'] + self.pkcs12_bytes = None + self.return_content = module.params['return_content'] + self.src = module.params['src'] + + if module.params['mode'] is None: + module.params['mode'] = '0400' + + self.backup = module.params['backup'] + self.backup_file = None + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + state_and_perms = super(Pkcs, self).check(module, perms_required) + + def _check_pkey_passphrase(): + if self.privatekey_passphrase: + try: + crypto_utils.load_privatekey(self.privatekey_path, + self.privatekey_passphrase) + except crypto.Error: + return False + except crypto_utils.OpenSSLBadPassphraseError: + return False + return True + + if not state_and_perms: + return state_and_perms + + if os.path.exists(self.path) and module.params['action'] == 'export': + dummy = self.generate(module) + self.src = self.path + try: + pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse() + except crypto.Error: + return False + if (pkcs12_privatekey is not None) and (self.privatekey_path is not None): + expected_pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, + self.pkcs12.get_privatekey()) + if pkcs12_privatekey != expected_pkey: + return False + elif bool(pkcs12_privatekey) != bool(self.privatekey_path): + return False + + if (pkcs12_certificate is not None) and (self.certificate_path is not None): + + expected_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, + self.pkcs12.get_certificate()) + if pkcs12_certificate != expected_cert: + return False + elif bool(pkcs12_certificate) != bool(self.certificate_path): + return False + + if (pkcs12_other_certificates is not None) and (self.other_certificates is not None): + expected_other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM, + other_cert) for other_cert in self.pkcs12.get_ca_certificates()] + if set(pkcs12_other_certificates) != set(expected_other_certs): + return False + elif bool(pkcs12_other_certificates) != bool(self.other_certificates): + return False + + if pkcs12_privatekey: + # This check is required because pyOpenSSL will not return a friendly name + # if the private key is not set in the file + if ((self.pkcs12.get_friendlyname() is not None) and (pkcs12_friendly_name is not None)): + if self.pkcs12.get_friendlyname() != pkcs12_friendly_name: + return False + elif bool(self.pkcs12.get_friendlyname()) != bool(pkcs12_friendly_name): + return False + else: + return False + + return _check_pkey_passphrase() + + def dump(self): + """Serialize the object into a dictionary.""" + + result = { + 'filename': self.path, + } + if self.privatekey_path: + result['privatekey_path'] = self.privatekey_path + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + if self.pkcs12_bytes is None: + self.pkcs12_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['pkcs12'] = base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None + + return result + + def generate(self, module): + """Generate PKCS#12 file archive.""" + self.pkcs12 = crypto.PKCS12() + + if self.other_certificates: + other_certs = [crypto_utils.load_certificate(other_cert) for other_cert + in self.other_certificates] + self.pkcs12.set_ca_certificates(other_certs) + + if self.certificate_path: + self.pkcs12.set_certificate(crypto_utils.load_certificate( + self.certificate_path)) + + if self.friendly_name: + self.pkcs12.set_friendlyname(to_bytes(self.friendly_name)) + + if self.privatekey_path: + try: + self.pkcs12.set_privatekey(crypto_utils.load_privatekey( + self.privatekey_path, + self.privatekey_passphrase) + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise PkcsError(exc) + + return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size) + + def remove(self, module): + if self.backup: + self.backup_file = module.backup_local(self.path) + super(Pkcs, self).remove(module) + + def parse(self): + """Read PKCS#12 file.""" + + try: + with open(self.src, 'rb') as pkcs12_fh: + pkcs12_content = pkcs12_fh.read() + p12 = crypto.load_pkcs12(pkcs12_content, + self.passphrase) + pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, + p12.get_privatekey()) + crt = crypto.dump_certificate(crypto.FILETYPE_PEM, + p12.get_certificate()) + other_certs = [] + if p12.get_ca_certificates() is not None: + other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM, + other_cert) for other_cert in p12.get_ca_certificates()] + + friendly_name = p12.get_friendlyname() + + return (pkey, crt, other_certs, friendly_name) + + except IOError as exc: + raise PkcsError(exc) + + def write(self, module, content, mode=None): + """Write the PKCS#12 file.""" + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, content, mode) + if self.return_content: + self.pkcs12_bytes = content + + +def main(): + argument_spec = dict( + action=dict(type='str', default='export', choices=['export', 'parse']), + other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']), + certificate_path=dict(type='path'), + force=dict(type='bool', default=False), + friendly_name=dict(type='str', aliases=['name']), + iter_size=dict(type='int', default=2048), + maciter_size=dict(type='int', default=1), + passphrase=dict(type='str', no_log=True), + path=dict(type='path', required=True), + privatekey_passphrase=dict(type='str', no_log=True), + privatekey_path=dict(type='path'), + state=dict(type='str', default='present', choices=['absent', 'present']), + src=dict(type='path'), + backup=dict(type='bool', default=False), + return_content=dict(type='bool', default=False), + ) + + required_if = [ + ['action', 'parse', ['src']], + ] + + module = AnsibleModule( + add_file_common_args=True, + argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + if not pyopenssl_found: + module.fail_json(msg=missing_required_lib('pyOpenSSL'), exception=PYOPENSSL_IMP_ERR) + + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg="The directory '%s' does not exist or the path is not a directory" % base_dir + ) + + try: + pkcs12 = Pkcs(module) + changed = False + + if module.params['state'] == 'present': + if module.check_mode: + result = pkcs12.dump() + result['changed'] = module.params['force'] or not pkcs12.check(module) + module.exit_json(**result) + + if not pkcs12.check(module, perms_required=False) or module.params['force']: + if module.params['action'] == 'export': + if not module.params['friendly_name']: + module.fail_json(msg='Friendly_name is required') + pkcs12_content = pkcs12.generate(module) + pkcs12.write(module, pkcs12_content, 0o600) + changed = True + else: + pkey, cert, other_certs, friendly_name = pkcs12.parse() + dump_content = '%s%s%s' % (to_native(pkey), to_native(cert), to_native(b''.join(other_certs))) + pkcs12.write(module, to_bytes(dump_content)) + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, changed): + changed = True + else: + if module.check_mode: + result = pkcs12.dump() + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + if os.path.exists(module.params['path']): + pkcs12.remove(module) + changed = True + + result = pkcs12.dump() + result['changed'] = changed + if os.path.exists(module.params['path']): + file_mode = "%04o" % stat.S_IMODE(os.stat(module.params['path']).st_mode) + result['mode'] = file_mode + + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/openssl_privatekey.py b/plugins/modules/openssl_privatekey.py new file mode 100644 index 00000000..492bbc67 --- /dev/null +++ b/plugins/modules/openssl_privatekey.py @@ -0,0 +1,933 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_privatekey +short_description: Generate OpenSSL private keys +description: + - This module allows one to (re)generate OpenSSL private keys. + - One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29), + L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm), + L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or + L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys. + - Keys are generated in PEM format. + - "Please note that the module regenerates private keys if they don't match + the module's options. In particular, if you provide another passphrase + (or specify none), change the keysize, etc., the private key will be + regenerated. If you are concerned that this could **overwrite your private key**, + consider using the I(backup) option." + - The module can use the cryptography Python library, or the pyOpenSSL Python + library. By default, it tries to detect which one is available. This can be + overridden with the I(select_crypto_backend) option. Please note that the + PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." +requirements: + - Either cryptography >= 1.2.3 (older versions might work as well) + - Or pyOpenSSL +author: + - Yanis Guenane (@Spredzy) + - Felix Fontein (@felixfontein) +options: + state: + description: + - Whether the private key should exist or not, taking action if the state is different from what is stated. + type: str + default: present + choices: [ absent, present ] + size: + description: + - Size (in bits) of the TLS/SSL key to generate. + type: int + default: 4096 + type: + description: + - The algorithm used to generate the TLS/SSL private key. + - Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend. + C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require + cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the + I(curve) option. + type: str + default: RSA + choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ] + curve: + description: + - Note that not all curves are supported by all versions of C(cryptography). + - For maximal interoperability, C(secp384r1) or C(secp256r1) should be used. + - We use the curve names as defined in the + L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8). + type: str + choices: + - secp384r1 + - secp521r1 + - secp224r1 + - secp192r1 + - secp256r1 + - secp256k1 + - brainpoolP256r1 + - brainpoolP384r1 + - brainpoolP512r1 + - sect571k1 + - sect409k1 + - sect283k1 + - sect233k1 + - sect163k1 + - sect571r1 + - sect409r1 + - sect283r1 + - sect233r1 + - sect163r2 + force: + description: + - Should the key be regenerated even if it already exists. + type: bool + default: no + path: + description: + - Name of the file in which the generated TLS/SSL private key will be written. It will have 0600 mode. + type: path + required: true + passphrase: + description: + - The passphrase for the private key. + type: str + cipher: + description: + - The cipher to encrypt the private key. (Valid values can be found by + running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`, + depending on your OpenSSL version.) + - When using the C(cryptography) backend, use C(auto). + type: str + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. + From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + format: + description: + - Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format) + is used for all keys which support it. Please note that not every key can be exported in any format. + - The value C(auto) selects a fromat based on the key format. The value C(auto_ignore) does the same, + but for existing private key files, it will not force a regenerate when its format is not the automatically + selected one for generation. + - Note that if the format for an existing private key mismatches, the key is *regenerated* by default. + To change this behavior, use the I(format_mismatch) option. + - The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will + fail if a value different from C(auto_ignore) is used. + type: str + default: auto_ignore + choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ] + format_mismatch: + description: + - Determines behavior of the module if the format of a private key does not match the expected format, but all + other parameters are as expected. + - If set to C(regenerate) (default), generates a new private key. + - If set to C(convert), the key will be converted to the new format instead. + - Only supported by the C(cryptography) backend. + type: str + default: regenerate + choices: [ regenerate, convert ] + backup: + description: + - Create a backup file including a timestamp so you can get + the original private key back if you overwrote it with a new one by accident. + type: bool + default: no + return_content: + description: + - If set to C(yes), will return the (current or generated) private key's content as I(privatekey). + - Note that especially if the private key is not encrypted, you have to make sure that the returned + value is treated appropriately and not accidentally written to logs etc.! Use with care! + type: bool + default: no + regenerate: + description: + - Allows to configure in which situations the module is allowed to regenerate private keys. + The module will always generate a new key if the destination file does not exist. + - By default, the key will be regenerated when it doesn't match the module's options, + except when the key cannot be read or the passphrase does not match. Please note that + this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence) + is specified. + - If set to C(never), the module will fail if the key cannot be read or the passphrase + isn't matching, and will never regenerate an existing key. + - If set to C(fail), the module will fail if the key does not correspond to the module's + options. + - If set to C(partial_idempotence), the key will be regenerated if it does not conform to + the module's options. The key is B(not) regenerated if it cannot be read (broken file), + the key is protected by an unknown passphrase, or when they key is not protected by a + passphrase, but a passphrase is specified. + - If set to C(full_idempotence), the key will be regenerated if it does not conform to the + module's options. This is also the case if the key cannot be read (broken file), the key + is protected by an unknown passphrase, or when they key is not protected by a passphrase, + but a passphrase is specified. Make sure you have a B(backup) when using this option! + - If set to C(always), the module will always regenerate the key. This is equivalent to + setting I(force) to C(yes). + - Note that if I(format_mismatch) is set to C(convert) and everything matches except the + format, the key will always be converted, except if I(regenerate) is set to C(always). + type: str + choices: + - never + - fail + - partial_idempotence + - full_idempotence + - always + default: full_idempotence +extends_documentation_fragment: +- files +seealso: +- module: openssl_certificate +- module: openssl_csr +- module: openssl_dhparam +- module: openssl_pkcs12 +- module: openssl_publickey +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) + openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + +- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) and a passphrase + openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + passphrase: ansible + cipher: aes256 + +- name: Generate an OpenSSL private key with a different size (2048 bits) + openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + size: 2048 + +- name: Force regenerate an OpenSSL private key if it already exists + openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + force: yes + +- name: Generate an OpenSSL private key with a different algorithm (DSA) + openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + type: DSA +''' + +RETURN = r''' +size: + description: Size (in bits) of the TLS/SSL private key. + returned: changed or success + type: int + sample: 4096 +type: + description: Algorithm used to generate the TLS/SSL private key. + returned: changed or success + type: str + sample: RSA +curve: + description: Elliptic curve used to generate the TLS/SSL private key. + returned: changed or success, and I(type) is C(ECC) + type: str + sample: secp256r1 +filename: + description: Path to the generated TLS/SSL private key file. + returned: changed or success + type: str + sample: /etc/ssl/private/ansible.com.pem +fingerprint: + description: + - The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available. + - The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output. + returned: changed or success + type: dict + sample: + md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29" + sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10" + sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46" + sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7" + sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d" + sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b" +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/privatekey.pem.2019-03-09@11:22~ +privatekey: + description: + - The (current or generated) private key's content. + - Will be Base64-encoded if the key is in raw format. + returned: if I(state) is C(present) and I(return_content) is C(yes) + type: str + version_added: "2.10" +''' + +import abc +import base64 +import os +import traceback +from distutils.version import LooseVersion + +MINIMAL_PYOPENSSL_VERSION = '0.6' +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.exceptions + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization + import cryptography.hazmat.primitives.asymmetric.rsa + import cryptography.hazmat.primitives.asymmetric.dsa + import cryptography.hazmat.primitives.asymmetric.ec + import cryptography.hazmat.primitives.asymmetric.utils + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +from ansible_collections.community.crypto.plugins.module_utils.crypto import ( + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, +) + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils._text import to_native, to_bytes +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + + +class PrivateKeyError(crypto_utils.OpenSSLObjectError): + pass + + +class PrivateKeyBase(crypto_utils.OpenSSLObject): + + def __init__(self, module): + super(PrivateKeyBase, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.size = module.params['size'] + self.passphrase = module.params['passphrase'] + self.cipher = module.params['cipher'] + self.privatekey = None + self.fingerprint = {} + self.format = module.params['format'] + self.format_mismatch = module.params['format_mismatch'] + self.privatekey_bytes = None + self.return_content = module.params['return_content'] + self.regenerate = module.params['regenerate'] + if self.regenerate == 'always': + self.force = True + + self.backup = module.params['backup'] + self.backup_file = None + + if module.params['mode'] is None: + module.params['mode'] = '0600' + + @abc.abstractmethod + def _generate_private_key(self): + """(Re-)Generate private key.""" + pass + + @abc.abstractmethod + def _ensure_private_key_loaded(self): + """Make sure that the private key has been loaded.""" + pass + + @abc.abstractmethod + def _get_private_key_data(self): + """Return bytes for self.privatekey""" + pass + + @abc.abstractmethod + def _get_fingerprint(self): + pass + + def generate(self, module): + """Generate a keypair.""" + + if not self.check(module, perms_required=False, ignore_conversion=True) or self.force: + # Regenerate + if self.backup: + self.backup_file = module.backup_local(self.path) + self._generate_private_key() + privatekey_data = self._get_private_key_data() + if self.return_content: + self.privatekey_bytes = privatekey_data + crypto_utils.write_file(module, privatekey_data, 0o600) + self.changed = True + elif not self.check(module, perms_required=False, ignore_conversion=False): + # Convert + if self.backup: + self.backup_file = module.backup_local(self.path) + self._ensure_private_key_loaded() + privatekey_data = self._get_private_key_data() + if self.return_content: + self.privatekey_bytes = privatekey_data + crypto_utils.write_file(module, privatekey_data, 0o600) + self.changed = True + + self.fingerprint = self._get_fingerprint() + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def remove(self, module): + if self.backup: + self.backup_file = module.backup_local(self.path) + super(PrivateKeyBase, self).remove(module) + + @abc.abstractmethod + def _check_passphrase(self): + pass + + @abc.abstractmethod + def _check_size_and_type(self): + pass + + @abc.abstractmethod + def _check_format(self): + pass + + def check(self, module, perms_required=True, ignore_conversion=True): + """Ensure the resource is in its desired state.""" + + state_and_perms = super(PrivateKeyBase, self).check(module, perms_required=False) + + if not state_and_perms: + # key does not exist + return False + + if not self._check_passphrase(): + if self.regenerate in ('full_idempotence', 'always'): + return False + module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `full_idempotence` or `always`, or with `force=yes`.') + + if self.regenerate != 'never': + if not self._check_size_and_type(): + if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): + return False + module.fail_json(msg='Key has wrong type and/or size.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.') + + if not self._check_format(): + # During conversion step, convert if format does not match and format_mismatch == 'convert' + if not ignore_conversion and self.format_mismatch == 'convert': + return False + # During generation step, regenerate if format does not match and format_mismatch == 'regenerate' + if ignore_conversion and self.format_mismatch == 'regenerate' and self.regenerate != 'never': + if not ignore_conversion or self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): + return False + module.fail_json(msg='Key has wrong format.' + ' Will not proceed. To force regeneration, call the module with `generate`' + ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.' + ' To convert the key, set `format_mismatch` to `convert`.') + + # check whether permissions are correct (in case that needs to be checked) + return not perms_required or super(PrivateKeyBase, self).check(module, perms_required=perms_required) + + def dump(self): + """Serialize the object into a dictionary.""" + + result = { + 'size': self.size, + 'filename': self.path, + 'changed': self.changed, + 'fingerprint': self.fingerprint, + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + if self.privatekey_bytes is None: + self.privatekey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + if self.privatekey_bytes: + if crypto_utils.identify_private_key_format(self.privatekey_bytes) == 'raw': + result['privatekey'] = base64.b64encode(self.privatekey_bytes) + else: + result['privatekey'] = self.privatekey_bytes.decode('utf-8') + else: + result['privatekey'] = None + + return result + + +# Implementation with using pyOpenSSL +class PrivateKeyPyOpenSSL(PrivateKeyBase): + + def __init__(self, module): + super(PrivateKeyPyOpenSSL, self).__init__(module) + + if module.params['type'] == 'RSA': + self.type = crypto.TYPE_RSA + elif module.params['type'] == 'DSA': + self.type = crypto.TYPE_DSA + else: + module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.") + + if self.format != 'auto_ignore': + module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.") + + def _generate_private_key(self): + """(Re-)Generate private key.""" + self.privatekey = crypto.PKey() + try: + self.privatekey.generate_key(self.type, self.size) + except (TypeError, ValueError) as exc: + raise PrivateKeyError(exc) + + def _ensure_private_key_loaded(self): + """Make sure that the private key has been loaded.""" + if self.privatekey is None: + try: + self.privatekey = privatekey = crypto_utils.load_privatekey(self.path, self.passphrase) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise PrivateKeyError(exc) + + def _get_private_key_data(self): + """Return bytes for self.privatekey""" + if self.cipher and self.passphrase: + return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey, + self.cipher, to_bytes(self.passphrase)) + else: + return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey) + + def _get_fingerprint(self): + return crypto_utils.get_fingerprint(self.path, self.passphrase) + + def _check_passphrase(self): + try: + crypto_utils.load_privatekey(self.path, self.passphrase) + return True + except Exception as dummy: + return False + + def _check_size_and_type(self): + def _check_size(privatekey): + return self.size == privatekey.bits() + + def _check_type(privatekey): + return self.type == privatekey.type() + + self._ensure_private_key_loaded() + return _check_size(self.privatekey) and _check_type(self.privatekey) + + def _check_format(self): + # Not supported by this backend + return True + + def dump(self): + """Serialize the object into a dictionary.""" + + result = super(PrivateKeyPyOpenSSL, self).dump() + + if self.type == crypto.TYPE_RSA: + result['type'] = 'RSA' + else: + result['type'] = 'DSA' + + return result + + +# Implementation with using cryptography +class PrivateKeyCryptography(PrivateKeyBase): + + def _get_ec_class(self, ectype): + ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype) + if ecclass is None: + self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype)) + return ecclass + + def _add_curve(self, name, ectype, deprecated=False): + def create(size): + ecclass = self._get_ec_class(ectype) + return ecclass() + + def verify(privatekey): + ecclass = self._get_ec_class(ectype) + return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass) + + self.curves[name] = { + 'create': create, + 'verify': verify, + 'deprecated': deprecated, + } + + def __init__(self, module): + super(PrivateKeyCryptography, self).__init__(module) + + self.curves = dict() + self._add_curve('secp384r1', 'SECP384R1') + self._add_curve('secp521r1', 'SECP521R1') + self._add_curve('secp224r1', 'SECP224R1') + self._add_curve('secp192r1', 'SECP192R1') + self._add_curve('secp256r1', 'SECP256R1') + self._add_curve('secp256k1', 'SECP256K1') + self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True) + self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True) + self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True) + self._add_curve('sect571k1', 'SECT571K1', deprecated=True) + self._add_curve('sect409k1', 'SECT409K1', deprecated=True) + self._add_curve('sect283k1', 'SECT283K1', deprecated=True) + self._add_curve('sect233k1', 'SECT233K1', deprecated=True) + self._add_curve('sect163k1', 'SECT163K1', deprecated=True) + self._add_curve('sect571r1', 'SECT571R1', deprecated=True) + self._add_curve('sect409r1', 'SECT409R1', deprecated=True) + self._add_curve('sect283r1', 'SECT283R1', deprecated=True) + self._add_curve('sect233r1', 'SECT233R1', deprecated=True) + self._add_curve('sect163r2', 'SECT163R2', deprecated=True) + + self.module = module + self.cryptography_backend = cryptography.hazmat.backends.default_backend() + + self.type = module.params['type'] + self.curve = module.params['curve'] + if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519': + self.module.fail_json(msg='Your cryptography version does not support X25519') + if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': + self.module.fail_json(msg='Your cryptography version does not support X25519 serialization') + if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': + self.module.fail_json(msg='Your cryptography version does not support X448') + if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': + self.module.fail_json(msg='Your cryptography version does not support Ed25519') + if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': + self.module.fail_json(msg='Your cryptography version does not support Ed448') + + def _get_wanted_format(self): + if self.format not in ('auto', 'auto_ignore'): + return self.format + if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'): + return 'pkcs8' + else: + return 'pkcs1' + + def _generate_private_key(self): + """(Re-)Generate private key.""" + try: + if self.type == 'RSA': + self.privatekey = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key( + public_exponent=65537, # OpenSSL always uses this + key_size=self.size, + backend=self.cryptography_backend + ) + if self.type == 'DSA': + self.privatekey = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key( + key_size=self.size, + backend=self.cryptography_backend + ) + if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': + self.privatekey = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate() + if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': + self.privatekey = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate() + if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': + self.privatekey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate() + if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': + self.privatekey = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate() + if self.type == 'ECC' and self.curve in self.curves: + if self.curves[self.curve]['deprecated']: + self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve)) + self.privatekey = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key( + curve=self.curves[self.curve]['create'](self.size), + backend=self.cryptography_backend + ) + except cryptography.exceptions.UnsupportedAlgorithm as dummy: + self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type)) + + def _ensure_private_key_loaded(self): + """Make sure that the private key has been loaded.""" + if self.privatekey is None: + self.privatekey = self._load_privatekey() + + def _get_private_key_data(self): + """Return bytes for self.privatekey""" + # Select export format and encoding + try: + export_format = self._get_wanted_format() + export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM + if export_format == 'pkcs1': + # "TraditionalOpenSSL" format is PKCS1 + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL + elif export_format == 'pkcs8': + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 + elif export_format == 'raw': + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw + export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw + except AttributeError: + self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format)) + + # Select key encryption + encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() + if self.cipher and self.passphrase: + if self.cipher == 'auto': + encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase)) + else: + self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.') + + # Serialize key + try: + return self.privatekey.private_bytes( + encoding=export_encoding, + format=export_format, + encryption_algorithm=encryption_algorithm + ) + except ValueError as dummy: + self.module.fail_json( + msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format) + ) + except Exception as dummy: + self.module.fail_json( + msg='Error while serializing the private key in the required format "{0}"'.format(self.format), + exception=traceback.format_exc() + ) + + def _load_privatekey(self): + try: + # Read bytes + with open(self.path, 'rb') as f: + data = f.read() + # Interpret bytes depending on format. + format = crypto_utils.identify_private_key_format(data) + if format == 'raw': + if len(data) == 56 and CRYPTOGRAPHY_HAS_X448: + return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data) + if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448: + return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data) + if len(data) == 32: + if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519): + return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519): + return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519: + try: + return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + except Exception: + return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + raise PrivateKeyError('Cannot load raw key') + else: + return cryptography.hazmat.primitives.serialization.load_pem_private_key( + data, + None if self.passphrase is None else to_bytes(self.passphrase), + backend=self.cryptography_backend + ) + except Exception as e: + raise PrivateKeyError(e) + + def _get_fingerprint(self): + # Get bytes of public key + private_key = self._load_privatekey() + public_key = private_key.public_key() + public_key_bytes = public_key.public_bytes( + cryptography.hazmat.primitives.serialization.Encoding.DER, + cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo + ) + # Get fingerprints of public_key_bytes + return crypto_utils.get_fingerprint_of_bytes(public_key_bytes) + + def _check_passphrase(self): + try: + with open(self.path, 'rb') as f: + data = f.read() + format = crypto_utils.identify_private_key_format(data) + if format == 'raw': + # Raw keys cannot be encrypted. To avoid incompatibilities, we try to + # actually load the key (and return False when this fails). + self._load_privatekey() + # Loading the key succeeded. Only return True when no passphrase was + # provided. + return self.passphrase is None + else: + return cryptography.hazmat.primitives.serialization.load_pem_private_key( + data, + None if self.passphrase is None else to_bytes(self.passphrase), + backend=self.cryptography_backend + ) + except Exception as dummy: + return False + + def _check_size_and_type(self): + self._ensure_private_key_loaded() + + if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + return self.type == 'RSA' and self.size == self.privatekey.key_size + if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + return self.type == 'DSA' and self.size == self.privatekey.key_size + if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): + return self.type == 'X25519' + if CRYPTOGRAPHY_HAS_X448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): + return self.type == 'X448' + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + return self.type == 'Ed25519' + if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + return self.type == 'Ed448' + if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + if self.type != 'ECC': + return False + if self.curve not in self.curves: + return False + return self.curves[self.curve]['verify'](self.privatekey) + + return False + + def _check_format(self): + if self.format == 'auto_ignore': + return True + try: + with open(self.path, 'rb') as f: + content = f.read() + format = crypto_utils.identify_private_key_format(content) + return format == self._get_wanted_format() + except Exception as dummy: + return False + + def dump(self): + """Serialize the object into a dictionary.""" + result = super(PrivateKeyCryptography, self).dump() + result['type'] = self.type + if self.type == 'ECC': + result['curve'] = self.curve + return result + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + size=dict(type='int', default=4096), + type=dict(type='str', default='RSA', choices=[ + 'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448' + ]), + curve=dict(type='str', choices=[ + 'secp384r1', 'secp521r1', 'secp224r1', 'secp192r1', 'secp256r1', + 'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', + 'sect571k1', 'sect409k1', 'sect283k1', 'sect233k1', 'sect163k1', + 'sect571r1', 'sect409r1', 'sect283r1', 'sect233r1', 'sect163r2', + ]), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + passphrase=dict(type='str', no_log=True), + cipher=dict(type='str'), + backup=dict(type='bool', default=False), + format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']), + format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']), + select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), + return_content=dict(type='bool', default=False), + regenerate=dict( + type='str', + default='full_idempotence', + choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] + ), + ), + supports_check_mode=True, + add_file_common_args=True, + required_together=[ + ['cipher', 'passphrase'] + ], + required_if=[ + ['type', 'ECC', ['curve']], + ], + ) + + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # Decision + if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto': + # First try pyOpenSSL, then cryptography + if can_use_pyopenssl: + backend = 'pyopenssl' + elif can_use_cryptography: + backend = 'cryptography' + else: + # First try cryptography, then pyOpenSSL + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + try: + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + private_key = PrivateKeyPyOpenSSL(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + private_key = PrivateKeyCryptography(module) + + if private_key.state == 'present': + if module.check_mode: + result = private_key.dump() + result['changed'] = private_key.force \ + or not private_key.check(module, ignore_conversion=True) \ + or not private_key.check(module, ignore_conversion=False) + module.exit_json(**result) + + private_key.generate(module) + else: + if module.check_mode: + result = private_key.dump() + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + private_key.remove(module) + + result = private_key.dump() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/openssl_privatekey_info.py b/plugins/modules/openssl_privatekey_info.py new file mode 100644 index 00000000..bc21b2d4 --- /dev/null +++ b/plugins/modules/openssl_privatekey_info.py @@ -0,0 +1,649 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016-2017, Yanis Guenane +# Copyright: (c) 2017, Markus Teufelberger +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_privatekey_info +short_description: Provide information for OpenSSL private keys +description: + - This module allows one to query information on OpenSSL private keys. + - In case the key consistency checks fail, the module will fail as this indicates a faked + private key. In this case, all return variables are still returned. Note that key consistency + checks are not available all key types; if none is available, C(none) is returned for + C(key_is_consistent). + - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the + cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) + cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with + C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 + and will be removed in Ansible 2.13. +requirements: + - PyOpenSSL >= 0.15 or cryptography >= 1.2.3 +author: + - Felix Fontein (@felixfontein) + - Yanis Guenane (@Spredzy) +options: + path: + description: + - Remote absolute path where the private key file is loaded from. + type: path + content: + description: + - Content of the private key file. + - Either I(path) or I(content) must be specified, but not both. + type: str + passphrase: + description: + - The passphrase for the private key. + type: str + return_private_key_data: + description: + - Whether to return private key data. + - Only set this to C(yes) when you want private information about this key to + leave the remote machine. + - "WARNING: you have to make sure that private key data isn't accidentally logged!" + type: bool + default: no + + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. + From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + +seealso: +- module: openssl_privatekey +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) + openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + +- name: Get information on generated key + openssl_privatekey_info: + path: /etc/ssl/private/ansible.com.pem + register: result + +- name: Dump information + debug: + var: result +''' + +RETURN = r''' +can_load_key: + description: Whether the module was able to load the private key from disk + returned: always + type: bool +can_parse_key: + description: Whether the module was able to parse the private key + returned: always + type: bool +key_is_consistent: + description: + - Whether the key is consistent. Can also return C(none) next to C(yes) and + C(no), to indicate that consistency couldn't be checked. + - In case the check returns C(no), the module will fail. + returned: always + type: bool +public_key: + description: Private key's public key in PEM format + returned: success + type: str + sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." +public_key_fingerprints: + description: + - Fingerprints of private key's public key. + - For every hash algorithm available, the fingerprint is computed. + returned: success + type: dict + sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', + 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." +type: + description: + - The key's type. + - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448). + - Will start with C(unknown) if the key type cannot be determined. + returned: success + type: str + sample: RSA +public_data: + description: + - Public key data. Depends on key type. + returned: success + type: dict +private_data: + description: + - Private key data. Depends on key type. + returned: success and when I(return_private_key_data) is set to C(yes) + type: dict +''' + + +import abc +import os +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native, to_bytes + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' +MINIMAL_PYOPENSSL_VERSION = '0.15' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography.hazmat.primitives import serialization + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) + try: + import cryptography.hazmat.primitives.asymmetric.x25519 + CRYPTOGRAPHY_HAS_X25519 = True + except ImportError: + CRYPTOGRAPHY_HAS_X25519 = False + try: + import cryptography.hazmat.primitives.asymmetric.x448 + CRYPTOGRAPHY_HAS_X448 = True + except ImportError: + CRYPTOGRAPHY_HAS_X448 = False + try: + import cryptography.hazmat.primitives.asymmetric.ed25519 + CRYPTOGRAPHY_HAS_ED25519 = True + except ImportError: + CRYPTOGRAPHY_HAS_ED25519 = False + try: + import cryptography.hazmat.primitives.asymmetric.ed448 + CRYPTOGRAPHY_HAS_ED448 = True + except ImportError: + CRYPTOGRAPHY_HAS_ED448 = False +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +SIGNATURE_TEST_DATA = b'1234' + + +def _get_cryptography_key_info(key): + key_public_data = dict() + key_private_data = dict() + if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + key_type = 'RSA' + key_public_data['size'] = key.key_size + key_public_data['modulus'] = key.public_key().public_numbers().n + key_public_data['exponent'] = key.public_key().public_numbers().e + key_private_data['p'] = key.private_numbers().p + key_private_data['q'] = key.private_numbers().q + key_private_data['exponent'] = key.private_numbers().d + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + key_type = 'DSA' + key_public_data['size'] = key.key_size + key_public_data['p'] = key.parameters().parameter_numbers().p + key_public_data['q'] = key.parameters().parameter_numbers().q + key_public_data['g'] = key.parameters().parameter_numbers().g + key_public_data['y'] = key.public_key().public_numbers().y + key_private_data['x'] = key.private_numbers().x + elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): + key_type = 'X25519' + elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): + key_type = 'X448' + elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + key_type = 'Ed25519' + elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + key_type = 'Ed448' + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + key_type = 'ECC' + key_public_data['curve'] = key.public_key().curve.name + key_public_data['x'] = key.public_key().public_numbers().x + key_public_data['y'] = key.public_key().public_numbers().y + key_public_data['exponent_size'] = key.public_key().curve.key_size + key_private_data['multiplier'] = key.private_numbers().private_value + else: + key_type = 'unknown ({0})'.format(type(key)) + return key_type, key_public_data, key_private_data + + +def _check_dsa_consistency(key_public_data, key_private_data): + # Get parameters + p = key_public_data.get('p') + q = key_public_data.get('q') + g = key_public_data.get('g') + y = key_public_data.get('y') + x = key_private_data.get('x') + for v in (p, q, g, y, x): + if v is None: + return None + # Make sure that g is not 0, 1 or -1 in Z/pZ + if g < 2 or g >= p - 1: + return False + # Make sure that x is in range + if x < 1 or x >= q: + return False + # Check whether q divides p-1 + if (p - 1) % q != 0: + return False + # Check that g**q mod p == 1 + if crypto_utils.binary_exp_mod(g, q, p) != 1: + return False + # Check whether g**x mod p == y + if crypto_utils.binary_exp_mod(g, x, p) != y: + return False + # Check (quickly) whether p or q are not primes + if crypto_utils.quick_is_not_prime(q) or crypto_utils.quick_is_not_prime(p): + return False + return True + + +def _is_cryptography_key_consistent(key, key_public_data, key_private_data): + if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + return bool(key._backend._lib.RSA_check_key(key._rsa_cdata)) + if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + result = _check_dsa_consistency(key_public_data, key_private_data) + if result is not None: + return result + try: + signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256()) + except AttributeError: + # sign() was added in cryptography 1.5, but we support older versions + return None + try: + key.public_key().verify( + signature, + SIGNATURE_TEST_DATA, + cryptography.hazmat.primitives.hashes.SHA256() + ) + return True + except cryptography.exceptions.InvalidSignature: + return False + if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + try: + signature = key.sign( + SIGNATURE_TEST_DATA, + cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256()) + ) + except AttributeError: + # sign() was added in cryptography 1.5, but we support older versions + return None + try: + key.public_key().verify( + signature, + SIGNATURE_TEST_DATA, + cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256()) + ) + return True + except cryptography.exceptions.InvalidSignature: + return False + has_simple_sign_function = False + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + has_simple_sign_function = True + if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + has_simple_sign_function = True + if has_simple_sign_function: + signature = key.sign(SIGNATURE_TEST_DATA) + try: + key.public_key().verify(signature, SIGNATURE_TEST_DATA) + return True + except cryptography.exceptions.InvalidSignature: + return False + # For X25519 and X448, there's no test yet. + return None + + +class PrivateKeyInfo(crypto_utils.OpenSSLObject): + def __init__(self, module, backend): + super(PrivateKeyInfo, self).__init__( + module.params['path'] or '', + 'present', + False, + module.check_mode, + ) + self.backend = backend + self.module = module + self.content = module.params['content'] + + self.passphrase = module.params['passphrase'] + self.return_private_key_data = module.params['return_private_key_data'] + + def generate(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + def dump(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + @abc.abstractmethod + def _get_public_key(self, binary): + pass + + @abc.abstractmethod + def _get_key_info(self): + pass + + @abc.abstractmethod + def _is_key_consistent(self, key_public_data, key_private_data): + pass + + def get_info(self): + result = dict( + can_load_key=False, + can_parse_key=False, + key_is_consistent=None, + ) + if self.content is not None: + priv_key_detail = self.content.encode('utf-8') + result['can_load_key'] = True + else: + try: + with open(self.path, 'rb') as b_priv_key_fh: + priv_key_detail = b_priv_key_fh.read() + result['can_load_key'] = True + except (IOError, OSError) as exc: + self.module.fail_json(msg=to_native(exc), **result) + try: + self.key = crypto_utils.load_privatekey( + path=None, + content=priv_key_detail, + passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase, + backend=self.backend + ) + result['can_parse_key'] = True + except crypto_utils.OpenSSLObjectError as exc: + self.module.fail_json(msg=to_native(exc), **result) + + result['public_key'] = self._get_public_key(binary=False) + pk = self._get_public_key(binary=True) + result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + + key_type, key_public_data, key_private_data = self._get_key_info() + result['type'] = key_type + result['public_data'] = key_public_data + if self.return_private_key_data: + result['private_data'] = key_private_data + + result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data) + if result['key_is_consistent'] is False: + # Only fail when it is False, to avoid to fail on None (which means "we don't know") + result['key_is_consistent'] = False + self.module.fail_json( + msg="Private key is not consistent! (See " + "https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)", + **result + ) + return result + + +class PrivateKeyInfoCryptography(PrivateKeyInfo): + """Validate the supplied private key, using the cryptography backend""" + def __init__(self, module): + super(PrivateKeyInfoCryptography, self).__init__(module, 'cryptography') + + def _get_public_key(self, binary): + return self.key.public_key().public_bytes( + serialization.Encoding.DER if binary else serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + + def _get_key_info(self): + return _get_cryptography_key_info(self.key) + + def _is_key_consistent(self, key_public_data, key_private_data): + return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data) + + +class PrivateKeyInfoPyOpenSSL(PrivateKeyInfo): + """validate the supplied private key.""" + + def __init__(self, module): + super(PrivateKeyInfoPyOpenSSL, self).__init__(module, 'pyopenssl') + + def _get_public_key(self, binary): + try: + return crypto.dump_publickey( + crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM, + self.key + ) + except AttributeError: + try: + # pyOpenSSL < 16.0: + bio = crypto._new_mem_buf() + if binary: + rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey) + else: + rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey) + if rc != 1: + crypto._raise_current_error() + return crypto._bio_to_string(bio) + except AttributeError: + self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' + 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') + + def bigint_to_int(self, bn): + '''Convert OpenSSL BIGINT to Python integer''' + if bn == OpenSSL._util.ffi.NULL: + return None + hexstr = OpenSSL._util.lib.BN_bn2hex(bn) + try: + return int(OpenSSL._util.ffi.string(hexstr), 16) + finally: + OpenSSL._util.lib.OPENSSL_free(hexstr) + + def _get_key_info(self): + key_public_data = dict() + key_private_data = dict() + openssl_key_type = self.key.type() + try_fallback = True + if crypto.TYPE_RSA == openssl_key_type: + key_type = 'RSA' + key_public_data['size'] = self.key.bits() + + try: + # Use OpenSSL directly to extract key data + key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(self.key._pkey) + key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free) + # OpenSSL 1.1 and newer have functions to extract the parameters + # from the EVP PKEY data structures. Older versions didn't have + # these getters, and it was common use to simply access the values + # directly. Since there's no guarantee that these data structures + # will still be accessible in the future, we use the getters for + # 1.1 and later, and directly access the values for 1.0.x and + # earlier. + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: + # Get modulus and exponents + n = OpenSSL._util.ffi.new("BIGNUM **") + e = OpenSSL._util.ffi.new("BIGNUM **") + d = OpenSSL._util.ffi.new("BIGNUM **") + OpenSSL._util.lib.RSA_get0_key(key, n, e, d) + key_public_data['modulus'] = self.bigint_to_int(n[0]) + key_public_data['exponent'] = self.bigint_to_int(e[0]) + key_private_data['exponent'] = self.bigint_to_int(d[0]) + # Get factors + p = OpenSSL._util.ffi.new("BIGNUM **") + q = OpenSSL._util.ffi.new("BIGNUM **") + OpenSSL._util.lib.RSA_get0_factors(key, p, q) + key_private_data['p'] = self.bigint_to_int(p[0]) + key_private_data['q'] = self.bigint_to_int(q[0]) + else: + # Get modulus and exponents + key_public_data['modulus'] = self.bigint_to_int(key.n) + key_public_data['exponent'] = self.bigint_to_int(key.e) + key_private_data['exponent'] = self.bigint_to_int(key.d) + # Get factors + key_private_data['p'] = self.bigint_to_int(key.p) + key_private_data['q'] = self.bigint_to_int(key.q) + try_fallback = False + except AttributeError: + # Use fallback if available + pass + elif crypto.TYPE_DSA == openssl_key_type: + key_type = 'DSA' + key_public_data['size'] = self.key.bits() + + try: + # Use OpenSSL directly to extract key data + key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(self.key._pkey) + key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free) + # OpenSSL 1.1 and newer have functions to extract the parameters + # from the EVP PKEY data structures. Older versions didn't have + # these getters, and it was common use to simply access the values + # directly. Since there's no guarantee that these data structures + # will still be accessible in the future, we use the getters for + # 1.1 and later, and directly access the values for 1.0.x and + # earlier. + if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: + # Get public parameters (primes and group element) + p = OpenSSL._util.ffi.new("BIGNUM **") + q = OpenSSL._util.ffi.new("BIGNUM **") + g = OpenSSL._util.ffi.new("BIGNUM **") + OpenSSL._util.lib.DSA_get0_pqg(key, p, q, g) + key_public_data['p'] = self.bigint_to_int(p[0]) + key_public_data['q'] = self.bigint_to_int(q[0]) + key_public_data['g'] = self.bigint_to_int(g[0]) + # Get public and private key exponents + y = OpenSSL._util.ffi.new("BIGNUM **") + x = OpenSSL._util.ffi.new("BIGNUM **") + OpenSSL._util.lib.DSA_get0_key(key, y, x) + key_public_data['y'] = self.bigint_to_int(y[0]) + key_private_data['x'] = self.bigint_to_int(x[0]) + else: + # Get public parameters (primes and group element) + key_public_data['p'] = self.bigint_to_int(key.p) + key_public_data['q'] = self.bigint_to_int(key.q) + key_public_data['g'] = self.bigint_to_int(key.g) + # Get public and private key exponents + key_public_data['y'] = self.bigint_to_int(key.pub_key) + key_private_data['x'] = self.bigint_to_int(key.priv_key) + try_fallback = False + except AttributeError: + # Use fallback if available + pass + else: + # Return 'unknown' + key_type = 'unknown ({0})'.format(self.key.type()) + # If needed and if possible, fall back to cryptography + if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND: + return _get_cryptography_key_info(self.key.to_cryptography_key()) + return key_type, key_public_data, key_private_data + + def _is_key_consistent(self, key_public_data, key_private_data): + openssl_key_type = self.key.type() + if crypto.TYPE_RSA == openssl_key_type: + try: + return self.key.check() + except crypto.Error: + # OpenSSL error means that key is not consistent + return False + if crypto.TYPE_DSA == openssl_key_type: + result = _check_dsa_consistency(key_public_data, key_private_data) + if result is not None: + return result + signature = crypto.sign(self.key, SIGNATURE_TEST_DATA, 'sha256') + # Verify wants a cert (where it can get the public key from) + cert = crypto.X509() + cert.set_pubkey(self.key) + try: + crypto.verify(cert, signature, SIGNATURE_TEST_DATA, 'sha256') + return True + except crypto.Error: + return False + # If needed and if possible, fall back to cryptography + if PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND: + return _is_cryptography_key_consistent(self.key.to_cryptography_key(), key_public_data, key_private_data) + return None + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str'), + passphrase=dict(type='str', no_log=True), + return_private_key_data=dict(type='bool', default=False), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + try: + if module.params['path'] is not None: + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detect what backend we can use + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # If cryptography is available we'll use it + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Fail if no backend has been found + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + privatekey = PrivateKeyInfoPyOpenSSL(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + privatekey = PrivateKeyInfoCryptography(module) + + result = privatekey.get_info() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/openssl_publickey.py b/plugins/modules/openssl_publickey.py new file mode 100644 index 00000000..847c422b --- /dev/null +++ b/plugins/modules/openssl_publickey.py @@ -0,0 +1,471 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: openssl_publickey +short_description: Generate an OpenSSL public key from its private key. +description: + - This module allows one to (re)generate OpenSSL public keys from their private keys. + - Keys are generated in PEM or OpenSSH format. + - The module can use the cryptography Python library, or the pyOpenSSL Python + library. By default, it tries to detect which one is available. This can be + overridden with the I(select_crypto_backend) option. When I(format) is C(OpenSSH), + the C(cryptography) backend has to be used. Please note that the PyOpenSSL backend + was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." +requirements: + - Either cryptography >= 1.2.3 (older versions might work as well) + - Or pyOpenSSL >= 16.0.0 + - Needs cryptography >= 1.4 if I(format) is C(OpenSSH) +author: + - Yanis Guenane (@Spredzy) + - Felix Fontein (@felixfontein) +options: + state: + description: + - Whether the public key should exist or not, taking action if the state is different from what is stated. + type: str + default: present + choices: [ absent, present ] + force: + description: + - Should the key be regenerated even it it already exists. + type: bool + default: no + format: + description: + - The format of the public key. + type: str + default: PEM + choices: [ OpenSSH, PEM ] + path: + description: + - Name of the file in which the generated TLS/SSL public key will be written. + type: path + required: true + privatekey_path: + description: + - Path to the TLS/SSL private key from which to generate the public key. + - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. + If I(state) is C(present), one of them is required. + type: path + privatekey_content: + description: + - The content of the TLS/SSL private key from which to generate the public key. + - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. + If I(state) is C(present), one of them is required. + type: str + privatekey_passphrase: + description: + - The passphrase for the private key. + type: str + backup: + description: + - Create a backup file including a timestamp so you can get the original + public key back if you overwrote it with a different one by accident. + type: bool + default: no + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + return_content: + description: + - If set to C(yes), will return the (current or generated) public key's content as I(publickey). + type: bool + default: no +extends_documentation_fragment: +- files +seealso: +- module: openssl_certificate +- module: openssl_csr +- module: openssl_dhparam +- module: openssl_pkcs12 +- module: openssl_privatekey +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL public key in PEM format + openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem + +- name: Generate an OpenSSL public key in PEM format from an inline key + openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_content: "{{ private_key_content }}" + +- name: Generate an OpenSSL public key in OpenSSH v2 format + openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem + format: OpenSSH + +- name: Generate an OpenSSL public key with a passphrase protected private key + openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem + privatekey_passphrase: ansible + +- name: Force regenerate an OpenSSL public key if it already exists + openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem + force: yes + +- name: Remove an OpenSSL public key + openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + state: absent +''' + +RETURN = r''' +privatekey: + description: + - Path to the TLS/SSL private key the public key was generated from. + - Will be C(none) if the private key has been provided in I(privatekey_content). + returned: changed or success + type: str + sample: /etc/ssl/private/ansible.com.pem +format: + description: The format of the public key (PEM, OpenSSH, ...). + returned: changed or success + type: str + sample: PEM +filename: + description: Path to the generated TLS/SSL public key file. + returned: changed or success + type: str + sample: /etc/ssl/public/ansible.com.pem +fingerprint: + description: + - The fingerprint of the public key. Fingerprint will be generated for each hashlib.algorithms available. + - Requires PyOpenSSL >= 16.0 for meaningful output. + returned: changed or success + type: dict + sample: + md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29" + sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10" + sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46" + sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7" + sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d" + sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b" +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/publickey.pem.2019-03-09@11:22~ +publickey: + description: The (current or generated) public key's content. + returned: if I(state) is C(present) and I(return_content) is C(yes) + type: str + version_added: "2.10" +''' + +import os +import traceback +from distutils.version import LooseVersion + +MINIMAL_PYOPENSSL_VERSION = '16.0.0' +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' +MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) +except ImportError: + PYOPENSSL_IMP_ERR = traceback.format_exc() + PYOPENSSL_FOUND = False +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization as crypto_serialization + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + + +class PublicKeyError(crypto_utils.OpenSSLObjectError): + pass + + +class PublicKey(crypto_utils.OpenSSLObject): + + def __init__(self, module, backend): + super(PublicKey, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.format = module.params['format'] + self.privatekey_path = module.params['privatekey_path'] + self.privatekey_content = module.params['privatekey_content'] + if self.privatekey_content is not None: + self.privatekey_content = self.privatekey_content.encode('utf-8') + self.privatekey_passphrase = module.params['privatekey_passphrase'] + self.privatekey = None + self.publickey_bytes = None + self.return_content = module.params['return_content'] + self.fingerprint = {} + self.backend = backend + + self.backup = module.params['backup'] + self.backup_file = None + + def _create_publickey(self, module): + self.privatekey = crypto_utils.load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend + ) + if self.backend == 'cryptography': + if self.format == 'OpenSSH': + return self.privatekey.public_key().public_bytes( + crypto_serialization.Encoding.OpenSSH, + crypto_serialization.PublicFormat.OpenSSH + ) + else: + return self.privatekey.public_key().public_bytes( + crypto_serialization.Encoding.PEM, + crypto_serialization.PublicFormat.SubjectPublicKeyInfo + ) + else: + try: + return crypto.dump_publickey(crypto.FILETYPE_PEM, self.privatekey) + except AttributeError as dummy: + raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys') + + def generate(self, module): + """Generate the public key.""" + + if self.privatekey_content is None and not os.path.exists(self.privatekey_path): + raise PublicKeyError( + 'The private key %s does not exist' % self.privatekey_path + ) + + if not self.check(module, perms_required=False) or self.force: + try: + publickey_content = self._create_publickey(module) + if self.return_content: + self.publickey_bytes = publickey_content + + if self.backup: + self.backup_file = module.backup_local(self.path) + crypto_utils.write_file(module, publickey_content) + + self.changed = True + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise PublicKeyError(exc) + except (IOError, OSError) as exc: + raise PublicKeyError(exc) + + self.fingerprint = crypto_utils.get_fingerprint( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend, + ) + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + state_and_perms = super(PublicKey, self).check(module, perms_required) + + def _check_privatekey(): + if self.privatekey_content is None and not os.path.exists(self.privatekey_path): + return False + + try: + with open(self.path, 'rb') as public_key_fh: + publickey_content = public_key_fh.read() + if self.return_content: + self.publickey_bytes = publickey_content + if self.backend == 'cryptography': + if self.format == 'OpenSSH': + # Read and dump public key. Makes sure that the comment is stripped off. + current_publickey = crypto_serialization.load_ssh_public_key(publickey_content, backend=default_backend()) + publickey_content = current_publickey.public_bytes( + crypto_serialization.Encoding.OpenSSH, + crypto_serialization.PublicFormat.OpenSSH + ) + else: + current_publickey = crypto_serialization.load_pem_public_key(publickey_content, backend=default_backend()) + publickey_content = current_publickey.public_bytes( + crypto_serialization.Encoding.PEM, + crypto_serialization.PublicFormat.SubjectPublicKeyInfo + ) + else: + publickey_content = crypto.dump_publickey( + crypto.FILETYPE_PEM, + crypto.load_publickey(crypto.FILETYPE_PEM, publickey_content) + ) + except Exception as dummy: + return False + + try: + desired_publickey = self._create_publickey(module) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise PublicKeyError(exc) + + return publickey_content == desired_publickey + + if not state_and_perms: + return state_and_perms + + return _check_privatekey() + + def remove(self, module): + if self.backup: + self.backup_file = module.backup_local(self.path) + super(PublicKey, self).remove(module) + + def dump(self): + """Serialize the object into a dictionary.""" + + result = { + 'privatekey': self.privatekey_path, + 'filename': self.path, + 'format': self.format, + 'changed': self.changed, + 'fingerprint': self.fingerprint, + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + if self.publickey_bytes is None: + self.publickey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None + + return result + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str'), + format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']), + privatekey_passphrase=dict(type='str', no_log=True), + backup=dict(type='bool', default=False), + select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), + return_content=dict(type='bool', default=False), + ), + supports_check_mode=True, + add_file_common_args=True, + required_if=[('state', 'present', ['privatekey_path', 'privatekey_content'], True)], + mutually_exclusive=( + ['privatekey_path', 'privatekey_content'], + ), + ) + + minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION + if module.params['format'] == 'OpenSSH': + minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH + + backend = module.params['select_crypto_backend'] + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(minimal_cryptography_version) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # Decision + if can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + if module.params['format'] == 'OpenSSH': + module.fail_json( + msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH)), + exception=CRYPTOGRAPHY_IMP_ERR + ) + backend = 'pyopenssl' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + minimal_cryptography_version, + MINIMAL_PYOPENSSL_VERSION)) + + if module.params['format'] == 'OpenSSH' and backend != 'cryptography': + module.fail_json(msg="Format OpenSSH requires the cryptography backend.") + + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(minimal_cryptography_version)), + exception=CRYPTOGRAPHY_IMP_ERR) + + base_dir = os.path.dirname(module.params['path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg="The directory '%s' does not exist or the file is not a directory" % base_dir + ) + + try: + public_key = PublicKey(module, backend) + + if public_key.state == 'present': + if module.check_mode: + result = public_key.dump() + result['changed'] = module.params['force'] or not public_key.check(module) + module.exit_json(**result) + + public_key.generate(module) + else: + if module.check_mode: + result = public_key.dump() + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + public_key.remove(module) + + result = public_key.dump() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/x509_crl.py b/plugins/modules/x509_crl.py new file mode 100644 index 00000000..6f5a05e9 --- /dev/null +++ b/plugins/modules/x509_crl.py @@ -0,0 +1,782 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Felix Fontein +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: x509_crl +short_description: Generate Certificate Revocation Lists (CRLs) +description: + - This module allows one to (re)generate or update Certificate Revocation Lists (CRLs). + - Certificates on the revocation list can be either specified via serial number and (optionally) their issuer, + or as a path to a certificate file in PEM format. +requirements: + - cryptography >= 1.2 +author: + - Felix Fontein (@felixfontein) +options: + state: + description: + - Whether the CRL file should exist or not, taking action if the state is different from what is stated. + type: str + default: present + choices: [ absent, present ] + + mode: + description: + - Defines how to process entries of existing CRLs. + - If set to C(generate), makes sure that the CRL has the exact set of revoked certificates + as specified in I(revoked_certificates). + - If set to C(update), makes sure that the CRL contains the revoked certificates from + I(revoked_certificates), but can also contain other revoked certificates. If the CRL file + already exists, all entries from the existing CRL will also be included in the new CRL. + When using C(update), you might be interested in setting I(ignore_timestamps) to C(yes). + type: str + default: generate + choices: [ generate, update ] + + force: + description: + - Should the CRL be forced to be regenerated. + type: bool + default: no + + backup: + description: + - Create a backup file including a timestamp so you can get the original + CRL back if you overwrote it with a new one by accident. + type: bool + default: no + + path: + description: + - Remote absolute path where the generated CRL file should be created or is already located. + type: path + required: yes + + privatekey_path: + description: + - Path to the CA's private key to use when signing the CRL. + - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. + type: path + + privatekey_content: + description: + - The content of the CA's private key to use when signing the CRL. + - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. + type: str + + privatekey_passphrase: + description: + - The passphrase for the I(privatekey_path). + - This is required if the private key is password protected. + type: str + + issuer: + description: + - Key/value pairs that will be present in the issuer name field of the CRL. + - If you need to specify more than one value with the same key, use a list as value. + - Required if I(state) is C(present). + type: dict + + last_update: + description: + - The point in time from which this CRL can be trusted. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent, except when + I(ignore_timestamps) is set to C(yes). + type: str + default: "+0s" + + next_update: + description: + - "The absolute latest point in time by which this I(issuer) is expected to have issued + another CRL. Many clients will treat a CRL as expired once I(next_update) occurs." + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent, except when + I(ignore_timestamps) is set to C(yes). + - Required if I(state) is C(present). + type: str + + digest: + description: + - Digest algorithm to be used when signing the CRL. + type: str + default: sha256 + + revoked_certificates: + description: + - List of certificates to be revoked. + - Required if I(state) is C(present). + type: list + elements: dict + suboptions: + path: + description: + - Path to a certificate in PEM format. + - The serial number and issuer will be extracted from the certificate. + - Mutually exclusive with I(content) and I(serial_number). One of these three options + must be specified. + type: path + content: + description: + - Content of a certificate in PEM format. + - The serial number and issuer will be extracted from the certificate. + - Mutually exclusive with I(path) and I(serial_number). One of these three options + must be specified. + type: str + serial_number: + description: + - Serial number of the certificate. + - Mutually exclusive with I(path) and I(content). One of these three options must + be specified. + type: int + revocation_date: + description: + - The point in time the certificate was revoked. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent, except when + I(ignore_timestamps) is set to C(yes). + type: str + default: "+0s" + issuer: + description: + - The certificate's issuer. + - "Example: C(DNS:ca.example.org)" + type: list + elements: str + issuer_critical: + description: + - Whether the certificate issuer extension should be critical. + type: bool + default: no + reason: + description: + - The value for the revocation reason extension. + type: str + choices: + - unspecified + - key_compromise + - ca_compromise + - affiliation_changed + - superseded + - cessation_of_operation + - certificate_hold + - privilege_withdrawn + - aa_compromise + - remove_from_crl + reason_critical: + description: + - Whether the revocation reason extension should be critical. + type: bool + default: no + invalidity_date: + description: + - The point in time it was known/suspected that the private key was compromised + or that the certificate otherwise became invalid. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (e.g. C(+32w1d2h). + - Note that if using relative time this module is NOT idempotent. This will NOT + change when I(ignore_timestamps) is set to C(yes). + type: str + invalidity_date_critical: + description: + - Whether the invalidity date extension should be critical. + type: bool + default: no + + ignore_timestamps: + description: + - Whether the timestamps I(last_update), I(next_update) and I(revocation_date) (in + I(revoked_certificates)) should be ignored for idempotency checks. The timestamp + I(invalidity_date) in I(revoked_certificates) will never be ignored. + - Use this in combination with relative timestamps for these values to get idempotency. + type: bool + default: no + + return_content: + description: + - If set to C(yes), will return the (current or generated) CRL's content as I(crl). + type: bool + default: no + +extends_documentation_fragment: + - files + +notes: + - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern. + - Date specified should be UTC. Minutes and seconds are mandatory. +''' + +EXAMPLES = r''' +- name: Generate a CRL + x509_crl: + path: /etc/ssl/my-ca.crl + privatekey_path: /etc/ssl/private/my-ca.pem + issuer: + CN: My CA + last_update: "+0s" + next_update: "+7d" + revoked_certificates: + - serial_number: 1234 + revocation_date: 20190331202428Z + issuer: + CN: My CA + - serial_number: 2345 + revocation_date: 20191013152910Z + reason: affiliation_changed + invalidity_date: 20191001000000Z + - path: /etc/ssl/crt/revoked-cert.pem + revocation_date: 20191010010203Z +''' + +RETURN = r''' +filename: + description: Path to the generated CRL + returned: changed or success + type: str + sample: /path/to/my-ca.crl +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/my-ca.crl.2019-03-09@11:22~ +privatekey: + description: Path to the private CA key + returned: changed or success + type: str + sample: /path/to/my-ca.pem +issuer: + description: + - The CRL's issuer. + - Note that for repeated values, only the last one will be returned. + returned: success + type: dict + sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}' +issuer_ordered: + description: The CRL's issuer as an ordered list of tuples. + returned: success + type: list + elements: list + sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]' +last_update: + description: The point in time from which this CRL can be trusted as ASN.1 TIME. + returned: success + type: str + sample: 20190413202428Z +next_update: + description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME. + returned: success + type: str + sample: 20190413202428Z +digest: + description: The signature algorithm used to sign the CRL. + returned: success + type: str + sample: sha256WithRSAEncryption +revoked_certificates: + description: List of certificates to be revoked. + returned: success + type: list + elements: dict + contains: + serial_number: + description: Serial number of the certificate. + type: int + sample: 1234 + revocation_date: + description: The point in time the certificate was revoked as ASN.1 TIME. + type: str + sample: 20190413202428Z + issuer: + description: The certificate's issuer. + type: list + elements: str + sample: '["DNS:ca.example.org"]' + issuer_critical: + description: Whether the certificate issuer extension is critical. + type: bool + sample: no + reason: + description: + - The value for the revocation reason extension. + - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded), + C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and + C(remove_from_crl). + type: str + sample: key_compromise + reason_critical: + description: Whether the revocation reason extension is critical. + type: bool + sample: no + invalidity_date: + description: | + The point in time it was known/suspected that the private key was compromised + or that the certificate otherwise became invalid as ASN.1 TIME. + type: str + sample: 20190413202428Z + invalidity_date_critical: + description: Whether the invalidity date extension is critical. + type: bool + sample: no +crl: + description: The (current or generated) CRL's content. + returned: if I(state) is C(present) and I(return_content) is C(yes) + type: str +''' + + +import os +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import Encoding + from cryptography.x509 import ( + CertificateRevocationListBuilder, + RevokedCertificateBuilder, + NameAttribute, + Name, + ) + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" + + +class CRLError(crypto_utils.OpenSSLObjectError): + pass + + +class CRL(crypto_utils.OpenSSLObject): + + def __init__(self, module): + super(CRL, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + + self.update = module.params['mode'] == 'update' + self.ignore_timestamps = module.params['ignore_timestamps'] + self.return_content = module.params['return_content'] + self.crl_content = None + + self.privatekey_path = module.params['privatekey_path'] + self.privatekey_content = module.params['privatekey_content'] + if self.privatekey_content is not None: + self.privatekey_content = self.privatekey_content.encode('utf-8') + self.privatekey_passphrase = module.params['privatekey_passphrase'] + + self.issuer = crypto_utils.parse_name_field(module.params['issuer']) + self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]] + + self.last_update = crypto_utils.get_relative_time_option(module.params['last_update'], 'last_update') + self.next_update = crypto_utils.get_relative_time_option(module.params['next_update'], 'next_update') + + self.digest = crypto_utils.select_message_digest(module.params['digest']) + if self.digest is None: + raise CRLError('The digest "{0}" is not supported'.format(module.params['digest'])) + + self.revoked_certificates = [] + for i, rc in enumerate(module.params['revoked_certificates']): + result = { + 'serial_number': None, + 'revocation_date': None, + 'issuer': None, + 'issuer_critical': False, + 'reason': None, + 'reason_critical': False, + 'invalidity_date': None, + 'invalidity_date_critical': False, + } + path_prefix = 'revoked_certificates[{0}].'.format(i) + if rc['path'] is not None or rc['content'] is not None: + # Load certificate from file or content + try: + if rc['content'] is not None: + rc['content'] = rc['content'].encode('utf-8') + cert = crypto_utils.load_certificate(rc['path'], content=rc['content'], backend='cryptography') + try: + result['serial_number'] = cert.serial_number + except AttributeError: + # The property was called "serial" before cryptography 1.4 + result['serial_number'] = cert.serial + except crypto_utils.OpenSSLObjectError as e: + if rc['content'] is not None: + module.fail_json( + msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e)) + ) + else: + module.fail_json( + msg='Cannot read certificate "{1}" from {0}path: {2}'.format(path_prefix, rc['path'], to_native(e)) + ) + else: + # Specify serial_number (and potentially issuer) directly + result['serial_number'] = rc['serial_number'] + # All other options + if rc['issuer']: + result['issuer'] = [crypto_utils.cryptography_get_name(issuer) for issuer in rc['issuer']] + result['issuer_critical'] = rc['issuer_critical'] + result['revocation_date'] = crypto_utils.get_relative_time_option( + rc['revocation_date'], + path_prefix + 'revocation_date' + ) + if rc['reason']: + result['reason'] = crypto_utils.REVOCATION_REASON_MAP[rc['reason']] + result['reason_critical'] = rc['reason_critical'] + if rc['invalidity_date']: + result['invalidity_date'] = crypto_utils.get_relative_time_option( + rc['invalidity_date'], + path_prefix + 'invalidity_date' + ) + result['invalidity_date_critical'] = rc['invalidity_date_critical'] + self.revoked_certificates.append(result) + + self.module = module + + self.backup = module.params['backup'] + self.backup_file = None + + try: + self.privatekey = crypto_utils.load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend='cryptography' + ) + except crypto_utils.OpenSSLBadPassphraseError as exc: + raise CRLError(exc) + + self.crl = None + try: + with open(self.path, 'rb') as f: + data = f.read() + self.crl = x509.load_pem_x509_crl(data, default_backend()) + if self.return_content: + self.crl_content = data + except Exception as dummy: + self.crl_content = None + + def remove(self): + if self.backup: + self.backup_file = self.module.backup_local(self.path) + super(CRL, self).remove(self.module) + + def _compress_entry(self, entry): + if self.ignore_timestamps: + # Throw out revocation_date + return ( + entry['serial_number'], + tuple(entry['issuer']) if entry['issuer'] is not None else None, + entry['issuer_critical'], + entry['reason'], + entry['reason_critical'], + entry['invalidity_date'], + entry['invalidity_date_critical'], + ) + else: + return ( + entry['serial_number'], + entry['revocation_date'], + tuple(entry['issuer']) if entry['issuer'] is not None else None, + entry['issuer_critical'], + entry['reason'], + entry['reason_critical'], + entry['invalidity_date'], + entry['invalidity_date_critical'], + ) + + def check(self, perms_required=True): + """Ensure the resource is in its desired state.""" + + state_and_perms = super(CRL, self).check(self.module, perms_required) + + if not state_and_perms: + return False + + if self.crl is None: + return False + + if self.last_update != self.crl.last_update and not self.ignore_timestamps: + return False + if self.next_update != self.crl.next_update and not self.ignore_timestamps: + return False + if self.digest.name != self.crl.signature_hash_algorithm.name: + return False + + want_issuer = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer] + if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]: + return False + + old_entries = [self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(cert)) for cert in self.crl] + new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates] + if self.update: + # We don't simply use a set so that duplicate entries are treated correctly + for entry in new_entries: + try: + old_entries.remove(entry) + except ValueError: + return False + else: + if old_entries != new_entries: + return False + + return True + + def _generate_crl(self): + backend = default_backend() + crl = CertificateRevocationListBuilder() + + try: + crl = crl.issuer_name(Name([ + NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1])) + for entry in self.issuer + ])) + except ValueError as e: + raise CRLError(e) + + crl = crl.last_update(self.last_update) + crl = crl.next_update(self.next_update) + + if self.update and self.crl: + new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates]) + for entry in self.crl: + decoded_entry = self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(entry)) + if decoded_entry not in new_entries: + crl = crl.add_revoked_certificate(entry) + for entry in self.revoked_certificates: + revoked_cert = RevokedCertificateBuilder() + revoked_cert = revoked_cert.serial_number(entry['serial_number']) + revoked_cert = revoked_cert.revocation_date(entry['revocation_date']) + if entry['issuer'] is not None: + revoked_cert = revoked_cert.add_extension( + x509.CertificateIssuer([ + crypto_utils.cryptography_get_name(name) for name in self.entry['issuer'] + ]), + entry['issuer_critical'] + ) + if entry['reason'] is not None: + revoked_cert = revoked_cert.add_extension( + x509.CRLReason(entry['reason']), + entry['reason_critical'] + ) + if entry['invalidity_date'] is not None: + revoked_cert = revoked_cert.add_extension( + x509.InvalidityDate(entry['invalidity_date']), + entry['invalidity_date_critical'] + ) + crl = crl.add_revoked_certificate(revoked_cert.build(backend)) + + self.crl = crl.sign(self.privatekey, self.digest, backend=backend) + return self.crl.public_bytes(Encoding.PEM) + + def generate(self): + if not self.check(perms_required=False) or self.force: + result = self._generate_crl() + if self.return_content: + self.crl_content = result + if self.backup: + self.backup_file = self.module.backup_local(self.path) + crypto_utils.write_file(self.module, result) + self.changed = True + + file_args = self.module.load_file_common_arguments(self.module.params) + if self.module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def _dump_revoked(self, entry): + return { + 'serial_number': entry['serial_number'], + 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), + 'issuer': + [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']] + if entry['issuer'] is not None else None, + 'issuer_critical': entry['issuer_critical'], + 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, + 'reason_critical': entry['reason_critical'], + 'invalidity_date': + entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) + if entry['invalidity_date'] is not None else None, + 'invalidity_date_critical': entry['invalidity_date_critical'], + } + + def dump(self, check_mode=False): + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'last_update': None, + 'next_update': None, + 'digest': None, + 'issuer_ordered': None, + 'issuer': None, + 'revoked_certificates': [], + } + if self.backup_file: + result['backup_file'] = self.backup_file + + if check_mode: + result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT) + result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT) + # result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) + result['digest'] = self.module.params['digest'] + result['issuer_ordered'] = self.issuer + result['issuer'] = {} + for k, v in self.issuer: + result['issuer'][k] = v + result['revoked_certificates'] = [] + for entry in self.revoked_certificates: + result['revoked_certificates'].append(self._dump_revoked(entry)) + elif self.crl: + result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) + result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) + try: + result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) + except AttributeError: + # Older cryptography versions don't have signature_algorithm_oid yet + dotted = crypto_utils._obj2txt( + self.crl._backend._lib, + self.crl._backend._ffi, + self.crl._x509_crl.sig_alg.algorithm + ) + oid = x509.oid.ObjectIdentifier(dotted) + result['digest'] = crypto_utils.cryptography_oid_to_name(oid) + issuer = [] + for attribute in self.crl.issuer: + issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + result['issuer_ordered'] = issuer + result['issuer'] = {} + for k, v in issuer: + result['issuer'][k] = v + result['revoked_certificates'] = [] + for cert in self.crl: + entry = crypto_utils.cryptography_decode_revoked_certificate(cert) + result['revoked_certificates'].append(self._dump_revoked(entry)) + + if self.return_content: + result['crl'] = self.crl_content + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + mode=dict(type='str', default='generate', choices=['generate', 'update']), + force=dict(type='bool', default=False), + backup=dict(type='bool', default=False), + path=dict(type='path', required=True), + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str'), + privatekey_passphrase=dict(type='str', no_log=True), + issuer=dict(type='dict'), + last_update=dict(type='str', default='+0s'), + next_update=dict(type='str'), + digest=dict(type='str', default='sha256'), + ignore_timestamps=dict(type='bool', default=False), + return_content=dict(type='bool', default=False), + revoked_certificates=dict( + type='list', + elements='dict', + options=dict( + path=dict(type='path'), + content=dict(type='str'), + serial_number=dict(type='int'), + revocation_date=dict(type='str', default='+0s'), + issuer=dict(type='list', elements='str'), + issuer_critical=dict(type='bool', default=False), + reason=dict( + type='str', + choices=[ + 'unspecified', 'key_compromise', 'ca_compromise', 'affiliation_changed', + 'superseded', 'cessation_of_operation', 'certificate_hold', + 'privilege_withdrawn', 'aa_compromise', 'remove_from_crl' + ] + ), + reason_critical=dict(type='bool', default=False), + invalidity_date=dict(type='str'), + invalidity_date_critical=dict(type='bool', default=False), + ), + required_one_of=[['path', 'content', 'serial_number']], + mutually_exclusive=[['path', 'content', 'serial_number']], + ), + ), + required_if=[ + ('state', 'present', ['privatekey_path', 'privatekey_content'], True), + ('state', 'present', ['issuer', 'next_update', 'revoked_certificates'], False), + ], + mutually_exclusive=( + ['privatekey_path', 'privatekey_content'], + ), + supports_check_mode=True, + add_file_common_args=True, + ) + + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + + try: + crl = CRL(module) + + if module.params['state'] == 'present': + if module.check_mode: + result = crl.dump(check_mode=True) + result['changed'] = module.params['force'] or not crl.check() + module.exit_json(**result) + + crl.generate() + else: + if module.check_mode: + result = crl.dump(check_mode=True) + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + crl.remove() + + result = crl.dump() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/x509_crl_info.py b/plugins/modules/x509_crl_info.py new file mode 100644 index 00000000..87b1b8cc --- /dev/null +++ b/plugins/modules/x509_crl_info.py @@ -0,0 +1,280 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Felix Fontein +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: x509_crl_info +short_description: Retrieve information on Certificate Revocation Lists (CRLs) +description: + - This module allows one to retrieve information on Certificate Revocation Lists (CRLs). +requirements: + - cryptography >= 1.2 +author: + - Felix Fontein (@felixfontein) +options: + path: + description: + - Remote absolute path where the generated CRL file should be created or is already located. + - Either I(path) or I(content) must be specified, but not both. + type: path + content: + description: + - Content of the X.509 certificate in PEM format. + - Either I(path) or I(content) must be specified, but not both. + type: str + +notes: + - All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern. + They are all in UTC. +seealso: + - module: x509_crl +''' + +EXAMPLES = r''' +- name: Get information on CRL + x509_crl_info: + path: /etc/ssl/my-ca.crl + register: result + +- debug: + msg: "{{ result }}" +''' + +RETURN = r''' +issuer: + description: + - The CRL's issuer. + - Note that for repeated values, only the last one will be returned. + returned: success + type: dict + sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}' +issuer_ordered: + description: The CRL's issuer as an ordered list of tuples. + returned: success + type: list + elements: list + sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]' +last_update: + description: The point in time from which this CRL can be trusted as ASN.1 TIME. + returned: success + type: str + sample: 20190413202428Z +next_update: + description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME. + returned: success + type: str + sample: 20190413202428Z +digest: + description: The signature algorithm used to sign the CRL. + returned: success + type: str + sample: sha256WithRSAEncryption +revoked_certificates: + description: List of certificates to be revoked. + returned: success + type: list + elements: dict + contains: + serial_number: + description: Serial number of the certificate. + type: int + sample: 1234 + revocation_date: + description: The point in time the certificate was revoked as ASN.1 TIME. + type: str + sample: 20190413202428Z + issuer: + description: The certificate's issuer. + type: list + elements: str + sample: '["DNS:ca.example.org"]' + issuer_critical: + description: Whether the certificate issuer extension is critical. + type: bool + sample: no + reason: + description: + - The value for the revocation reason extension. + - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded), + C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and + C(remove_from_crl). + type: str + sample: key_compromise + reason_critical: + description: Whether the revocation reason extension is critical. + type: bool + sample: no + invalidity_date: + description: | + The point in time it was known/suspected that the private key was compromised + or that the certificate otherwise became invalid as ASN.1 TIME. + type: str + sample: 20190413202428Z + invalidity_date_critical: + description: Whether the invalidity date extension is critical. + type: bool + sample: no +''' + + +import traceback +from distutils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" + + +class CRLError(crypto_utils.OpenSSLObjectError): + pass + + +class CRLInfo(crypto_utils.OpenSSLObject): + """The main module implementation.""" + + def __init__(self, module): + super(CRLInfo, self).__init__( + module.params['path'] or '', + 'present', + False, + module.check_mode + ) + + self.content = module.params['content'] + + self.module = module + + self.crl = None + if self.content is None: + try: + with open(self.path, 'rb') as f: + data = f.read() + except Exception as e: + self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e)) + else: + data = self.content.encode('utf-8') + + try: + self.crl = x509.load_pem_x509_crl(data, default_backend()) + except Exception as e: + self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e)) + + def _dump_revoked(self, entry): + return { + 'serial_number': entry['serial_number'], + 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), + 'issuer': + [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']] + if entry['issuer'] is not None else None, + 'issuer_critical': entry['issuer_critical'], + 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, + 'reason_critical': entry['reason_critical'], + 'invalidity_date': + entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) + if entry['invalidity_date'] is not None else None, + 'invalidity_date_critical': entry['invalidity_date_critical'], + } + + def get_info(self): + result = { + 'changed': False, + 'last_update': None, + 'next_update': None, + 'digest': None, + 'issuer_ordered': None, + 'issuer': None, + 'revoked_certificates': [], + } + + result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) + result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) + try: + result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) + except AttributeError: + # Older cryptography versions don't have signature_algorithm_oid yet + dotted = crypto_utils._obj2txt( + self.crl._backend._lib, + self.crl._backend._ffi, + self.crl._x509_crl.sig_alg.algorithm + ) + oid = x509.oid.ObjectIdentifier(dotted) + result['digest'] = crypto_utils.cryptography_oid_to_name(oid) + issuer = [] + for attribute in self.crl.issuer: + issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + result['issuer_ordered'] = issuer + result['issuer'] = {} + for k, v in issuer: + result['issuer'][k] = v + result['revoked_certificates'] = [] + for cert in self.crl: + entry = crypto_utils.cryptography_decode_revoked_certificate(cert) + result['revoked_certificates'].append(self._dump_revoked(entry)) + + return result + + def generate(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + def dump(self): + # Empty method because crypto_utils.OpenSSLObject wants this + pass + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str'), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + + try: + crl = CRLInfo(module) + result = crl.get_info() + module.exit_json(**result) + except crypto_utils.OpenSSLObjectError as e: + module.fail_json(msg=to_native(e)) + + +if __name__ == "__main__": + main() diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..ea1472ec --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/tests/integration/targets/acme_account/aliases b/tests/integration/targets/acme_account/aliases new file mode 100644 index 00000000..d7936330 --- /dev/null +++ b/tests/integration/targets/acme_account/aliases @@ -0,0 +1,2 @@ +shippable/cloud/group1 +cloud/acme diff --git a/tests/integration/targets/acme_account/meta/main.yml b/tests/integration/targets/acme_account/meta/main.yml new file mode 100644 index 00000000..81d1e7e7 --- /dev/null +++ b/tests/integration/targets/acme_account/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_acme diff --git a/tests/integration/targets/acme_account/tasks/impl.yml b/tests/integration/targets/acme_account/tasks/impl.yml new file mode 100644 index 00000000..3cd10c47 --- /dev/null +++ b/tests/integration/targets/acme_account/tasks/impl.yml @@ -0,0 +1,244 @@ +- name: Generate account key + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem + +- name: Parse account key (to ease debugging some test failures) + command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text + +- name: Do not try to create account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: no + ignore_errors: yes + register: account_not_created + +- name: Create it now (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: yes + terms_agreed: yes + contact: + - mailto:example@example.org + check_mode: yes + diff: yes + register: account_created_check + +- name: Create it now + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: yes + terms_agreed: yes + contact: + - mailto:example@example.org + register: account_created + +- name: Create it now (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: yes + terms_agreed: yes + contact: + - mailto:example@example.org + register: account_created_idempotent + +- name: Change email address (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + # allow_creation: no + contact: + - mailto:example@example.com + check_mode: yes + diff: yes + register: account_modified_check + +- name: Change email address + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + # allow_creation: no + contact: + - mailto:example@example.com + register: account_modified + +- name: Change email address (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_created.account_uri }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + # allow_creation: no + contact: + - mailto:example@example.com + register: account_modified_idempotent + +- name: Cannot access account with wrong URI + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_created.account_uri ~ '12345thisdoesnotexist' }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + contact: [] + ignore_errors: yes + register: account_modified_wrong_uri + +- name: Clear contact email addresses (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + # allow_creation: no + contact: [] + check_mode: yes + diff: yes + register: account_modified_2_check + +- name: Clear contact email addresses + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + # allow_creation: no + contact: [] + register: account_modified_2 + +- name: Clear contact email addresses (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + # allow_creation: no + contact: [] + register: account_modified_2_idempotent + +- name: Generate new account key + command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/accountkey2.pem + +- name: Parse account key (to ease debugging some test failures) + command: openssl ec -in {{ output_dir }}/accountkey2.pem -noout -text + +- name: Change account key (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + new_account_key_src: "{{ output_dir }}/accountkey2.pem" + state: changed_key + contact: + - mailto:example@example.com + check_mode: yes + diff: yes + register: account_change_key_check + +- name: Change account key + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + new_account_key_src: "{{ output_dir }}/accountkey2.pem" + state: changed_key + contact: + - mailto:example@example.com + register: account_change_key + +- name: Deactivate account (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: absent + check_mode: yes + diff: yes + register: account_deactivate_check + +- name: Deactivate account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: absent + register: account_deactivate + +- name: Deactivate account (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: absent + register: account_deactivate_idempotent + +- name: Do not try to create account II + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: no + ignore_errors: yes + register: account_not_created_2 + +- name: Do not try to create account III + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: no + ignore_errors: yes + register: account_not_created_3 diff --git a/tests/integration/targets/acme_account/tasks/main.yml b/tests/integration/targets/acme_account/tasks/main.yml new file mode 100644 index 00000000..e46c6dc4 --- /dev/null +++ b/tests/integration/targets/acme_account/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- block: + - name: Running tests with OpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: openssl + + - import_tasks: ../tests/validate.yml + + # Old 0.9.8 versions have insufficient CLI support for signing with EC keys + when: openssl_version.stdout is version('1.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/tests/integration/targets/acme_account/tests/validate.yml b/tests/integration/targets/acme_account/tests/validate.yml new file mode 100644 index 00000000..3bc1d7aa --- /dev/null +++ b/tests/integration/targets/acme_account/tests/validate.yml @@ -0,0 +1,129 @@ +--- +- name: Validate that account wasn't created in the first step + assert: + that: + - account_not_created is failed + - account_not_created.msg == 'Account does not exist or is deactivated.' + +- name: Validate that account was created in the second step (check mode) + assert: + that: + - account_created_check is changed + - account_created_check.account_uri is none + - "'diff' in account_created_check" + - "account_created_check.diff.before == {}" + - "'after' in account_created_check.diff" + - account_created_check.diff.after.contact | length == 1 + - account_created_check.diff.after.contact[0] == 'mailto:example@example.org' + +- name: Validate that account was created in the second step + assert: + that: + - account_created is changed + - account_created.account_uri is not none + +- name: Validate that account was created in the second step (idempotency) + assert: + that: + - account_created_idempotent is not changed + - account_created_idempotent.account_uri is not none + +- name: Validate that email address was changed (check mode) + assert: + that: + - account_modified_check is changed + - account_modified_check.account_uri is not none + - "'diff' in account_modified_check" + - account_modified_check.diff.before.contact | length == 1 + - account_modified_check.diff.before.contact[0] == 'mailto:example@example.org' + - account_modified_check.diff.after.contact | length == 1 + - account_modified_check.diff.after.contact[0] == 'mailto:example@example.com' + +- name: Validate that email address was changed + assert: + that: + - account_modified is changed + - account_modified.account_uri is not none + +- name: Validate that email address was not changed a second time (idempotency) + assert: + that: + - account_modified_idempotent is not changed + - account_modified_idempotent.account_uri is not none + +- name: Make sure that with the wrong account URI, the account cannot be changed + assert: + that: + - account_modified_wrong_uri is failed + +- name: Validate that email address was cleared (check mode) + assert: + that: + - account_modified_2_check is changed + - account_modified_2_check.account_uri is not none + - "'diff' in account_modified_2_check" + - account_modified_2_check.diff.before.contact | length == 1 + - account_modified_2_check.diff.before.contact[0] == 'mailto:example@example.com' + - account_modified_2_check.diff.after.contact | length == 0 + +- name: Validate that email address was cleared + assert: + that: + - account_modified_2 is changed + - account_modified_2.account_uri is not none + +- name: Validate that email address was not cleared a second time (idempotency) + assert: + that: + - account_modified_2_idempotent is not changed + - account_modified_2_idempotent.account_uri is not none + +- name: Validate that the account key was changed (check mode) + assert: + that: + - account_change_key_check is changed + - account_change_key_check.account_uri is not none + - "'diff' in account_change_key_check" + - account_change_key_check.diff.before.public_account_key != account_change_key_check.diff.after.public_account_key + +- name: Validate that the account key was changed + assert: + that: + - account_change_key is changed + - account_change_key.account_uri is not none + +- name: Validate that the account was deactivated (check mode) + assert: + that: + - account_deactivate_check is changed + - account_deactivate_check.account_uri is not none + - "'diff' in account_deactivate_check" + - "account_deactivate_check.diff.before != {}" + - "account_deactivate_check.diff.after == {}" + +- name: Validate that the account was deactivated + assert: + that: + - account_deactivate is changed + - account_deactivate.account_uri is not none + +- name: Validate that the account was really deactivated (idempotency) + assert: + that: + - account_deactivate_idempotent is not changed + # The next condition should be true for all conforming ACME servers. + # In case it is not true, it could be both an error in acme_account + # and in the ACME server. + - account_deactivate_idempotent.account_uri is none + +- name: Validate that the account is gone (new account key) + assert: + that: + - account_not_created_2 is failed + - account_not_created_2.msg == 'Account does not exist or is deactivated.' + +- name: Validate that the account is gone (old account key) + assert: + that: + - account_not_created_3 is failed + - account_not_created_3.msg == 'Account does not exist or is deactivated.' diff --git a/tests/integration/targets/acme_account_info/aliases b/tests/integration/targets/acme_account_info/aliases new file mode 100644 index 00000000..d7936330 --- /dev/null +++ b/tests/integration/targets/acme_account_info/aliases @@ -0,0 +1,2 @@ +shippable/cloud/group1 +cloud/acme diff --git a/tests/integration/targets/acme_account_info/meta/main.yml b/tests/integration/targets/acme_account_info/meta/main.yml new file mode 100644 index 00000000..81d1e7e7 --- /dev/null +++ b/tests/integration/targets/acme_account_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_acme diff --git a/tests/integration/targets/acme_account_info/tasks/impl.yml b/tests/integration/targets/acme_account_info/tasks/impl.yml new file mode 100644 index 00000000..3bce6bba --- /dev/null +++ b/tests/integration/targets/acme_account_info/tasks/impl.yml @@ -0,0 +1,82 @@ +--- +- name: Generate account key + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem + +- name: Generate second account key + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey2.pem + +- name: Parse account key (to ease debugging some test failures) + command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text + +- name: Check that account does not exist + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + register: account_not_created + +- name: Create it now + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: yes + terms_agreed: yes + contact: + - mailto:example@example.org + +- name: Check that account exists + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + register: account_created + +- name: Clear email address + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + state: present + allow_creation: no + contact: [] + +- name: Check that account was modified + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_uri: "{{ account_created.account_uri }}" + register: account_modified + +- name: Check with wrong account URI + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_uri: "{{ account_created.account_uri }}test1234doesnotexists" + register: account_not_exist + +- name: Check with wrong account key + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/accountkey2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_uri: "{{ account_created.account_uri }}" + ignore_errors: yes + register: account_wrong_key diff --git a/tests/integration/targets/acme_account_info/tasks/main.yml b/tests/integration/targets/acme_account_info/tasks/main.yml new file mode 100644 index 00000000..e46c6dc4 --- /dev/null +++ b/tests/integration/targets/acme_account_info/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- block: + - name: Running tests with OpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: openssl + + - import_tasks: ../tests/validate.yml + + # Old 0.9.8 versions have insufficient CLI support for signing with EC keys + when: openssl_version.stdout is version('1.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/tests/integration/targets/acme_account_info/tests/validate.yml b/tests/integration/targets/acme_account_info/tests/validate.yml new file mode 100644 index 00000000..5863a8c3 --- /dev/null +++ b/tests/integration/targets/acme_account_info/tests/validate.yml @@ -0,0 +1,40 @@ +--- +- name: Validate that account wasn't there + assert: + that: + - not account_not_created.exists + - account_not_created.account_uri is none + - "'account' not in account_not_created" + +- name: Validate that account was created + assert: + that: + - account_created.exists + - account_created.account_uri is not none + - "'account' in account_created" + - "'contact' in account_created.account" + - "'public_account_key' in account_created.account" + - account_created.account.contact | length == 1 + - "account_created.account.contact[0] == 'mailto:example@example.org'" + +- name: Validate that account email was removed + assert: + that: + - account_modified.exists + - account_modified.account_uri is not none + - "'account' in account_modified" + - "'contact' in account_modified.account" + - "'public_account_key' in account_modified.account" + - account_modified.account.contact | length == 0 + +- name: Validate that account does not exist with wrong account URI + assert: + that: + - not account_not_exist.exists + - account_not_exist.account_uri is none + - "'account' not in account_not_exist" + +- name: Validate that account cannot be accessed with wrong key + assert: + that: + - account_wrong_key is failed diff --git a/tests/integration/targets/acme_certificate/aliases b/tests/integration/targets/acme_certificate/aliases new file mode 100644 index 00000000..d7936330 --- /dev/null +++ b/tests/integration/targets/acme_certificate/aliases @@ -0,0 +1,2 @@ +shippable/cloud/group1 +cloud/acme diff --git a/tests/integration/targets/acme_certificate/meta/main.yml b/tests/integration/targets/acme_certificate/meta/main.yml new file mode 100644 index 00000000..81d1e7e7 --- /dev/null +++ b/tests/integration/targets/acme_certificate/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_acme diff --git a/tests/integration/targets/acme_certificate/tasks/impl.yml b/tests/integration/targets/acme_certificate/tasks/impl.yml new file mode 100644 index 00000000..04f02710 --- /dev/null +++ b/tests/integration/targets/acme_certificate/tasks/impl.yml @@ -0,0 +1,451 @@ +--- +## SET UP ACCOUNT KEYS ######################################################################## +- name: Create ECC256 account key + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem +- name: Create ECC384 account key + command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem +- name: Create RSA-2048 account key + command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048 +## SET UP ACCOUNTS ############################################################################ +- name: Make sure ECC256 account hasn't been created yet + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_key_src: "{{ output_dir }}/account-ec256.pem" + state: absent +- name: Create ECC384 account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}" + state: present + allow_creation: yes + terms_agreed: yes + contact: + - mailto:example@example.org + - mailto:example@example.com +- name: Create RSA-2048 account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_key_src: "{{ output_dir }}/account-rsa2048.pem" + state: present + allow_creation: yes + terms_agreed: yes + contact: [] +## OBTAIN CERTIFICATES ######################################################################## +- name: Obtain cert 1 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 1 + certificate_name: cert-1 + key_type: rsa + rsa_bits: 2048 + subject_alt_name: "DNS:example.com" + subject_alt_name_critical: no + account_key: account-ec256 + challenge: http-01 + modify_account: yes + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" + retrieve_all_alternates: yes + acme_expected_root_number: 1 + select_chain: + - test_certificates: last + issuer: "{{ acme_roots[1].subject }}" +- name: Store obtain results for cert 1 + set_fact: + cert_1_obtain_results: "{{ certificate_obtain_result }}" + cert_1_alternate: "{{ 1 if select_crypto_backend == 'cryptography' else 0 }}" +- name: Obtain cert 2 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 2 + certificate_name: cert-2 + key_type: ec256 + subject_alt_name: "DNS:*.example.com,DNS:example.com" + subject_alt_name_critical: yes + account_key: account-ec384 + challenge: dns-01 + modify_account: no + deactivate_authzs: yes + force: no + remaining_days: 10 + terms_agreed: no + account_email: "" + acme_expected_root_number: 0 + retrieve_all_alternates: yes + select_chain: + # All intermediates have the same subject, so always the first + # chain will be found, and we need a second condition to make sure + # that the first condition actually works. (The second condition + # has been tested above.) + - test_certificates: all + subject: "{{ acme_intermediates[0].subject }}" + - test_certificates: all + issuer: "{{ acme_roots[2].subject }}" +- name: Store obtain results for cert 2 + set_fact: + cert_2_obtain_results: "{{ certificate_obtain_result }}" + cert_2_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" +- name: Obtain cert 3 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 3 + certificate_name: cert-3 + key_type: ec384 + subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com" + subject_alt_name_critical: no + account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}" + challenge: dns-01 + modify_account: no + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: no + account_email: "" + acme_expected_root_number: 0 + retrieve_all_alternates: yes + select_chain: + - test_certificates: last + subject: "{{ acme_roots[1].subject }}" +- name: Store obtain results for cert 3 + set_fact: + cert_3_obtain_results: "{{ certificate_obtain_result }}" + cert_3_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" +- name: Obtain cert 4 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 4 + certificate_name: cert-4 + key_type: rsa + rsa_bits: 2048 + subject_alt_name: "DNS:example.com,DNS:t1.example.com,DNS:test.t2.example.com,DNS:example.org,DNS:test.example.org" + subject_alt_name_critical: no + account_key: account-rsa2048 + challenge: http-01 + modify_account: no + deactivate_authzs: yes + force: yes + remaining_days: 10 + terms_agreed: no + account_email: "" + acme_expected_root_number: 2 + select_chain: + - test_certificates: last + issuer: "{{ acme_roots[2].subject }}" + - test_certificates: last + issuer: "{{ acme_roots[1].subject }}" +- name: Store obtain results for cert 4 + set_fact: + cert_4_obtain_results: "{{ certificate_obtain_result }}" + cert_4_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}" +- name: Obtain cert 5 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 5, Iteration 1/4 + certificate_name: cert-5 + key_type: ec521 + subject_alt_name: "DNS:t2.example.com" + subject_alt_name_critical: no + account_key: account-ec384 + challenge: http-01 + modify_account: no + deactivate_authzs: yes + force: yes + remaining_days: 10 + terms_agreed: no + account_email: "" +- name: Store obtain results for cert 5a + set_fact: + cert_5a_obtain_results: "{{ certificate_obtain_result }}" + cert_5_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" +- name: Obtain cert 5 (should not, since already there and valid for more than 10 days) + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 5, Iteration 2/4 + certificate_name: cert-5 + key_type: ec521 + subject_alt_name: "DNS:t2.example.com" + subject_alt_name_critical: no + account_key: account-ec384 + challenge: http-01 + modify_account: no + deactivate_authzs: yes + force: no + remaining_days: 10 + terms_agreed: no + account_email: "" +- name: Store obtain results for cert 5b + set_fact: + cert_5_recreate_1: "{{ challenge_data is changed }}" +- name: Obtain cert 5 (should again by less days) + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 5, Iteration 3/4 + certificate_name: cert-5 + key_type: ec521 + subject_alt_name: "DNS:t2.example.com" + subject_alt_name_critical: no + account_key: account-ec384 + challenge: http-01 + modify_account: no + deactivate_authzs: yes + force: yes + remaining_days: 1000 + terms_agreed: no + account_email: "" +- name: Store obtain results for cert 5c + set_fact: + cert_5_recreate_2: "{{ challenge_data is changed }}" + cert_5c_obtain_results: "{{ certificate_obtain_result }}" +- name: Obtain cert 5 (should again by force) + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 5, Iteration 4/4 + certificate_name: cert-5 + key_type: ec521 + subject_alt_name: "DNS:t2.example.com" + subject_alt_name_critical: no + account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}" + challenge: http-01 + modify_account: no + deactivate_authzs: yes + force: yes + remaining_days: 10 + terms_agreed: no + account_email: "" +- name: Store obtain results for cert 5d + set_fact: + cert_5_recreate_3: "{{ challenge_data is changed }}" + cert_5d_obtain_results: "{{ certificate_obtain_result }}" +- name: Obtain cert 6 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 6 + certificate_name: cert-6 + key_type: rsa + rsa_bits: 2048 + subject_alt_name: "DNS:example.org" + subject_alt_name_critical: no + account_key: account-ec256 + challenge: tls-alpn-01 + modify_account: yes + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" + acme_expected_root_number: 0 + select_chain: + # All intermediates have the same subject key identifier, so always + # the first chain will be found, and we need a second condition to + # make sure that the first condition actually works. (The second + # condition has been tested above.) + - test_certificates: last + subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}" + - test_certificates: last + issuer: "{{ acme_roots[1].subject }}" +- name: Store obtain results for cert 6 + set_fact: + cert_6_obtain_results: "{{ certificate_obtain_result }}" + cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" +- name: Obtain cert 7 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 7 + certificate_name: cert-7 + key_type: rsa + rsa_bits: 2048 + subject_alt_name: + - "IP:127.0.0.1" + # - "IP:::1" + subject_alt_name_critical: no + account_key: account-ec256 + challenge: http-01 + modify_account: yes + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" + acme_expected_root_number: 2 + select_chain: + - test_certificates: last + authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}" +- name: Store obtain results for cert 7 + set_fact: + cert_7_obtain_results: "{{ certificate_obtain_result }}" + cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}" +- name: Obtain cert 8 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 8 + certificate_name: cert-8 + key_type: rsa + rsa_bits: 2048 + subject_alt_name: + - "IP:127.0.0.1" + # IPv4 only since our test validation server doesn't work + # with IPv6 (thanks to Python's socketserver). + subject_alt_name_critical: no + account_key: account-ec256 + challenge: tls-alpn-01 + challenge_alpn_tls: acme_challenge_cert_helper + modify_account: yes + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" +- name: Store obtain results for cert 8 + set_fact: + cert_8_obtain_results: "{{ certificate_obtain_result }}" + cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" +## DISSECT CERTIFICATES ####################################################################### +# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate. +- name: Verifying cert 1 + command: openssl verify -CAfile "{{ output_dir }}/cert-1-root.pem" -untrusted "{{ output_dir }}/cert-1-chain.pem" "{{ output_dir }}/cert-1.pem" + ignore_errors: yes + register: cert_1_valid +- name: Verifying cert 2 + command: openssl verify -CAfile "{{ output_dir }}/cert-2-root.pem" -untrusted "{{ output_dir }}/cert-2-chain.pem" "{{ output_dir }}/cert-2.pem" + ignore_errors: yes + register: cert_2_valid +- name: Verifying cert 3 + command: openssl verify -CAfile "{{ output_dir }}/cert-3-root.pem" -untrusted "{{ output_dir }}/cert-3-chain.pem" "{{ output_dir }}/cert-3.pem" + ignore_errors: yes + register: cert_3_valid +- name: Verifying cert 4 + command: openssl verify -CAfile "{{ output_dir }}/cert-4-root.pem" -untrusted "{{ output_dir }}/cert-4-chain.pem" "{{ output_dir }}/cert-4.pem" + ignore_errors: yes + register: cert_4_valid +- name: Verifying cert 5 + command: openssl verify -CAfile "{{ output_dir }}/cert-5-root.pem" -untrusted "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem" + ignore_errors: yes + register: cert_5_valid +- name: Verifying cert 6 + command: openssl verify -CAfile "{{ output_dir }}/cert-6-root.pem" -untrusted "{{ output_dir }}/cert-6-chain.pem" "{{ output_dir }}/cert-6.pem" + ignore_errors: yes + register: cert_6_valid +- name: Verifying cert 7 + command: openssl verify -CAfile "{{ output_dir }}/cert-7-root.pem" -untrusted "{{ output_dir }}/cert-7-chain.pem" "{{ output_dir }}/cert-7.pem" + ignore_errors: yes + register: cert_7_valid +- name: Verifying cert 8 + command: openssl verify -CAfile "{{ output_dir }}/cert-8-root.pem" -untrusted "{{ output_dir }}/cert-8-chain.pem" "{{ output_dir }}/cert-8.pem" + ignore_errors: yes + register: cert_8_valid +# Dump certificate info +- name: Dumping cert 1 + command: openssl x509 -in "{{ output_dir }}/cert-1.pem" -noout -text + register: cert_1_text +- name: Dumping cert 2 + command: openssl x509 -in "{{ output_dir }}/cert-2.pem" -noout -text + register: cert_2_text +- name: Dumping cert 3 + command: openssl x509 -in "{{ output_dir }}/cert-3.pem" -noout -text + register: cert_3_text +- name: Dumping cert 4 + command: openssl x509 -in "{{ output_dir }}/cert-4.pem" -noout -text + register: cert_4_text +- name: Dumping cert 5 + command: openssl x509 -in "{{ output_dir }}/cert-5.pem" -noout -text + register: cert_5_text +- name: Dumping cert 6 + command: openssl x509 -in "{{ output_dir }}/cert-6.pem" -noout -text + register: cert_6_text +- name: Dumping cert 7 + command: openssl x509 -in "{{ output_dir }}/cert-7.pem" -noout -text + register: cert_7_text +- name: Dumping cert 8 + command: openssl x509 -in "{{ output_dir }}/cert-8.pem" -noout -text + register: cert_8_text +# Dump certificate info +- name: Dumping cert 1 + openssl_certificate_info: + path: "{{ output_dir }}/cert-1.pem" + register: cert_1_info +- name: Dumping cert 2 + openssl_certificate_info: + path: "{{ output_dir }}/cert-2.pem" + register: cert_2_info +- name: Dumping cert 3 + openssl_certificate_info: + path: "{{ output_dir }}/cert-3.pem" + register: cert_3_info +- name: Dumping cert 4 + openssl_certificate_info: + path: "{{ output_dir }}/cert-4.pem" + register: cert_4_info +- name: Dumping cert 5 + openssl_certificate_info: + path: "{{ output_dir }}/cert-5.pem" + register: cert_5_info +- name: Dumping cert 6 + openssl_certificate_info: + path: "{{ output_dir }}/cert-6.pem" + register: cert_6_info +- name: Dumping cert 7 + openssl_certificate_info: + path: "{{ output_dir }}/cert-7.pem" + register: cert_7_info +- name: Dumping cert 8 + openssl_certificate_info: + path: "{{ output_dir }}/cert-8.pem" + register: cert_8_info +## GET ACCOUNT ORDERS ######################################################################### +- name: Don't retrieve orders + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: ignore + register: account_orders_not +- name: Retrieve orders as URL list (1/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: url_list + register: account_orders_urls +- name: Retrieve orders as URL list (2/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec384.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: url_list + register: account_orders_urls2 +- name: Retrieve orders as object list (1/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: object_list + register: account_orders_full +- name: Retrieve orders as object list (2/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec384.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: object_list + register: account_orders_full2 diff --git a/tests/integration/targets/acme_certificate/tasks/main.yml b/tests/integration/targets/acme_certificate/tasks/main.yml new file mode 100644 index 00000000..da66f5f3 --- /dev/null +++ b/tests/integration/targets/acme_certificate/tasks/main.yml @@ -0,0 +1,102 @@ +--- +- block: + - name: Obtain root and intermediate certificates + get_url: + url: "http://{{ acme_host }}:5000/{{ item.0 }}-certificate-for-ca/{{ item.1 }}" + dest: "{{ output_dir }}/acme-{{ item.0 }}-{{ item.1 }}.pem" + loop: "{{ query('nested', types, root_numbers) }}" + + - name: Analyze root certificates + openssl_certificate_info: + path: "{{ output_dir }}/acme-root-{{ item }}.pem" + loop: "{{ root_numbers }}" + register: acme_roots + + - name: Analyze intermediate certificates + openssl_certificate_info: + path: "{{ output_dir }}/acme-intermediate-{{ item }}.pem" + loop: "{{ root_numbers }}" + register: acme_intermediates + + - set_fact: + x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}" + y__: "{{ lookup('file', output_dir ~ '/acme-root-' ~ item.item ~ '.pem', rstrip=False) }}" + loop: "{{ acme_roots.results }}" + register: acme_roots_tmp + + - set_fact: + x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}" + y__: "{{ lookup('file', output_dir ~ '/acme-intermediate-' ~ item.item ~ '.pem', rstrip=False) }}" + loop: "{{ acme_intermediates.results }}" + register: acme_intermediates_tmp + + - set_fact: + acme_roots: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.x__') | list }}" + acme_root_certs: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.y__') | list }}" + acme_intermediates: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.x__') | list }}" + acme_intermediate_certs: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.y__') | list }}" + + vars: + types: + - root + - intermediate + root_numbers: + # The number 3 comes from here: https://github.com/ansible/acme-test-container/blob/master/run.sh#L12 + - 0 + - 1 + - 2 + - 3 + interesting_keys: + - authority_key_identifier + - subject_key_identifier + - issuer + - subject + #- serial_number + #- public_key_fingerprints + +- name: ACME root certificate info + debug: + var: acme_roots + +#- name: ACME root certificates as PEM +# debug: +# var: acme_root_certs + +- name: ACME intermediate certificate info + debug: + var: acme_intermediates + +#- name: ACME intermediate certificates as PEM +# debug: +# var: acme_intermediate_certs + +- block: + - name: Running tests with OpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: openssl + + - import_tasks: ../tests/validate.yml + + # Old 0.9.8 versions have insufficient CLI support for signing with EC keys + when: openssl_version.stdout is version('1.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/tests/integration/targets/acme_certificate/tasks/obtain-cert.yml b/tests/integration/targets/acme_certificate/tasks/obtain-cert.yml new file mode 120000 index 00000000..532df945 --- /dev/null +++ b/tests/integration/targets/acme_certificate/tasks/obtain-cert.yml @@ -0,0 +1 @@ +../../setup_acme/tasks/obtain-cert.yml \ No newline at end of file diff --git a/tests/integration/targets/acme_certificate/tests/validate.yml b/tests/integration/targets/acme_certificate/tests/validate.yml new file mode 100644 index 00000000..cd7d28b0 --- /dev/null +++ b/tests/integration/targets/acme_certificate/tests/validate.yml @@ -0,0 +1,144 @@ +--- +- name: Check that certificate 1 is valid + assert: + that: + - cert_1_valid is not failed +- name: Check that certificate 1 contains correct SANs + assert: + that: + - "'DNS:example.com' in cert_1_text.stdout" +- name: Check that certificate 1 retrieval got all chains + assert: + that: + - "'all_chains' in cert_1_obtain_results" + - "cert_1_obtain_results.all_chains | length > 1" + - "'cert' in cert_1_obtain_results.all_chains[cert_1_alternate | int]" + - "'chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]" + - "'full_chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]" + - "lookup('file', output_dir ~ '/cert-1.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].cert" + - "lookup('file', output_dir ~ '/cert-1-chain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].chain" + - "lookup('file', output_dir ~ '/cert-1-fullchain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].full_chain" + +- name: Check that certificate 2 is valid + assert: + that: + - cert_2_valid is not failed +- name: Check that certificate 2 contains correct SANs + assert: + that: + - "'DNS:*.example.com' in cert_2_text.stdout" + - "'DNS:example.com' in cert_2_text.stdout" +- name: Check that certificate 1 retrieval got all chains + assert: + that: + - "'all_chains' in cert_2_obtain_results" + - "cert_2_obtain_results.all_chains | length > 1" + - "'cert' in cert_2_obtain_results.all_chains[cert_2_alternate | int]" + - "'chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]" + - "'full_chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]" + - "lookup('file', output_dir ~ '/cert-2.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].cert" + - "lookup('file', output_dir ~ '/cert-2-chain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].chain" + - "lookup('file', output_dir ~ '/cert-2-fullchain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].full_chain" + +- name: Check that certificate 3 is valid + assert: + that: + - cert_3_valid is not failed +- name: Check that certificate 3 contains correct SANs + assert: + that: + - "'DNS:*.example.com' in cert_3_text.stdout" + - "'DNS:example.org' in cert_3_text.stdout" + - "'DNS:t1.example.com' in cert_3_text.stdout" +- name: Check that certificate 1 retrieval got all chains + assert: + that: + - "'all_chains' in cert_3_obtain_results" + - "cert_3_obtain_results.all_chains | length > 1" + - "'cert' in cert_3_obtain_results.all_chains[cert_3_alternate | int]" + - "'chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]" + - "'full_chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]" + - "lookup('file', output_dir ~ '/cert-3.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].cert" + - "lookup('file', output_dir ~ '/cert-3-chain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].chain" + - "lookup('file', output_dir ~ '/cert-3-fullchain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].full_chain" + +- name: Check that certificate 4 is valid + assert: + that: + - cert_4_valid is not failed +- name: Check that certificate 4 contains correct SANs + assert: + that: + - "'DNS:example.com' in cert_4_text.stdout" + - "'DNS:t1.example.com' in cert_4_text.stdout" + - "'DNS:test.t2.example.com' in cert_4_text.stdout" + - "'DNS:example.org' in cert_4_text.stdout" + - "'DNS:test.example.org' in cert_4_text.stdout" +- name: Check that certificate 4 retrieval did not get all chains + assert: + that: + - "'all_chains' not in cert_4_obtain_results" + +- name: Check that certificate 5 is valid + assert: + that: + - cert_5_valid is not failed +- name: Check that certificate 5 contains correct SANs + assert: + that: + - "'DNS:t2.example.com' in cert_5_text.stdout" +- name: Check that certificate 5 was not recreated on the first try + assert: + that: + - cert_5_recreate_1 == False +- name: Check that certificate 5 was recreated on the second try + assert: + that: + - cert_5_recreate_2 == True +- name: Check that certificate 5 was recreated on the third try + assert: + that: + - cert_5_recreate_3 == True + +- name: Check that certificate 6 is valid + assert: + that: + - cert_6_valid is not failed +- name: Check that certificate 6 contains correct SANs + assert: + that: + - "'DNS:example.org' in cert_6_text.stdout" + +- name: Validate that orders were not retrieved + assert: + that: + - "'account' in account_orders_not" + - "'orders' not in account_orders_not" + +- name: Validate that orders were retrieved as list of URLs (1/2) + assert: + that: + - "'account' in account_orders_urls" + - "'orders' in account_orders_urls" + - "account_orders_urls.orders[0] is string" + +- name: Validate that orders were retrieved as list of URLs (2/2) + assert: + that: + - "'account' in account_orders_urls2" + - "'orders' in account_orders_urls2" + - "account_orders_urls2.orders[0] is string" + +- name: Validate that orders were retrieved as list of objects (1/2) + assert: + that: + - "'account' in account_orders_full" + - "'orders' in account_orders_full" + - "account_orders_full.orders[0].status is string" + +- name: Validate that orders were retrieved as list of objects (2/2) + assert: + that: + - "'account' in account_orders_full2" + - "'orders' in account_orders_full2" + - "account_orders_full2.orders[0].status is string" diff --git a/tests/integration/targets/acme_certificate_revoke/aliases b/tests/integration/targets/acme_certificate_revoke/aliases new file mode 100644 index 00000000..d7936330 --- /dev/null +++ b/tests/integration/targets/acme_certificate_revoke/aliases @@ -0,0 +1,2 @@ +shippable/cloud/group1 +cloud/acme diff --git a/tests/integration/targets/acme_certificate_revoke/meta/main.yml b/tests/integration/targets/acme_certificate_revoke/meta/main.yml new file mode 100644 index 00000000..81d1e7e7 --- /dev/null +++ b/tests/integration/targets/acme_certificate_revoke/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_acme diff --git a/tests/integration/targets/acme_certificate_revoke/tasks/impl.yml b/tests/integration/targets/acme_certificate_revoke/tasks/impl.yml new file mode 100644 index 00000000..69545b69 --- /dev/null +++ b/tests/integration/targets/acme_certificate_revoke/tasks/impl.yml @@ -0,0 +1,89 @@ +--- +## SET UP ACCOUNT KEYS ######################################################################## +- name: Create ECC256 account key + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem +- name: Create ECC384 account key + command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem +- name: Create RSA-2048 account key + command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048 +## CREATE ACCOUNTS AND OBTAIN CERTIFICATES #################################################### +- name: Obtain cert 1 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 1 for revocation + certificate_name: cert-1 + key_type: rsa + rsa_bits: 2048 + subject_alt_name: "DNS:example.com" + subject_alt_name_critical: no + account_key_content: "{{ lookup('file', output_dir ~ '/account-ec256.pem') }}" + challenge: http-01 + modify_account: yes + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" +- name: Obtain cert 2 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 2 for revocation + certificate_name: cert-2 + key_type: ec256 + subject_alt_name: "DNS:*.example.com" + subject_alt_name_critical: yes + account_key: account-ec384 + challenge: dns-01 + modify_account: yes + deactivate_authzs: yes + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" +- name: Obtain cert 3 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 3 for revocation + certificate_name: cert-3 + key_type: ec384 + subject_alt_name: "DNS:t1.example.com" + subject_alt_name_critical: no + account_key: account-rsa2048 + challenge: dns-01 + modify_account: yes + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" +## REVOKE CERTIFICATES ######################################################################## +- name: Revoke certificate 1 via account key + acme_certificate_revoke: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec256.pem" + certificate: "{{ output_dir }}/cert-1.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + ignore_errors: yes + register: cert_1_revoke +- name: Revoke certificate 2 via certificate private key + acme_certificate_revoke: + select_crypto_backend: "{{ select_crypto_backend }}" + private_key_src: "{{ output_dir }}/cert-2.key" + certificate: "{{ output_dir }}/cert-2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + ignore_errors: yes + register: cert_2_revoke +- name: Revoke certificate 3 via account key (fullchain) + acme_certificate_revoke: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}" + certificate: "{{ output_dir }}/cert-3-fullchain.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + ignore_errors: yes + register: cert_3_revoke diff --git a/tests/integration/targets/acme_certificate_revoke/tasks/main.yml b/tests/integration/targets/acme_certificate_revoke/tasks/main.yml new file mode 100644 index 00000000..e46c6dc4 --- /dev/null +++ b/tests/integration/targets/acme_certificate_revoke/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- block: + - name: Running tests with OpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: openssl + + - import_tasks: ../tests/validate.yml + + # Old 0.9.8 versions have insufficient CLI support for signing with EC keys + when: openssl_version.stdout is version('1.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/tests/integration/targets/acme_certificate_revoke/tasks/obtain-cert.yml b/tests/integration/targets/acme_certificate_revoke/tasks/obtain-cert.yml new file mode 120000 index 00000000..532df945 --- /dev/null +++ b/tests/integration/targets/acme_certificate_revoke/tasks/obtain-cert.yml @@ -0,0 +1 @@ +../../setup_acme/tasks/obtain-cert.yml \ No newline at end of file diff --git a/tests/integration/targets/acme_certificate_revoke/tests/validate.yml b/tests/integration/targets/acme_certificate_revoke/tests/validate.yml new file mode 100644 index 00000000..7f8f9c54 --- /dev/null +++ b/tests/integration/targets/acme_certificate_revoke/tests/validate.yml @@ -0,0 +1,16 @@ +--- +- name: Check that certificate 1 was revoked + assert: + that: + - cert_1_revoke is changed + - cert_1_revoke is not failed +- name: Check that certificate 2 was revoked + assert: + that: + - cert_2_revoke is changed + - cert_2_revoke is not failed +- name: Check that certificate 3 was revoked + assert: + that: + - cert_3_revoke is changed + - cert_3_revoke is not failed diff --git a/tests/integration/targets/acme_challenge_cert_helper/aliases b/tests/integration/targets/acme_challenge_cert_helper/aliases new file mode 100644 index 00000000..d7936330 --- /dev/null +++ b/tests/integration/targets/acme_challenge_cert_helper/aliases @@ -0,0 +1,2 @@ +shippable/cloud/group1 +cloud/acme diff --git a/tests/integration/targets/acme_challenge_cert_helper/meta/main.yml b/tests/integration/targets/acme_challenge_cert_helper/meta/main.yml new file mode 100644 index 00000000..81d1e7e7 --- /dev/null +++ b/tests/integration/targets/acme_challenge_cert_helper/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_acme diff --git a/tests/integration/targets/acme_challenge_cert_helper/tasks/main.yml b/tests/integration/targets/acme_challenge_cert_helper/tasks/main.yml new file mode 100644 index 00000000..85748563 --- /dev/null +++ b/tests/integration/targets/acme_challenge_cert_helper/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- block: + - name: Create ECC256 account key + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem + - name: Obtain cert 1 + include_tasks: obtain-cert.yml + vars: + select_crypto_backend: auto + certgen_title: Certificate 1 + certificate_name: cert-1 + key_type: rsa + rsa_bits: 2048 + subject_alt_name: "DNS:example.com" + subject_alt_name_critical: no + account_key: account-ec256 + challenge: tls-alpn-01 + challenge_alpn_tls: acme_challenge_cert_helper + modify_account: yes + deactivate_authzs: no + force: no + remaining_days: 10 + terms_agreed: yes + account_email: "example@example.org" + + when: openssl_version.stdout is version('1.0.0', '>=') or cryptography_version.stdout is version('1.5', '>=') diff --git a/tests/integration/targets/acme_challenge_cert_helper/tasks/obtain-cert.yml b/tests/integration/targets/acme_challenge_cert_helper/tasks/obtain-cert.yml new file mode 120000 index 00000000..532df945 --- /dev/null +++ b/tests/integration/targets/acme_challenge_cert_helper/tasks/obtain-cert.yml @@ -0,0 +1 @@ +../../setup_acme/tasks/obtain-cert.yml \ No newline at end of file diff --git a/tests/integration/targets/acme_inspect/aliases b/tests/integration/targets/acme_inspect/aliases new file mode 100644 index 00000000..d7936330 --- /dev/null +++ b/tests/integration/targets/acme_inspect/aliases @@ -0,0 +1,2 @@ +shippable/cloud/group1 +cloud/acme diff --git a/tests/integration/targets/acme_inspect/meta/main.yml b/tests/integration/targets/acme_inspect/meta/main.yml new file mode 100644 index 00000000..81d1e7e7 --- /dev/null +++ b/tests/integration/targets/acme_inspect/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_acme diff --git a/tests/integration/targets/acme_inspect/tasks/impl.yml b/tests/integration/targets/acme_inspect/tasks/impl.yml new file mode 100644 index 00000000..6628548d --- /dev/null +++ b/tests/integration/targets/acme_inspect/tasks/impl.yml @@ -0,0 +1,151 @@ +--- +- name: Generate account key + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem + +- name: Parse account key (to ease debugging some test failures) + command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text + +- name: Get directory + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + method: directory-only + register: directory +- debug: var=directory + +- name: Create an account + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + url: "{{ directory.directory.newAccount}}" + method: post + content: '{"termsOfServiceAgreed":true}' + register: account_creation + # account_creation.headers.location contains the account URI + # if creation was successful +- debug: var=account_creation + +- name: Get account information + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ account_creation.headers.location }}" + method: get + register: account_get +- debug: var=account_get + +- name: Update account contacts + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ account_creation.headers.location }}" + method: post + content: '{{ account_info | to_json }}' + vars: + account_info: + # For valid values, see + # https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3 + contact: + - mailto:me@example.com + register: account_update +- debug: var=account_update + +- name: Create certificate order + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ directory.directory.newOrder }}" + method: post + content: '{{ create_order | to_json }}' + vars: + create_order: + # For valid values, see + # https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4 and + # https://www.rfc-editor.org/rfc/rfc8738.html + identifiers: + - type: dns + value: example.com + - type: dns + value: example.org + register: new_order +- debug: var=new_order + +- name: Get order information + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ new_order.headers.location }}" + method: get + register: order +- debug: var=order + +- name: Get authzs for order + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ item }}" + method: get + loop: "{{ order.output_json.authorizations }}" + register: authz +- debug: var=authz + +- name: Get HTTP-01 challenge for authz + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ (item.challenges | selectattr('type', 'equalto', 'http-01') | list)[0].url }}" + method: get + register: http01challenge + loop: "{{ authz.results | map(attribute='output_json') | list }}" +- debug: var=http01challenge + +- name: Activate HTTP-01 challenge manually + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ item.url }}" + method: post + content: '{}' + register: activation + loop: "{{ http01challenge.results | map(attribute='output_json') | list }}" +- debug: var=activation + +- name: Get HTTP-01 challenge results + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: no + account_key_src: "{{ output_dir }}/accountkey.pem" + account_uri: "{{ account_creation.headers.location }}" + url: "{{ item.url }}" + method: get + register: validation_result + loop: "{{ http01challenge.results | map(attribute='output_json') | list }}" + until: "validation_result.output_json.status != 'pending'" + retries: 20 + delay: 1 +- debug: var=validation_result diff --git a/tests/integration/targets/acme_inspect/tasks/main.yml b/tests/integration/targets/acme_inspect/tasks/main.yml new file mode 100644 index 00000000..e46c6dc4 --- /dev/null +++ b/tests/integration/targets/acme_inspect/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- block: + - name: Running tests with OpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: openssl + + - import_tasks: ../tests/validate.yml + + # Old 0.9.8 versions have insufficient CLI support for signing with EC keys + when: openssl_version.stdout is version('1.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/tests/integration/targets/acme_inspect/tests/validate.yml b/tests/integration/targets/acme_inspect/tests/validate.yml new file mode 100644 index 00000000..a45b512f --- /dev/null +++ b/tests/integration/targets/acme_inspect/tests/validate.yml @@ -0,0 +1,131 @@ +--- +- name: Check directory output + assert: + that: + - directory is not changed + - "'directory' in directory" + - "'newAccount' in directory.directory" + - "'newOrder' in directory.directory" + - "'newNonce' in directory.directory" + - "'headers' not in directory" + - "'output_text' not in directory" + - "'output_json' not in directory" + +- name: Check account creation output + assert: + that: + - account_creation is changed + - "'directory' in account_creation" + - "'headers' in account_creation" + - "'output_text' in account_creation" + - "'output_json' in account_creation" + - account_creation.headers.status == 201 + - "'location' in account_creation.headers" + - account_creation.output_json.status == 'valid' + - not (account_creation.output_json.contact | default([])) + - account_creation.output_text | from_json == account_creation.output_json + +- name: Check account get output + assert: + that: + - account_get is not changed + - "'directory' in account_get" + - "'headers' in account_get" + - "'output_text' in account_get" + - "'output_json' in account_get" + - account_get.headers.status == 200 + - account_get.output_json == account_creation.output_json + +- name: Check account update output + assert: + that: + - account_update is changed + - "'directory' in account_update" + - "'headers' in account_update" + - "'output_text' in account_update" + - "'output_json' in account_update" + - account_update.output_json.status == 'valid' + - account_update.output_json.contact | length == 1 + - account_update.output_json.contact[0] == 'mailto:me@example.com' + +- name: Check certificate request output + assert: + that: + - new_order is changed + - "'directory' in new_order" + - "'headers' in new_order" + - "'output_text' in new_order" + - "'output_json' in new_order" + - new_order.output_json.authorizations | length == 2 + - new_order.output_json.identifiers | length == 2 + - new_order.output_json.status == 'pending' + - "'finalize' in new_order.output_json" + +- name: Check get order output + assert: + that: + - order is not changed + - "'directory' in order" + - "'headers' in order" + - "'output_text' in order" + - "'output_json' in order" + # The order of identifiers and authorizations is randomized! + # - new_order.output_json == order.output_json + +- name: Check get authz output + assert: + that: + - item is not changed + - "'directory' in item" + - "'headers' in item" + - "'output_text' in item" + - "'output_json' in item" + - item.output_json.challenges | length >= 3 + - item.output_json.identifier.type == 'dns' + - item.output_json.status == 'pending' + loop: "{{ authz.results }}" + +- name: Check get challenge output + assert: + that: + - item is not changed + - "'directory' in item" + - "'headers' in item" + - "'output_text' in item" + - "'output_json' in item" + - item.output_json.status == 'pending' + - item.output_json.type == 'http-01' + - item.output_json.url == item.invocation.module_args.url + - "'token' in item.output_json" + loop: "{{ http01challenge.results }}" + +- name: Check challenge activation output + assert: + that: + - item is changed + - "'directory' in item" + - "'headers' in item" + - "'output_text' in item" + - "'output_json' in item" + - item.output_json.status == 'pending' + - item.output_json.type == 'http-01' + - item.output_json.url == item.invocation.module_args.url + - "'token' in item.output_json" + loop: "{{ activation.results }}" + +- name: Check validation result + assert: + that: + - item is not changed + - "'directory' in item" + - "'headers' in item" + - "'output_text' in item" + - "'output_json' in item" + - item.output_json.status == 'invalid' + - item.output_json.type == 'http-01' + - item.output_json.url == item.invocation.module_args.url + - "'token' in item.output_json" + - "'validated' in item.output_json" + - "'error' in item.output_json" + - item.output_json.error.type == 'urn:ietf:params:acme:error:unauthorized' + loop: "{{ validation_result.results }}" diff --git a/tests/integration/targets/certificate_complete_chain/aliases b/tests/integration/targets/certificate_complete_chain/aliases new file mode 100644 index 00000000..f8e28c7e --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/aliases @@ -0,0 +1,2 @@ +shippable/posix/group1 +skip/aix diff --git a/tests/integration/targets/certificate_complete_chain/files/cert1-chain.pem b/tests/integration/targets/certificate_complete_chain/files/cert1-chain.pem new file mode 100644 index 00000000..3e0f6c08 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert1-chain.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnzCCAyWgAwIBAgIQWyXOaQfEJlVm0zkMmalUrTAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwOTI1MDAw +MDAwWhcNMjkwOTI0MjM1OTU5WjCBkjELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxODA2BgNVBAMTL0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlk +YXRpb24gU2VjdXJlIFNlcnZlciBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEAjgZgTrJaYRwWQKOqIofMN+83gP8eR06JSxrQSEYgur5PkrkM8wSzypD/A7y +ZADA4SVQgiTNtkk4DyVHkUikraOCAWYwggFiMB8GA1UdIwQYMBaAFHVxpxlIGbyd +nepBR9+UxEh3mdN5MB0GA1UdDgQWBBRACWFn8LyDcU/eEggsb9TUK3Y9ljAOBgNV +HQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAECATBMBgNV +HR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9FQ0ND +ZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDByBggrBgEFBQcBAQRmMGQwOwYIKwYB +BQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET0VDQ0FkZFRydXN0 +Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5jb21vZG9jYTQuY29tMAoG +CCqGSM49BAMDA2gAMGUCMQCsaEclgBNPE1bAojcJl1pQxOfttGHLKIoKETKm4nHf +EQGJbwd6IGZrGNC5LkP3Um8CMBKFfI4TZpIEuppFCZRKMGHRSdxv6+ctyYnPHmp8 +7IXOMCVZuoFwNLg0f+cB0eLLUg== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert1-fullchain.pem b/tests/integration/targets/certificate_complete_chain/files/cert1-fullchain.pem new file mode 100644 index 00000000..e12a7ca8 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert1-fullchain.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIFBTCCBKugAwIBAgIQL+c9oQXpvdcOD3BKAncbgDAKBggqhkjOPQQDAjCBkjEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxODA2BgNVBAMT +L0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQSAy +MB4XDTE4MDcxMTAwMDAwMFoXDTE5MDExNzIzNTk1OVowbDEhMB8GA1UECxMYRG9t +YWluIENvbnRyb2wgVmFsaWRhdGVkMSEwHwYDVQQLExhQb3NpdGl2ZVNTTCBNdWx0 +aS1Eb21haW4xJDAiBgNVBAMTG3NzbDgwMzAyNS5jbG91ZGZsYXJlc3NsLmNvbTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABMap9sMZnCzTXID1chTOmtOk8p6+SHbG +3fmyJJljI7sN9RddlLKar9VBS48WguVv1R6trvERIYj8TzKCVBzu9mmjggMGMIID +AjAfBgNVHSMEGDAWgBRACWFn8LyDcU/eEggsb9TUK3Y9ljAdBgNVHQ4EFgQUd/6a +t8j7v5DsL7xWacf8VyzOLJcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAw +HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCME8GA1UdIARIMEYwOgYLKwYB +BAGyMQECAgcwKzApBggrBgEFBQcCARYdaHR0cHM6Ly9zZWN1cmUuY29tb2RvLmNv +bS9DUFMwCAYGZ4EMAQIBMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwuY29t +b2RvY2E0LmNvbS9DT01PRE9FQ0NEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVy +Q0EyLmNybDCBiAYIKwYBBQUHAQEEfDB6MFEGCCsGAQUFBzAChkVodHRwOi8vY3J0 +LmNvbW9kb2NhNC5jb20vQ09NT0RPRUNDRG9tYWluVmFsaWRhdGlvblNlY3VyZVNl +cnZlckNBMi5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLmNvbW9kb2NhNC5j +b20wSAYDVR0RBEEwP4Ibc3NsODAzMDI1LmNsb3VkZmxhcmVzc2wuY29tghAqLmhz +Y29zY2RuNDAubmV0gg5oc2Nvc2NkbjQwLm5ldDCCAQMGCisGAQQB1nkCBAIEgfQE +gfEA7wB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAABZIbVA88A +AAQDAEcwRQIhANtN489Izy3iss/eF8rUw/gir8rqyA2t3lpxnco+J2NlAiBBku5M +iGD8whW5/31byPj0/ype1MmG0QYrq3qWvYiQ3QB1AHR+2oMxrTMQkSGcziVPQnDC +v/1eQiAIxjc1eeYQe8xWAAABZIbVBB4AAAQDAEYwRAIgSjcL7B4cbgm2XED69G7/ +iFPe2zkWhxnkgGISSwuXw1gCICzwPmfbjEfwDNXEuBs7JXkPRaT1pi7hZ9aR5wJJ +TKH9MAoGCCqGSM49BAMCA0gAMEUCIQDqxmFLcme3Ldd+jiMQf7fT5pSezZfMOL0S +cNmfGvNtPQIgec3sO/ylnnaztCy5KDjYsnh+rm01bxs+nz2DnOPF+xo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDnzCCAyWgAwIBAgIQWyXOaQfEJlVm0zkMmalUrTAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwOTI1MDAw +MDAwWhcNMjkwOTI0MjM1OTU5WjCBkjELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxODA2BgNVBAMTL0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlk +YXRpb24gU2VjdXJlIFNlcnZlciBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEAjgZgTrJaYRwWQKOqIofMN+83gP8eR06JSxrQSEYgur5PkrkM8wSzypD/A7y +ZADA4SVQgiTNtkk4DyVHkUikraOCAWYwggFiMB8GA1UdIwQYMBaAFHVxpxlIGbyd +nepBR9+UxEh3mdN5MB0GA1UdDgQWBBRACWFn8LyDcU/eEggsb9TUK3Y9ljAOBgNV +HQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAECATBMBgNV +HR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9FQ0ND +ZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDByBggrBgEFBQcBAQRmMGQwOwYIKwYB +BQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET0VDQ0FkZFRydXN0 +Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5jb21vZG9jYTQuY29tMAoG +CCqGSM49BAMDA2gAMGUCMQCsaEclgBNPE1bAojcJl1pQxOfttGHLKIoKETKm4nHf +EQGJbwd6IGZrGNC5LkP3Um8CMBKFfI4TZpIEuppFCZRKMGHRSdxv6+ctyYnPHmp8 +7IXOMCVZuoFwNLg0f+cB0eLLUg== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert1-root.pem b/tests/integration/targets/certificate_complete_chain/files/cert1-root.pem new file mode 100644 index 00000000..546c95e3 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert1-root.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert1.pem b/tests/integration/targets/certificate_complete_chain/files/cert1.pem new file mode 100644 index 00000000..d00d252d --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert1.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFBTCCBKugAwIBAgIQL+c9oQXpvdcOD3BKAncbgDAKBggqhkjOPQQDAjCBkjEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxODA2BgNVBAMT +L0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQSAy +MB4XDTE4MDcxMTAwMDAwMFoXDTE5MDExNzIzNTk1OVowbDEhMB8GA1UECxMYRG9t +YWluIENvbnRyb2wgVmFsaWRhdGVkMSEwHwYDVQQLExhQb3NpdGl2ZVNTTCBNdWx0 +aS1Eb21haW4xJDAiBgNVBAMTG3NzbDgwMzAyNS5jbG91ZGZsYXJlc3NsLmNvbTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABMap9sMZnCzTXID1chTOmtOk8p6+SHbG +3fmyJJljI7sN9RddlLKar9VBS48WguVv1R6trvERIYj8TzKCVBzu9mmjggMGMIID +AjAfBgNVHSMEGDAWgBRACWFn8LyDcU/eEggsb9TUK3Y9ljAdBgNVHQ4EFgQUd/6a +t8j7v5DsL7xWacf8VyzOLJcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAw +HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCME8GA1UdIARIMEYwOgYLKwYB +BAGyMQECAgcwKzApBggrBgEFBQcCARYdaHR0cHM6Ly9zZWN1cmUuY29tb2RvLmNv +bS9DUFMwCAYGZ4EMAQIBMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwuY29t +b2RvY2E0LmNvbS9DT01PRE9FQ0NEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVy +Q0EyLmNybDCBiAYIKwYBBQUHAQEEfDB6MFEGCCsGAQUFBzAChkVodHRwOi8vY3J0 +LmNvbW9kb2NhNC5jb20vQ09NT0RPRUNDRG9tYWluVmFsaWRhdGlvblNlY3VyZVNl +cnZlckNBMi5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLmNvbW9kb2NhNC5j +b20wSAYDVR0RBEEwP4Ibc3NsODAzMDI1LmNsb3VkZmxhcmVzc2wuY29tghAqLmhz +Y29zY2RuNDAubmV0gg5oc2Nvc2NkbjQwLm5ldDCCAQMGCisGAQQB1nkCBAIEgfQE +gfEA7wB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAABZIbVA88A +AAQDAEcwRQIhANtN489Izy3iss/eF8rUw/gir8rqyA2t3lpxnco+J2NlAiBBku5M +iGD8whW5/31byPj0/ype1MmG0QYrq3qWvYiQ3QB1AHR+2oMxrTMQkSGcziVPQnDC +v/1eQiAIxjc1eeYQe8xWAAABZIbVBB4AAAQDAEYwRAIgSjcL7B4cbgm2XED69G7/ +iFPe2zkWhxnkgGISSwuXw1gCICzwPmfbjEfwDNXEuBs7JXkPRaT1pi7hZ9aR5wJJ +TKH9MAoGCCqGSM49BAMCA0gAMEUCIQDqxmFLcme3Ldd+jiMQf7fT5pSezZfMOL0S +cNmfGvNtPQIgec3sO/ylnnaztCy5KDjYsnh+rm01bxs+nz2DnOPF+xo= +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert2-altchain.pem b/tests/integration/targets/certificate_complete_chain/files/cert2-altchain.pem new file mode 100644 index 00000000..4e82cb56 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert2-altchain.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1 +WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX +NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf +89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl +Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc +Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz +uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB +AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU +BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB +FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo +SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js +LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF +BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG +AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD +VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB +ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx +A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM +UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2 +DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1 +eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu +OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw +p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY +2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0 +ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR +PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b +rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert2-altroot.pem b/tests/integration/targets/certificate_complete_chain/files/cert2-altroot.pem new file mode 100644 index 00000000..b85c8037 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert2-altroot.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert2-chain.pem b/tests/integration/targets/certificate_complete_chain/files/cert2-chain.pem new file mode 100644 index 00000000..0002462c --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert2-chain.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow +SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT +GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF +q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 +SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 +Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA +a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj +/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG +CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv +bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k +c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw +VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC +ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz +MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu +Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF +AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo +uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ +wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu +X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG +PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 +KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert2-fullchain.pem b/tests/integration/targets/certificate_complete_chain/files/cert2-fullchain.pem new file mode 100644 index 00000000..cf75a331 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert2-fullchain.pem @@ -0,0 +1,72 @@ +-----BEGIN CERTIFICATE----- +MIIH5jCCBs6gAwIBAgISA2gSCm/BtvCR2e2bIap5YbXaMA0GCSqGSIb3DQEBCwUA +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODA3MjcxNzMxMjdaFw0x +ODEwMjUxNzMxMjdaMB4xHDAaBgNVBAMTE3d3dy5sZXRzZW5jcnlwdC5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpL8ZjVL0MUkUAIbYO9+ZCni+c +ghGd9WhM2Ztaay6Wyh6lNoCdltdqTwUhE4O+d7UFModjM3G/KMyfuujr06c5iGKL +3saPmIzLaRPIEOUlB2rKgasKhe8mDRyRLzQSXXgnsaKcTBBuhIHvtP51ZMr05nJJ +sX/5FGjj96w+KJel6E/Ux1a1ZDOFkAYNSIrJJhA5jjIvUPr+Ri6Oc6UlhF9oueKI +uWBILxQpC778tBWdHoZeBCNTHA1VvtwC53OeuHvdZm1jB/e30Mgf5DtVizYpFXVD +mztkrd6z/3B6ZwPyfCE4KgzSf70/byOz971OJxNKTUVWedKHHDlrMxfsPclbAgMB +AAGjggTwMIIE7DAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFG1w4j/KDrYSFu7m9DPE +xRR0E5gzMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUF +BwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNy +eXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNy +eXB0Lm9yZy8wggHxBgNVHREEggHoMIIB5IIbY2VydC5pbnQteDEubGV0c2VuY3J5 +cHQub3JnghtjZXJ0LmludC14Mi5sZXRzZW5jcnlwdC5vcmeCG2NlcnQuaW50LXgz +LmxldHNlbmNyeXB0Lm9yZ4IbY2VydC5pbnQteDQubGV0c2VuY3J5cHQub3Jnghxj +ZXJ0LnJvb3QteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0YWdpbmcteDEubGV0 +c2VuY3J5cHQub3Jngh9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JngiBj +ZXJ0LnN0Zy1yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ISY3AubGV0c2VuY3J5cHQu +b3JnghpjcC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ITY3BzLmxldHNlbmNyeXB0 +Lm9yZ4IbY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3Jnghtjcmwucm9vdC14MS5s +ZXRzZW5jcnlwdC5vcmeCD2xldHNlbmNyeXB0Lm9yZ4IWb3JpZ2luLmxldHNlbmNy +eXB0Lm9yZ4IXb3JpZ2luMi5sZXRzZW5jcnlwdC5vcmeCFnN0YXR1cy5sZXRzZW5j +cnlwdC5vcmeCE3d3dy5sZXRzZW5jcnlwdC5vcmcwgf4GA1UdIASB9jCB8zAIBgZn +gQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3Bz +LmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmlj +YXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBh +bmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGlj +eSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzCC +AQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AMEWSuCnctLUOS3ICsEHcNTwxJvemRpI +QMH6B1Fk9jNgAAABZN0ChToAAAQDAEcwRQIgblal8oXnfoopr1+dWVhvBx+sqHT0 +eLYxJHBTaRp3j1QCIQDhFQqMk6DDXUgcU12K36zLVFwJTdAJI4RBisnX+g+W0AB2 +ACk8UZZUyDlluqpQ/FgH1Ldvv1h6KXLcpMMM9OVFR/R4AAABZN0Chz4AAAQDAEcw +RQIhAImOjvkritUNKJZB7dcUtjoyIbfNwdCspvRiEzXuvVQoAiAZryoyg3TcMun5 +Gb2dEn1cttMnPW9u670/JdRjvjU/wTANBgkqhkiG9w0BAQsFAAOCAQEAGepCmckP +Tn9Sz268FEwkdD+6wWaPfeYlh+9nacFh90nQ35EYQMOK8a+X7ixHGbRz19On3Wt4 +1fcbPa9SefocTjAintMwwreCxpRTmwGACYojd7vRWEmA6q7+/HO2BfZahWzclOjw +mSDBycDEm8R0ZK52vYjzVno8x0mrsmSO0403S/6syYB/guH6P17kIBw+Tgx6/i/c +I1C6MoFkuaAKUUcZmgGGBgE+L/7cWtWjbkVXyA3ZQQy9G7rcBT+N/RrDfBh4iZDq +jAN5UIIYL8upBhjiMYVuoJrH2nklzEwr5SWKcccJX5eWkGLUwlcY9LGAA8+17l2I +l1Ou20Dm9TxnNw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow +SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT +GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF +q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 +SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 +Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA +a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj +/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG +CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv +bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k +c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw +VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC +ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz +MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu +Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF +AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo +uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ +wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu +X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG +PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 +KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert2-root.pem b/tests/integration/targets/certificate_complete_chain/files/cert2-root.pem new file mode 100644 index 00000000..b2e43c93 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert2-root.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/cert2.pem b/tests/integration/targets/certificate_complete_chain/files/cert2.pem new file mode 100644 index 00000000..834eedc4 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/cert2.pem @@ -0,0 +1,45 @@ +-----BEGIN CERTIFICATE----- +MIIH5jCCBs6gAwIBAgISA2gSCm/BtvCR2e2bIap5YbXaMA0GCSqGSIb3DQEBCwUA +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODA3MjcxNzMxMjdaFw0x +ODEwMjUxNzMxMjdaMB4xHDAaBgNVBAMTE3d3dy5sZXRzZW5jcnlwdC5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpL8ZjVL0MUkUAIbYO9+ZCni+c +ghGd9WhM2Ztaay6Wyh6lNoCdltdqTwUhE4O+d7UFModjM3G/KMyfuujr06c5iGKL +3saPmIzLaRPIEOUlB2rKgasKhe8mDRyRLzQSXXgnsaKcTBBuhIHvtP51ZMr05nJJ +sX/5FGjj96w+KJel6E/Ux1a1ZDOFkAYNSIrJJhA5jjIvUPr+Ri6Oc6UlhF9oueKI +uWBILxQpC778tBWdHoZeBCNTHA1VvtwC53OeuHvdZm1jB/e30Mgf5DtVizYpFXVD +mztkrd6z/3B6ZwPyfCE4KgzSf70/byOz971OJxNKTUVWedKHHDlrMxfsPclbAgMB +AAGjggTwMIIE7DAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFG1w4j/KDrYSFu7m9DPE +xRR0E5gzMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUF +BwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNy +eXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNy +eXB0Lm9yZy8wggHxBgNVHREEggHoMIIB5IIbY2VydC5pbnQteDEubGV0c2VuY3J5 +cHQub3JnghtjZXJ0LmludC14Mi5sZXRzZW5jcnlwdC5vcmeCG2NlcnQuaW50LXgz +LmxldHNlbmNyeXB0Lm9yZ4IbY2VydC5pbnQteDQubGV0c2VuY3J5cHQub3Jnghxj +ZXJ0LnJvb3QteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0YWdpbmcteDEubGV0 +c2VuY3J5cHQub3Jngh9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JngiBj +ZXJ0LnN0Zy1yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ISY3AubGV0c2VuY3J5cHQu +b3JnghpjcC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ITY3BzLmxldHNlbmNyeXB0 +Lm9yZ4IbY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3Jnghtjcmwucm9vdC14MS5s +ZXRzZW5jcnlwdC5vcmeCD2xldHNlbmNyeXB0Lm9yZ4IWb3JpZ2luLmxldHNlbmNy +eXB0Lm9yZ4IXb3JpZ2luMi5sZXRzZW5jcnlwdC5vcmeCFnN0YXR1cy5sZXRzZW5j +cnlwdC5vcmeCE3d3dy5sZXRzZW5jcnlwdC5vcmcwgf4GA1UdIASB9jCB8zAIBgZn +gQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3Bz +LmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmlj +YXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBh +bmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGlj +eSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzCC +AQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AMEWSuCnctLUOS3ICsEHcNTwxJvemRpI +QMH6B1Fk9jNgAAABZN0ChToAAAQDAEcwRQIgblal8oXnfoopr1+dWVhvBx+sqHT0 +eLYxJHBTaRp3j1QCIQDhFQqMk6DDXUgcU12K36zLVFwJTdAJI4RBisnX+g+W0AB2 +ACk8UZZUyDlluqpQ/FgH1Ldvv1h6KXLcpMMM9OVFR/R4AAABZN0Chz4AAAQDAEcw +RQIhAImOjvkritUNKJZB7dcUtjoyIbfNwdCspvRiEzXuvVQoAiAZryoyg3TcMun5 +Gb2dEn1cttMnPW9u670/JdRjvjU/wTANBgkqhkiG9w0BAQsFAAOCAQEAGepCmckP +Tn9Sz268FEwkdD+6wWaPfeYlh+9nacFh90nQ35EYQMOK8a+X7ixHGbRz19On3Wt4 +1fcbPa9SefocTjAintMwwreCxpRTmwGACYojd7vRWEmA6q7+/HO2BfZahWzclOjw +mSDBycDEm8R0ZK52vYjzVno8x0mrsmSO0403S/6syYB/guH6P17kIBw+Tgx6/i/c +I1C6MoFkuaAKUUcZmgGGBgE+L/7cWtWjbkVXyA3ZQQy9G7rcBT+N/RrDfBh4iZDq +jAN5UIIYL8upBhjiMYVuoJrH2nklzEwr5SWKcccJX5eWkGLUwlcY9LGAA8+17l2I +l1Ou20Dm9TxnNw== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/roots.pem b/tests/integration/targets/certificate_complete_chain/files/roots.pem new file mode 100644 index 00000000..ee6c058d --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/roots.pem @@ -0,0 +1,3733 @@ +# ACCVRAIZ1 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# AC RAIZ FNMT-RCM +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Actalis Authentication Root CA +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# AddTrust External Root +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +# AffirmTrust Commercial +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# AffirmTrust Networking +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# AffirmTrust Premium +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# AffirmTrust Premium ECC +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Amazon Root CA 1 +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Amazon Root CA 2 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Amazon Root CA 3 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Amazon Root CA 4 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Atos TrustedRoot 2011 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Autoridad de Certificacion Firmaprofesional CIF A62634068 +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +# Baltimore CyberTrust Root +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Buypass Class 2 Root CA +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Buypass Class 3 Root CA +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# CA Disig Root R2 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Certigna +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Certinomis - Root CA +-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb +BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz +MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx +FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g +Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2 +fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl +LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV +WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF +TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb +5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc +CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri +wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ +wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG +m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4 +F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng +WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0 +2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF +AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/ +0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw +F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS +g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj +qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN +h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/ +ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V +btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj +Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ +8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW +gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE= +-----END CERTIFICATE----- + +# Certplus Class 2 Primary CA +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw +PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz +cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 +MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz +IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ +ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR +VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL +kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd +EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas +H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 +HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 +QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu +Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ +AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 +yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR +FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA +ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB +kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +# Certplus Root CA G1 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgISESBVg+QtPlRWhS2DN7cs3EYRMA0GCSqGSIb3DQEBDQUA +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBa +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ANpQh7bauKk+nWT6VjOaVj0W5QOVsjQcmm1iBdTYj+eJZJ+622SLZOZ5KmHNr49a +iZFluVj8tANfkT8tEBXgfs+8/H9DZ6itXjYj2JizTfNDnjl8KvzsiNWI7nC9hRYt +6kuJPKNxQv4c/dMcLRC4hlTqQ7jbxofaqK6AJc96Jh2qkbBIb6613p7Y1/oA/caP +0FG7Yn2ksYyy/yARujVjBYZHYEMzkPZHogNPlk2dT8Hq6pyi/jQu3rfKG3akt62f +6ajUeD94/vI4CTYd0hYCyOwqaK/1jpTvLRN6HkJKHRUxrgwEV/xhc/MxVoYxgKDE +EW4wduOU8F8ExKyHcomYxZ3MVwia9Az8fXoFOvpHgDm2z4QTd28n6v+WZxcIbekN +1iNQMLAVdBM+5S//Ds3EC0pd8NgAM0lm66EYfFkuPSi5YXHLtaW6uOrc4nBvCGrc +h2c0798wct3zyT8j/zXhviEpIDCB5BmlIOklynMxdCm+4kLV87ImZsdo/Rmz5yCT +mehd4F6H50boJZwKKSTUzViGUkAksnsPmBIgJPaQbEfIDbsYIC7Z/fyL8inqh3SV +4EJQeIQEQWGw9CEjjy3LKCHyamz0GqbFFLQ3ZU+V/YDI+HLlJWvEYLF7bY5KinPO +WftwenMGE9nTdDckQQoRb5fc5+R+ob0V8rqHDz1oihYHAgMBAAGjYzBhMA4GA1Ud +DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSowcCbkahDFXxd +Bie0KlHYlwuBsTAfBgNVHSMEGDAWgBSowcCbkahDFXxdBie0KlHYlwuBsTANBgkq +hkiG9w0BAQ0FAAOCAgEAnFZvAX7RvUz1isbwJh/k4DgYzDLDKTudQSk0YcbX8ACh +66Ryj5QXvBMsdbRX7gp8CXrc1cqh0DQT+Hern+X+2B50ioUHj3/MeXrKls3N/U/7 +/SMNkPX0XtPGYX2eEeAC7gkE2Qfdpoq3DIMku4NQkv5gdRE+2J2winq14J2by5BS +S7CTKtQ+FjPlnsZlFT5kOwQ/2wyPX1wdaR+v8+khjPPvl/aatxm2hHSco1S1cE5j +2FddUyGbQJJD+tZ3VTNPZNX70Cxqjm0lpu+F6ALEUz65noe8zDUa3qHpimOHZR4R +Kttjd5cUvpoUmRGywO6wT/gUITJDT5+rosuoD6o7BlXGEilXCNQ314cnrUlZp5Gr +RHpejXDbl85IULFzk/bwg2D5zfHhMf1bfHEhYxQUqq/F3pN+aLHsIqKqkHWetUNy +6mSjhEv9DKgma3GX7lZjZuhCVPnHHd/Qj1vfyDBviP4NxDMcU6ij/UgQ8uQKTuEV +V/xuZDDCVRHc6qnNSlSsKWNEz0pAoNZoWRsz+e86i9sgktxChL8Bq4fA1SCC28a5 +g4VCXA9DO2pJNdWY9BW/+mGBDAkgGNLQFwzLSABQ6XaCjGTXOqAHVcweMcDvOrRl +++O/QmueD6i9a5jc2NvLi6Td11n0bt3+qsOR0C5CB8AMTVPNJLFMWx5R9N/pkvo= +-----END CERTIFICATE----- + +# Certplus Root CA G2 +-----BEGIN CERTIFICATE----- +MIICHDCCAaKgAwIBAgISESDZkc6uo+jF5//pAq/Pc7xVMAoGCCqGSM49BAMDMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBaMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABM0PW1aC3/BFGtat +93nwHcmsltaeTpwftEIRyoa/bfuFo8XlGVzX7qY/aWfYeOKmycTbLXku54uNAm8x +Ik0G42ByRZ0OQneezs/lf4WbGOT8zC5y0xaTTsqZY1yhBSpsBqNjMGEwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNqDYwJ5jtpMxjwj +FNiPwyCrKGBZMB8GA1UdIwQYMBaAFNqDYwJ5jtpMxjwjFNiPwyCrKGBZMAoGCCqG +SM49BAMDA2gAMGUCMHD+sAvZ94OX7PNVHdTcswYO/jOYnYs5kGuUIe22113WTNch +p+e/IQ8rzfcq3IUHnQIxAIYUFuXcsGXCwI4Un78kFmjlvPl5adytRSv3tjFzzAal +U5ORGpOucGpnutee5WEaXw== +-----END CERTIFICATE----- + +# certSIGN ROOT CA +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# Certum Trusted Network CA 2 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Certum Trusted Network CA +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# CFCA EV ROOT +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Chambers of Commerce Root - 2008 +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz +IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz +MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj +dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw +EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp +MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 +28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq +VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q +DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR +5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL +ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a +Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl +UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s ++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 +Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx +hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV +HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 ++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN +YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t +L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy +ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt +IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV +HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w +DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW +PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF +5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 +glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH +FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 +pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD +xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG +tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq +jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De +fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ +d0jQ +-----END CERTIFICATE----- + +# Comodo AAA Services root +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# COMODO Certification Authority +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# COMODO ECC Certification Authority +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# COMODO RSA Certification Authority +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Cybertrust Global Root +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Deutsche Telekom Root CA 2 +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc +MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj +IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB +IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE +RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl +U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 +IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU +ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC +QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr +rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S +NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc +QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH +txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP +BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp +tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa +IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl +6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ +xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root CA +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root G2 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root G3 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# DigiCert Global Root CA +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# DigiCert Global Root G2 +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# DigiCert Global Root G3 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# DigiCert High Assurance EV Root CA +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# DigiCert Trusted Root G4 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# DST Root CA X3 +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +# D-TRUST Root Class 3 CA 2 2009 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# D-TRUST Root Class 3 CA 2 EV 2009 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# EC-ACC +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB +8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy +dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 +YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 +dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh +IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD +LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG +EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g +KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD +ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu +bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg +ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R +85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm +4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV +HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd +QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t +lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB +o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 +opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo +dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW +ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN +AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y +/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k +SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy +Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS +Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl +nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= +-----END CERTIFICATE----- + +# EE Certification Centre Root CA +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy +MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl +ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS +b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy +euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO +bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw +WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d +MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE +1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ +zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF +BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV +v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG +E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW +iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v +GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= +-----END CERTIFICATE----- + +# Entrust.net Premium 2048 Secure Server CA +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +# Entrust Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Entrust Root Certification Authority - EC1 +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +# Entrust Root Certification Authority - G2 +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +# ePKI Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# E-Tugra Certification Authority +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +# GDCA TrustAUTH R5 ROOT +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# GeoTrust Global CA +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority - G2 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority - G3 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# GeoTrust Universal CA +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# GeoTrust Universal CA 2 +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# Global Chambersign Root - 2008 +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx +MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy +cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG +A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl +BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed +KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 +G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 +zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 +ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG +HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 +Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V +yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e +beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r +6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog +zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW +BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr +ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp +ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk +cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt +YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC +CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI +hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ +UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz +X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x +fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz +a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd +Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd +SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O +AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso +M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge +v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +# GlobalSign ECC Root CA - R4 +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +# GlobalSign ECC Root CA - R5 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# GlobalSign Root CA +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# GlobalSign Root CA - R2 +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# GlobalSign Root CA - R3 +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Go Daddy Class 2 CA +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Go Daddy Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions ECC RootCA 2015 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions RootCA 2011 +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix +RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p +YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw +NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK +EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl +cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz +dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ +fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns +bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD +75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP +FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV +HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp +5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu +b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA +A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p +6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 +dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys +Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI +l7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions RootCA 2015 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Hongkong Post Root CA 1 +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- + +# IdenTrust Commercial Root CA 1 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# IdenTrust Public Sector Root CA 1 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# ISRG Root X1 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Izenpe.com +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# LuxTrust Global Root 2 +-----BEGIN CERTIFICATE----- +MIIFwzCCA6ugAwIBAgIUCn6m30tEntpqJIWe5rgV0xZ/u7EwDQYJKoZIhvcNAQEL +BQAwRjELMAkGA1UEBhMCTFUxFjAUBgNVBAoMDUx1eFRydXN0IFMuQS4xHzAdBgNV +BAMMFkx1eFRydXN0IEdsb2JhbCBSb290IDIwHhcNMTUwMzA1MTMyMTU3WhcNMzUw +MzA1MTMyMTU3WjBGMQswCQYDVQQGEwJMVTEWMBQGA1UECgwNTHV4VHJ1c3QgUy5B +LjEfMB0GA1UEAwwWTHV4VHJ1c3QgR2xvYmFsIFJvb3QgMjCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANeFl78RmOnwYoNMPIf5U2o3C/IPPIfOb9wmKb3F +ibrJgz337spbxm1Jc7TJRqMbNBM/wYlFV/TZsfs2ZUv7COJIcRHIbjuend+JZTem +hfY7RBi2xjcwYkSSl2l9QjAk5A0MiWtj3sXh306pFGxT4GHO9hcvHTy95iJMHZP1 +EMShduxq3sVs35a0VkBCwGKSMKEtFZSg0iAGCW5qbeXrt77U8PEVfIvmTroTzEsn +Xpk8F12PgX8zPU/TPxvsXD/wPEx1bvKm1Z3aLQdjAsZy6ZS8TEmVT4hSyNvoaYL4 +zDRbIvCGp4m9SAptZoFtyMhk+wHh9OHe2Z7d21vUKpkmFRseTJIpgp7VkoGSQXAZ +96Tlk0u8d2cx3Rz9MXANF5kM+Qw5GSoXtTBxVdUPrljhPS80m8+f9niFwpN6cj5m +j5wWEWCPnolvZ77gR1o7DJpni89Gxq44o/KnvObWhWszJHAiS8sIm7vI+AIpHb4g +DEa/a4ebsypmQjVGbKq6rfmYe+lQVRQxv7HaLe2ArWgk+2mr2HETMOZns4dA/Yl+ +8kPREd8vZS9kzl8UubG/Mb2HeFpZZYiq/FkySIbWTLkpS5XTdvN3JW1CHDiDTf2j +X5t/Lax5Gw5CMZdjpPuKadUiDTSQMC6otOBttpSsvItO13D8xTiOZCXhTTmQzsmH +hFhxAgMBAAGjgagwgaUwDwYDVR0TAQH/BAUwAwEB/zBCBgNVHSAEOzA5MDcGByuB +KwEBAQowLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9yZXBvc2l0b3J5Lmx1eHRydXN0 +Lmx1MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT/GCh2+UgFLKGu8SsbK7JT ++Et8szAdBgNVHQ4EFgQU/xgodvlIBSyhrvErGyuyU/hLfLMwDQYJKoZIhvcNAQEL +BQADggIBAGoZFO1uecEsh9QNcH7X9njJCwROxLHOk3D+sFTAMs2ZMGQXvw/l4jP9 +BzZAcg4atmpZ1gDlaCDdLnINH2pkMSCEfUmmWjfrRcmF9dTHF5kH5ptV5AzoqbTO +jFu1EVzPig4N1qx3gf4ynCSecs5U89BvolbW7MM3LGVYvlcAGvI1+ut7MV3CwRI9 +loGIlonBWVx65n9wNOeD4rHh4bhY79SV5GCc8JaXcozrhAIuZY+kt9J/Z93I055c +qqmkoCUUBpvsT34tC38ddfEz2O3OuHVtPlu5mB0xDVbYQw8wkbIEa91WvpWAVWe+ +2M2D2RjuLg+GLZKecBPs3lHJQ3gCpU3I+V/EkVhGFndadKpAvAefMLmx9xIX3eP/ +JEAdemrRTxgKqpAd60Ae36EeRJIQmvKN4dFLRp7oRUKX6kWZ8+xm1QL68qZKJKre +zrnK+T+Tb/mjuuqlPpmt/f97mfVl7vBZKGfXkJWkE4SphMHozs51k2MavDzq1WQf +LSoSOcbDWjLtR5EWDrw4wVDej8oqkDQc7kGUnF4ZLvhFSZl0kbAEb+MEWrGrKqv+ +x9CWttrhSmQGbmBNvUJO/3jaJMobtNeWOWyu8Q6qp31IiyBMz2TWuJdGsE7RKlY6 +oJO9r4Ak4Ap+58rVyuiFVdw2KuGUaJPHZnJED4AhMmwlxyOAgwrr +-----END CERTIFICATE----- + +# Microsec e-Szigno Root CA 2009 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# NetLock Arany (Class Gold) Főtanúsítvány +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Network Solutions Certificate Authority +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# OISTE WISeKey Global Root GA CA +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- + +# OISTE WISeKey Global Root GB CA +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# OpenTrust Root CA G1 +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESCzkFU5fX82bWTCp59rY45nMA0GCSqGSIb3DQEBCwUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcxMB4XDTE0MDUyNjA4NDU1MFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQD4eUbalsUwXopxAy1wpLuwxQjczeY1wICkES3d5oeuXT2R0odsN7faYp6b +wiTXj/HbpqbfRm9RpnHLPhsxZ2L3EVs0J9V5ToybWL0iEA1cJwzdMOWo010hOHQX +/uMftk87ay3bfWAfjH1MBcLrARYVmBSO0ZB3Ij/swjm4eTrwSSTilZHcYTSSjFR0 +77F9jAHiOH3BX2pfJLKOYheteSCtqx234LSWSE9mQxAGFiQD4eCcjsZGT44ameGP +uY4zbGneWK2gDqdkVBFpRGZPTBKnjix9xNRbxQA0MMHZmf4yzgeEtE7NCv82TWLx +p2NX5Ntqp66/K7nJ5rInieV+mhxNaMbBGN4zK1FGSxyO9z0M+Yo0FMT7MzUj8czx +Kselu7Cizv5Ta01BG2Yospb6p64KTrk5M0ScdMGTHPjgniQlQ/GbI4Kq3ywgsNw2 +TgOzfALU5nsaqocTvz6hdLubDuHAk5/XpGbKuxs74zD0M1mKB3IDVedzagMxbm+W +G+Oin6+Sx+31QrclTDsTBM8clq8cIqPQqwWyTBIjUtz9GVsnnB47ev1CI9sjgBPw +vFEVVJSmdz7QdFG9URQIOTfLHzSpMJ1ShC5VkLG631UAC9hWLbFJSXKAqWLXwPYY +EQRVzXR7z2FwefR7LFxckvzluFqrTJOVoSfupb7PcSNCupt2LQIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUl0YhVyE1 +2jZVx/PxN3DlCPaTKbYwHwYDVR0jBBgwFoAUl0YhVyE12jZVx/PxN3DlCPaTKbYw +DQYJKoZIhvcNAQELBQADggIBAB3dAmB84DWn5ph76kTOZ0BP8pNuZtQ5iSas000E +PLuHIT839HEl2ku6q5aCgZG27dmxpGWX4m9kWaSW7mDKHyP7Rbr/jyTwyqkxf3kf +gLMtMrpkZ2CvuVnN35pJ06iCsfmYlIrM4LvgBBuZYLFGZdwIorJGnkSI6pN+VxbS +FXJfLkur1J1juONI5f6ELlgKn0Md/rcYkoZDSw6cMoYsYPXpSOqV7XAp8dUv/TW0 +V8/bhUiZucJvbI/NeJWsZCj9VrDDb8O+WVLhX4SPgPL0DTatdrOjteFkdjpY3H1P +XlZs5VVZV6Xf8YpmMIzUUmI4d7S+KNfKNsSbBfD4Fdvb8e80nR14SohWZ25g/4/I +i+GOvUKpMwpZQhISKvqxnUOOBZuZ2mKtVzazHbYNeS2WuOvyDEsMpZTGMKcmGS3t +TAZQMPH9WD25SxdfGbRqhFS0OE85og2WaMMolP3tLR9Ka0OWLpABEPs4poEL0L91 +09S5zvE/bw4cHjdx5RiHdRk/ULlepEU0rbDK5uUTdg8xFKmOLZTW1YVNcxVPS/Ky +Pu1svf0OnWZzsD2097+o4BGkxK51CUpjAEggpsadCwmKtODmzj7HPiY46SvepghJ +AwSQiumPv+i2tCqjI40cHLI5kqiPAlxAOXXUc0ECd97N4EOH1uS6SsNsEn/+KuYj +1oxx +-----END CERTIFICATE----- + +# OpenTrust Root CA G2 +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESChaRu/vbm9UpaPI+hIvyYRMA0GCSqGSIb3DQEBDQUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcyMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDMtlelM5QQgTJT32F+D3Y5z1zCU3UdSXqWON2ic2rxb95eolq5cSG+Ntmh +/LzubKh8NBpxGuga2F8ORAbtp+Dz0mEL4DKiltE48MLaARf85KxP6O6JHnSrT78e +CbY2albz4e6WiWYkBuTNQjpK3eCasMSCRbP+yatcfD7J6xcvDH1urqWPyKwlCm/6 +1UWY0jUJ9gNDlP7ZvyCVeYCYitmJNbtRG6Q3ffyZO6v/v6wNj0OxmXsWEH4db0fE +FY8ElggGQgT4hNYdvJGmQr5J1WqIP7wtUdGejeBSzFfdNTVY27SPJIjki9/ca1TS +gSuyzpJLHB9G+h3Ykst2Z7UJmQnlrBcUVXDGPKBWCgOz3GIZ38i1MH/1PCZ1Eb3X +G7OHngevZXHloM8apwkQHZOJZlvoPGIytbU6bumFAYueQ4xncyhZW+vj3CzMpSZy +YhK05pyDRPZRpOLAeiRXyg6lPzq1O4vldu5w5pLeFlwoW5cZJ5L+epJUzpM5ChaH +vGOz9bGTXOBut9Dq+WIyiET7vycotjCVXRIouZW+j1MY5aIYFuJWpLIsEPUdN6b4 +t/bQWVyJ98LVtZR00dX+G7bw5tYee9I8y6jj9RjzIR9u701oBnstXW5DiabA+aC/ +gh7PU3+06yzbXfZqfUAkBXKJOAGTy3HCOV0GEfZvePg3DTmEJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUajn6QiL3 +5okATV59M4PLuG53hq8wHwYDVR0jBBgwFoAUajn6QiL35okATV59M4PLuG53hq8w +DQYJKoZIhvcNAQENBQADggIBAJjLq0A85TMCl38th6aP1F5Kr7ge57tx+4BkJamz +Gj5oXScmp7oq4fBXgwpkTx4idBvpkF/wrM//T2h6OKQQbA2xx6R3gBi2oihEdqc0 +nXGEL8pZ0keImUEiyTCYYW49qKgFbdEfwFFEVn8nNQLdXpgKQuswv42hm1GqO+qT +RmTFAHneIWv2V6CG1wZy7HBGS4tz3aAhdT7cHcCP009zHIXZ/n9iyJVvttN7jLpT +wm+bREx50B1ws9efAvSyB7DH5fitIw6mVskpEndI2S9G/Tvw/HRwkqWOOAgfZDC2 +t0v7NqwQjqBSM2OdAzVWxWm9xiNaJ5T2pBL4LTM8oValX9YZ6e18CL13zSdkzJTa +TkZQh+D5wVOAHrut+0dSixv9ovneDiK3PTNZbNTe9ZUGMg1RGUFcPk8G97krgCf2 +o6p6fAbhQ8MTOWIaNr3gKC6UAuQpLmBVrkA9sHSSXvAgZJY/X0VdiLWK2gKgW0VU +3jg9CcCoSmVGFvyqv1ROTVu+OEO3KMqLM6oaJbolXCkvW0pujOotnCr2BXbgd5eA +iN1nE28daCSLT7d0geX0YJ96Vdc+N9oWaz53rK4YcJUIeSkDiv7BO7M/Gg+kO14f +WKGVyasvc0rQLW6aWQ9VGHgtPFGml4vmu7JwqkwR3v98KzfUetF3NI/n+UL3PIEM +S1IK +-----END CERTIFICATE----- + +# OpenTrust Root CA G3 +-----BEGIN CERTIFICATE----- +MIICITCCAaagAwIBAgISESDm+Ez8JLC+BUCs2oMbNGA/MAoGCCqGSM49BAMDMEAx +CzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9wZW5U +cnVzdCBSb290IENBIEczMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAwMFow +QDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwUT3Bl +blRydXN0IFJvb3QgQ0EgRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARK7liuTcpm +3gY6oxH84Bjwbhy6LTAMidnW7ptzg6kjFYwvWYpa3RTqnVkrQ7cG7DK2uu5Bta1d +oYXM6h0UZqNnfkbilPPntlahFVmhTzeXuSIevRHr9LIfXsMUmuXZl5mjYzBhMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHd8MUi2I5 +DMlv4VBN0BBY3JWIbTAfBgNVHSMEGDAWgBRHd8MUi2I5DMlv4VBN0BBY3JWIbTAK +BggqhkjOPQQDAwNpADBmAjEAj6jcnboMBBf6Fek9LykBl7+BFjNAk2z8+e2AcG+q +j9uEwov1NcoG3GRvaBbhj5G5AjEA2Euly8LQCGzpGPta3U1fJAuwACEl74+nBCZx +4nxp5V2a+EEfOzmTk51V6s2N8fvB +-----END CERTIFICATE----- + +# QuoVadis Root CA 1 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# QuoVadis Root CA 2 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# QuoVadis Root CA +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- + +# QuoVadis Root CA 2 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# QuoVadis Root CA 3 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# QuoVadis Root CA 3 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Secure Global CA +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# SecureSign RootCA11 +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +# SecureTrust CA +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Security Communication Root CA +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- + +# Security Communication RootCA2 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Sonera Class 2 Root CA +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- + +# SSL.com EV Root Certification Authority ECC +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# SSL.com EV Root Certification Authority RSA R2 +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# SSL.com Root Certification Authority ECC +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# SSL.com Root Certification Authority RSA +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Staat der Nederlanden EV Root CA +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y +MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg +TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS +b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS +M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC +UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d +Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p +rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l +pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb +j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC +KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS +/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X +cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH +1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP +px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 +MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u +2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS +v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC +wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy +CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e +vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 +Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa +Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL +eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 +FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc +7uzXLg== +-----END CERTIFICATE----- + +# Staat der Nederlanden Root CA - G2 +-----BEGIN CERTIFICATE----- +MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX +DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 +qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp +uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU +Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE +pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp +5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M +UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN +GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy +5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv +6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK +eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 +B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ +BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov +L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG +SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS +CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen +5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 +IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK +gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL ++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL +vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm +bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk +N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC +Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z +ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== +-----END CERTIFICATE----- + +# Staat der Nederlanden Root CA - G3 +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- + +# Starfield Class 2 CA +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Starfield Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Starfield Services Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# SwissSign Gold CA - G2 +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +# SwissSign Silver CA - G2 +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu +IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow +RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY +U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv +Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br +YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF +nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH +6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt +eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ +c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ +MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH +HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf +jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 +5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB +rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c +wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB +AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp +WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 +xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ +2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ +IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 +aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X +em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR +dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ +OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ +hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy +tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +# SZAFIR ROOT CA2 +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Taiwan GRCA +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- + +# TeliaSonera Root CA v1 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# thawte Primary Root CA +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# thawte Primary Root CA - G2 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# thawte Primary Root CA - G3 +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +# TrustCor ECA-1 +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y +IEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig +RUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb +3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA +BoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5 +3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou +owReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/ +wZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF +ZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf +BgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv +civUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2 +AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50 +soIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI +WJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi +tJ/X5g== +-----END CERTIFICATE----- + +# TrustCor RootCert CA-1 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y +IFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB +pDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h +IENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG +A1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU +cnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid +RtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V +seq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme +9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV +EY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW +hnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/ +DeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I +/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ +yonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts +L1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN +zl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- + +# TrustCor RootCert CA-2 +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig +Um9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk +MQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg +Q2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD +VQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy +dXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+ +QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq +1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp +2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK +DOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape +az6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF +3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88 +oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM +g9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3 +mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd +BgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U +nrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX +dKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+ +MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL +/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX +CI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa +ZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW +2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7 +N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3 +Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB +As8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp +5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu +1uwJ +-----END CERTIFICATE----- + +# Trustis FPS Root CA +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL +ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx +MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc +MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ +AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH +iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj +vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA +0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB +OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ +BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E +FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 +GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW +zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 +1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE +f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F +jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN +ZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +# T-TeleSec GlobalRoot Class 2 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# T-TeleSec GlobalRoot Class 3 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# TWCA Global Root CA +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# TWCA Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# USERTrust ECC Certification Authority +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# USERTrust RSA Certification Authority +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Verisign Class 3 Public Primary Certification Authority - G3 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# VeriSign Class 3 Public Primary Certification Authority - G4 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# VeriSign Class 3 Public Primary Certification Authority - G5 +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# VeriSign Universal Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Visa eCommerce Root +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr +MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl +cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h +dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l +cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h +2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E +lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV +ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq +299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t +vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL +dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF +AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR +zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 +LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd +7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw +++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +# XRamp Global CA Root +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# CAcert Class 3 Root +-----BEGIN CERTIFICATE----- +MIIHWTCCBUGgAwIBAgIDCkGKMA0GCSqGSIb3DQEBCwUAMHkxEDAOBgNVBAoTB1Jv +b3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAGA1UEAxMZ +Q0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYSc3VwcG9y +dEBjYWNlcnQub3JnMB4XDTExMDUyMzE3NDgwMloXDTIxMDUyMDE3NDgwMlowVDEU +MBIGA1UEChMLQ0FjZXJ0IEluYy4xHjAcBgNVBAsTFWh0dHA6Ly93d3cuQ0FjZXJ0 +Lm9yZzEcMBoGA1UEAxMTQ0FjZXJ0IENsYXNzIDMgUm9vdDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAKtJNRFIfNImflOUz0Op3SjXQiqL84d4GVh8D57a +iX3h++tykA10oZZkq5+gJJlz2uJVdscXe/UErEa4w75/ZI0QbCTzYZzA8pD6Ueb1 +aQFjww9W4kpCz+JEjCUoqMV5CX1GuYrz6fM0KQhF5Byfy5QEHIGoFLOYZcRD7E6C +jQnRvapbjZLQ7N6QxX8KwuPr5jFaXnQ+lzNZ6MMDPWAzv/fRb0fEze5ig1JuLgia +pNkVGJGmhZJHsK5I6223IeyFGmhyNav/8BBdwPSUp2rVO5J+TJAFfpPBLIukjmJ0 +FXFuC3ED6q8VOJrU0gVyb4z5K+taciX5OUbjchs+BMNkJyIQKopPWKcDrb60LhPt +XapI19V91Cp7XPpGBFDkzA5CW4zt2/LP/JaT4NsRNlRiNDiPDGCbO5dWOK3z0luL +oFvqTpa4fNfVoIZwQNORKbeiPK31jLvPGpKK5DR7wNhsX+kKwsOnIJpa3yxdUly6 +R9Wb7yQocDggL9V/KcCyQQNokszgnMyXS0XvOhAKq3A6mJVwrTWx6oUrpByAITGp +rmB6gCZIALgBwJNjVSKRPFbnr9s6JfOPMVTqJouBWfmh0VMRxXudA/Z0EeBtsSw/ +LIaRmXGapneLNGDRFLQsrJ2vjBDTn8Rq+G8T/HNZ92ZCdB6K4/jc0m+YnMtHmJVA +BfvpAgMBAAGjggINMIICCTAdBgNVHQ4EFgQUdahxYEyIE/B42Yl3tW3Fid+8sXow +gaMGA1UdIwSBmzCBmIAUFrUyG9TH8+DmjvO90rA67rI5GNGhfaR7MHkxEDAOBgNV +BAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAG +A1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS +c3VwcG9ydEBjYWNlcnQub3JnggEAMA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUH +AQEEUTBPMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5DQWNlcnQub3JnLzAoBggr +BgEFBQcwAoYcaHR0cDovL3d3dy5DQWNlcnQub3JnL2NhLmNydDBKBgNVHSAEQzBB +MD8GCCsGAQQBgZBKMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuQ0FjZXJ0Lm9y +Zy9pbmRleC5waHA/aWQ9MTAwNAYJYIZIAYb4QgEIBCcWJWh0dHA6Ly93d3cuQ0Fj +ZXJ0Lm9yZy9pbmRleC5waHA/aWQ9MTAwUAYJYIZIAYb4QgENBEMWQVRvIGdldCB5 +b3VyIG93biBjZXJ0aWZpY2F0ZSBmb3IgRlJFRSwgZ28gdG8gaHR0cDovL3d3dy5D +QWNlcnQub3JnMA0GCSqGSIb3DQEBCwUAA4ICAQApKIWuRKm5r6R5E/CooyuXYPNc +7uMvwfbiZqARrjY3OnYVBFPqQvX56sAV2KaC2eRhrnILKVyQQ+hBsuF32wITRHhH +Va9Y/MyY9kW50SD42CEH/m2qc9SzxgfpCYXMO/K2viwcJdVxjDm1Luq+GIG6sJO4 +D+Pm1yaMMVpyA4RS5qb1MyJFCsgLDYq4Nm+QCaGrvdfVTi5xotSu+qdUK+s1jVq3 +VIgv7nSf7UgWyg1I0JTTrKSi9iTfkuO960NAkW4cGI5WtIIS86mTn9S8nK2cde5a +lxuV53QtHA+wLJef+6kzOXrnAzqSjiL2jA3k2X4Ndhj3AfnvlpaiVXPAPHG0HRpW +Q7fDCo1y/OIQCQtBzoyUoPkD/XFzS4pXM+WOdH4VAQDmzEoc53+VGS3FpQyLu7Xt +hbNc09+4ufLKxw0BFKxwWMWMjTPUnWajGlCVI/xI4AZDEtnNp4Y5LzZyo4AQ5OHz +0ctbGsDkgJp8E3MGT9ujayQKurMcvEp4u+XjdTilSKeiHq921F73OIZWWonO1sOn +ebJSoMbxhbQljPI/lrMQ2Y1sVzufb4Y6GIIiNsiwkTjbKqGTqoQ/9SdlrnPVyNXT +d+pLncdBu8fA46A/5H2kjXPmEkvfoXNzczqA6NXLji/L6hOn1kGLrPo8idck9U60 +4GGSt/M3mMS+lqO3ig== +-----END CERTIFICATE----- + +# CA Cert Signing Authority +-----BEGIN CERTIFICATE----- +MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO +BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi +MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ +ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ +8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6 +zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y +fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7 +w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc +G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k +epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q +laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ +QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU +fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826 +YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w +ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY +gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe +MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0 +IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy +dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw +czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0 +dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl +aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC +AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg +b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB +ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc +nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg +18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c +gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl +Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY +sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T +SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF +CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum +GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk +zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW +omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_Certification_Authority.pem b/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_Certification_Authority.pem new file mode 100644 index 00000000..6146dcb5 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_Certification_Authority.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_ECC_Certification_Authority.pem b/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_ECC_Certification_Authority.pem new file mode 100644 index 00000000..546c95e3 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_ECC_Certification_Authority.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_RSA_Certification_Authority.pem b/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_RSA_Certification_Authority.pem new file mode 100644 index 00000000..6508d1e8 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_RSA_Certification_Authority.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/roots/DST_Root_CA_X3.pem b/tests/integration/targets/certificate_complete_chain/files/roots/DST_Root_CA_X3.pem new file mode 100644 index 00000000..b2e43c93 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/roots/DST_Root_CA_X3.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/files/roots/ISRG_Root_X1.pem b/tests/integration/targets/certificate_complete_chain/files/roots/ISRG_Root_X1.pem new file mode 100644 index 00000000..b85c8037 --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/files/roots/ISRG_Root_X1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/tests/integration/targets/certificate_complete_chain/meta/main.yml b/tests/integration/targets/certificate_complete_chain/meta/main.yml new file mode 100644 index 00000000..1810d4be --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_remote_tmp_dir diff --git a/tests/integration/targets/certificate_complete_chain/tasks/main.yml b/tests/integration/targets/certificate_complete_chain/tasks/main.yml new file mode 100644 index 00000000..c9cf1dcc --- /dev/null +++ b/tests/integration/targets/certificate_complete_chain/tasks/main.yml @@ -0,0 +1,81 @@ +- name: register cryptography version + command: '{{ ansible_python.executable }} -c ''import cryptography; print(cryptography.__version__)''' + register: cryptography_version +- block: + - name: Archive test files + community.general.archive: + path: '{{ role_path }}/files/' + dest: '{{ output_dir }}/files.tgz' + - name: Create temporary directory to store files + file: + state: directory + path: '{{ remote_tmp_dir }}/files/' + - name: Unarchive test files on testhost + unarchive: + src: '{{ output_dir }}/files.tgz' + dest: '{{ remote_tmp_dir }}/files/' + - name: Find root for cert 1 + certificate_complete_chain: + input_chain: '{{ lookup(''file'', ''cert1-fullchain.pem'', rstrip=False) }}' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots/' + register: cert1_root + - name: Verify root for cert 1 + assert: + that: + - cert1_root.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False)) + - cert1_root.root == lookup('file', 'cert1-root.pem', rstrip=False) + - name: Find rootchain for cert 1 + certificate_complete_chain: + input_chain: '{{ lookup(''file'', ''cert1.pem'', rstrip=False) }}' + intermediate_certificates: + - '{{ remote_tmp_dir }}/files/cert1-chain.pem' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots.pem' + register: cert1_rootchain + - name: Verify rootchain for cert 1 + assert: + that: + - cert1_rootchain.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False)) + - cert1_rootchain.chain[:-1] | join('') == lookup('file', 'cert1-chain.pem', rstrip=False) + - cert1_rootchain.root == lookup('file', 'cert1-root.pem', rstrip=False) + - name: Find root for cert 2 + certificate_complete_chain: + input_chain: '{{ lookup(''file'', ''cert2-fullchain.pem'', rstrip=False) }}' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots/' + register: cert2_root + - name: Verify root for cert 2 + assert: + that: + - cert2_root.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False)) + - cert2_root.root == lookup('file', 'cert2-root.pem', rstrip=False) + - name: Find rootchain for cert 2 + certificate_complete_chain: + input_chain: '{{ lookup(''file'', ''cert2.pem'', rstrip=False) }}' + intermediate_certificates: + - '{{ remote_tmp_dir }}/files/cert2-chain.pem' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots.pem' + register: cert2_rootchain + - name: Verify rootchain for cert 2 + assert: + that: + - cert2_rootchain.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False)) + - cert2_rootchain.chain[:-1] | join('') == lookup('file', 'cert2-chain.pem', rstrip=False) + - cert2_rootchain.root == lookup('file', 'cert2-root.pem', rstrip=False) + - name: Find alternate rootchain for cert 2 + certificate_complete_chain: + input_chain: '{{ lookup(''file'', ''cert2.pem'', rstrip=True) }}' + intermediate_certificates: + - '{{ remote_tmp_dir }}/files/cert2-altchain.pem' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots.pem' + register: cert2_rootchain_alt + - name: Verify rootchain for cert 2 + assert: + that: + - cert2_rootchain_alt.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-altchain.pem', rstrip=False) ~ lookup('file', 'cert2-altroot.pem', rstrip=False)) + - cert2_rootchain_alt.chain[:-1] | join('') == lookup('file', 'cert2-altchain.pem', rstrip=False) + - cert2_rootchain_alt.root == lookup('file', 'cert2-altroot.pem', rstrip=False) + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/tests/integration/targets/ecs_certificate/aliases b/tests/integration/targets/ecs_certificate/aliases new file mode 100644 index 00000000..f320bbb3 --- /dev/null +++ b/tests/integration/targets/ecs_certificate/aliases @@ -0,0 +1,15 @@ +# Not enabled due to lack of access to test environments. May be enabled using custom integration_config.yml +# Example integation_config.yml +# --- +# entrust_api_user: +# entrust_api_key: +# entrust_api_client_cert_path: /var/integration-testing/publicCert.pem +# entrust_api_client_cert_key_path: /var/integration-testing/privateKey.pem +# entrust_api_ip_address: 127.0.0.1 +# entrust_cloud_ip_address: 127.0.0.1 +# # Used for certificate path validation of QA environments - we chose not to support disabling path validation ever. +# cacerts_bundle_path_local: /var/integration-testing/cacerts + +### WARNING: This test will update HOSTS file and CERTIFICATE STORE of target host, in order to be able to validate +# to a QA environment. ### +unsupported diff --git a/tests/integration/targets/ecs_certificate/defaults/main.yml b/tests/integration/targets/ecs_certificate/defaults/main.yml new file mode 100644 index 00000000..86bc36c3 --- /dev/null +++ b/tests/integration/targets/ecs_certificate/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for test_ecs_certificate diff --git a/tests/integration/targets/ecs_certificate/meta/main.yml b/tests/integration/targets/ecs_certificate/meta/main.yml new file mode 100644 index 00000000..a35b3306 --- /dev/null +++ b/tests/integration/targets/ecs_certificate/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_openssl diff --git a/tests/integration/targets/ecs_certificate/tasks/main.yml b/tests/integration/targets/ecs_certificate/tasks/main.yml new file mode 100644 index 00000000..9cc9b8cf --- /dev/null +++ b/tests/integration/targets/ecs_certificate/tasks/main.yml @@ -0,0 +1,215 @@ +--- +## Verify that integration_config was specified +- block: + - assert: + that: + - entrust_api_user is defined + - entrust_api_key is defined + - entrust_api_ip_address is defined + - entrust_cloud_ip_address is defined + - entrust_api_client_cert_path is defined or entrust_api_client_cert_contents is defined + - entrust_api_client_cert_key_path is defined or entrust_api_client_cert_key_contents + - cacerts_bundle_path_local is defined + +## SET UP TEST ENVIRONMENT ######################################################################## +- name: copy the files needed for verifying test server certificate to the host + copy: + src: '{{ cacerts_bundle_path_local }}/' + dest: '{{ cacerts_bundle_path }}' + +- name: Update the CA certificates for our QA certs (collection may need updating if new QA environments used) + command: c_rehash {{ cacerts_bundle_path }} + +- name: Update hosts file + lineinfile: + path: /etc/hosts + state: present + regexp: 'api.entrust.net$' + line: '{{ entrust_api_ip_address }} api.entrust.net' + +- name: Update hosts file + lineinfile: + path: /etc/hosts + state: present + regexp: 'cloud.entrust.net$' + line: '{{ entrust_cloud_ip_address }} cloud.entrust.net' + +- name: Clear out the temporary directory for storing the API connection information + file: + path: '{{ tmpdir_path }}' + state: absent + +- name: Create a directory for storing the API connection Information + file: + path: '{{ tmpdir_path }}' + state: directory + +- name: Copy the files needed for the connection to entrust API to the host + copy: + src: '{{ entrust_api_client_cert_path }}' + dest: '{{ entrust_api_cert }}' + +- name: Copy the files needed for the connection to entrust API to the host + copy: + src: '{{ entrust_api_client_cert_key_path }}' + dest: '{{ entrust_api_cert_key }}' + +## SETUP CSR TO REQUEST +- name: Generate a 2048 bit RSA private key + openssl_privatekey: + path: '{{ privatekey_path }}' + passphrase: '{{ privatekey_passphrase }}' + cipher: auto + type: RSA + size: 2048 + +- name: Generate a certificate signing request using the generated key + openssl_csr: + path: '{{ csr_path }}' + privatekey_path: '{{ privatekey_path }}' + privatekey_passphrase: '{{ privatekey_passphrase }}' + common_name: '{{ common_name }}' + organization_name: '{{ organization_name | default(omit) }}' + organizational_unit_name: '{{ organizational_unit_name | default(omit) }}' + country_name: '{{ country_name | default(omit) }}' + state_or_province_name: '{{ state_or_province_name | default(omit) }}' + digest: sha256 + +- block: + - name: Have ECS generate a signed certificate + ecs_certificate: + backup: True + path: '{{ example1_cert_path }}' + full_chain_path: '{{ example1_chain_path }}' + csr: '{{ csr_path }}' + cert_type: '{{ example1_cert_type }}' + requester_name: '{{ entrust_requester_name }}' + requester_email: '{{ entrust_requester_email }}' + requester_phone: '{{ entrust_requester_phone }}' + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: example1_result + + - assert: + that: + - example1_result is not failed + - example1_result.changed + - example1_result.tracking_id > 0 + - example1_result.serial_number is string + + # Internal CA refuses to issue certificates with the same DN in a short time frame + - name: Sleep for 5 seconds so we don't run into duplicate-request errors + pause: + seconds: 5 + + - name: Attempt to have ECS generate a signed certificate, but existing one is valid + ecs_certificate: + backup: True + path: '{{ example1_cert_path }}' + full_chain_path: '{{ example1_chain_path }}' + csr: '{{ csr_path }}' + cert_type: '{{ example1_cert_type }}' + requester_name: '{{ entrust_requester_name }}' + requester_email: '{{ entrust_requester_email }}' + requester_phone: '{{ entrust_requester_phone }}' + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: example2_result + + - assert: + that: + - example2_result is not failed + - not example2_result.changed + - example2_result.backup_file is undefined + - example2_result.backup_full_chain_file is undefined + - example2_result.serial_number == example1_result.serial_number + - example2_result.tracking_id == example1_result.tracking_id + + # Internal CA refuses to issue certificates with the same DN in a short time frame + - name: Sleep for 5 seconds so we don't run into duplicate-request errors + pause: + seconds: 5 + + - name: Force a reissue with no CSR, verify that contents changed + ecs_certificate: + backup: True + force: True + path: '{{ example1_cert_path }}' + full_chain_path: '{{ example1_chain_path }}' + cert_type: '{{ example1_cert_type }}' + request_type: reissue + requester_name: '{{ entrust_requester_name }}' + requester_email: '{{ entrust_requester_email }}' + requester_phone: '{{ entrust_requester_phone }}' + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: example3_result + + - assert: + that: + - example3_result is not failed + - example3_result.changed + - example3_result.backup_file is string + - example3_result.backup_full_chain_file is string + - example3_result.tracking_id > 0 + - example3_result.tracking_id != example1_result.tracking_id + - example3_result.serial_number != example1_result.serial_number + + # Internal CA refuses to issue certificates with the same DN in a short time frame + - name: Sleep for 5 seconds so we don't run into duplicate-request errors + pause: + seconds: 5 + + - name: Test a request with all of the various optional possible fields populated + ecs_certificate: + path: '{{ example4_cert_path }}' + full_chain_path: '{{ example4_full_chain_path }}' + csr: '{{ csr_path }}' + subject_alt_name: '{{ example4_subject_alt_name }}' + eku: '{{ example4_eku }}' + ct_log: True + cert_type: '{{ example4_cert_type }}' + org: '{{ example4_org }}' + ou: '{{ example4_ou }}' + tracking_info: '{{ example4_tracking_info }}' + additional_emails: '{{ example4_additional_emails }}' + custom_fields: '{{ example4_custom_fields }}' + cert_expiry: '{{ example4_cert_expiry }}' + requester_name: '{{ entrust_requester_name }}' + requester_email: '{{ entrust_requester_email }}' + requester_phone: '{{ entrust_requester_phone }}' + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: example4_result + + - assert: + that: + - example4_result is not failed + - example4_result.changed + - example4_result.backup_file is undefined + - example4_result.backup_full_chain_file is undefined + - example4_result.tracking_id > 0 + - example4_result.serial_number is string + + # For bug 61738, verify that the full chain is valid + - name: Verify that the full chain path can be successfully imported + command: openssl verify "{{ example4_full_chain_path }}" + register: openssl_result + + - assert: + that: + - "' OK' in openssl_result.stdout_lines[0]" + + always: + - name: clean-up temporary folder + file: + path: '{{ tmpdir_path }}' + state: absent diff --git a/tests/integration/targets/ecs_certificate/vars/main.yml b/tests/integration/targets/ecs_certificate/vars/main.yml new file mode 100644 index 00000000..8e617618 --- /dev/null +++ b/tests/integration/targets/ecs_certificate/vars/main.yml @@ -0,0 +1,52 @@ +--- +# vars file for test_ecs_certificate + +# Path on various hosts that cacerts need to be put as a prerequisite to API server cert validation. +# May need to be customized for some environments based on SSL implementations +# that ansible "urls" module utility is using as a backing. +cacerts_bundle_path: /etc/pki/tls/certs + +common_name: '{{ ansible_date_time.epoch }}.ansint.testcertificates.com' +organization_name: CMS API, Inc. +organizational_unit_name: RSA +country_name: US +state_or_province_name: MA +privatekey_passphrase: Passphrase452! +tmpdir_path: /tmp/ecs_cert_test/{{ ansible_date_time.epoch }} +privatekey_path: '{{ tmpdir_path }}/testcertificates.key' +entrust_api_cert: '{{ tmpdir_path }}/authcert.cer' +entrust_api_cert_key: '{{ tmpdir_path }}/authkey.cer' +csr_path: '{{ tmpdir_path }}/request.csr' + +entrust_requester_name: C Trufan +entrust_requester_email: CTIntegrationTests@entrustdatacard.com +entrust_requester_phone: 1-555-555-5555 # e.g. 15555555555 + +# TEST 1 +example1_cert_path: '{{ tmpdir_path }}/issuedcert_1.pem' +example1_chain_path: '{{ tmpdir_path }}/issuedcert_1_chain.pem' +example1_cert_type: EV_SSL + +example4_cert_path: '{{ tmpdir_path }}/issuedcert_2.pem' +example4_subject_alt_name: + - ansible.testcertificates.com + - www.testcertificates.com +example4_eku: SERVER_AND_CLIENT_AUTH +example4_cert_type: UC_SSL +# Test a secondary org and special characters +example4_org: Cañon City, Inc. +example4_ou: + - StringrsaString +example4_tracking_info: Submitted via Ansible Integration +example4_additional_emails: + - itsupport@testcertificates.com + - jsmith@ansible.com +example4_custom_fields: + text1: Admin + text2: Invoice 25 + number1: 342 + date3: '2018-01-01' + email2: sales@ansible.testcertificates.com + dropdown2: Dropdown 2 Value 1 +example4_cert_expiry: 2020-08-15 +example4_full_chain_path: '{{ tmpdir_path }}/issuedcert_2_chain.pem' diff --git a/tests/integration/targets/ecs_domain/aliases b/tests/integration/targets/ecs_domain/aliases new file mode 100644 index 00000000..f320bbb3 --- /dev/null +++ b/tests/integration/targets/ecs_domain/aliases @@ -0,0 +1,15 @@ +# Not enabled due to lack of access to test environments. May be enabled using custom integration_config.yml +# Example integation_config.yml +# --- +# entrust_api_user: +# entrust_api_key: +# entrust_api_client_cert_path: /var/integration-testing/publicCert.pem +# entrust_api_client_cert_key_path: /var/integration-testing/privateKey.pem +# entrust_api_ip_address: 127.0.0.1 +# entrust_cloud_ip_address: 127.0.0.1 +# # Used for certificate path validation of QA environments - we chose not to support disabling path validation ever. +# cacerts_bundle_path_local: /var/integration-testing/cacerts + +### WARNING: This test will update HOSTS file and CERTIFICATE STORE of target host, in order to be able to validate +# to a QA environment. ### +unsupported diff --git a/tests/integration/targets/ecs_domain/defaults/main.yml b/tests/integration/targets/ecs_domain/defaults/main.yml new file mode 100644 index 00000000..69034c4b --- /dev/null +++ b/tests/integration/targets/ecs_domain/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for test_ecs_domain diff --git a/tests/integration/targets/ecs_domain/meta/main.yml b/tests/integration/targets/ecs_domain/meta/main.yml new file mode 100644 index 00000000..07faa217 --- /dev/null +++ b/tests/integration/targets/ecs_domain/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_tests diff --git a/tests/integration/targets/ecs_domain/tasks/main.yml b/tests/integration/targets/ecs_domain/tasks/main.yml new file mode 100644 index 00000000..4a12e116 --- /dev/null +++ b/tests/integration/targets/ecs_domain/tasks/main.yml @@ -0,0 +1,270 @@ +--- +## Verify that integration_config was specified +- block: + - assert: + that: + - entrust_api_user is defined + - entrust_api_key is defined + - entrust_api_ip_address is defined + - entrust_cloud_ip_address is defined + - entrust_api_client_cert_path is defined or entrust_api_client_cert_contents is defined + - entrust_api_client_cert_key_path is defined or entrust_api_client_cert_key_contents + - cacerts_bundle_path_local is defined + +## SET UP TEST ENVIRONMENT ######################################################################## +- name: copy the files needed for verifying test server certificate to the host + copy: + src: '{{ cacerts_bundle_path_local }}/' + dest: '{{ cacerts_bundle_path }}' + +- name: Update the CA certificates for our QA certs (collection may need updating if new QA environments used) + command: c_rehash {{ cacerts_bundle_path }} + +- name: Update hosts file + lineinfile: + path: /etc/hosts + state: present + regexp: 'api.entrust.net$' + line: '{{ entrust_api_ip_address }} api.entrust.net' + +- name: Update hosts file + lineinfile: + path: /etc/hosts + state: present + regexp: 'cloud.entrust.net$' + line: '{{ entrust_cloud_ip_address }} cloud.entrust.net' + +- name: Clear out the temporary directory for storing the API connection information + file: + path: '{{ tmpdir_path }}' + state: absent + +- name: Create a directory for storing the API connection Information + file: + path: '{{ tmpdir_path }}' + state: directory + +- name: Copy the files needed for the connection to entrust API to the host + copy: + src: '{{ entrust_api_client_cert_path }}' + dest: '{{ entrust_api_cert }}' + +- name: Copy the files needed for the connection to entrust API to the host + copy: + src: '{{ entrust_api_client_cert_key_path }}' + dest: '{{ entrust_api_cert_key }}' + +- block: + - name: Have ECS request a domain validation via dns + ecs_domain: + domain_name: dns.{{ common_name }} + verification_method: dns + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: dns_result + + - assert: + that: + - dns_result is not failed + - dns_result.changed + - dns_result.domain_status == 'INITIAL_VERIFICATION' + - dns_result.verification_method == 'dns' + - dns_result.dns_location is string + - dns_result.dns_contents is string + - dns_result.dns_resource_type is string + - dns_result.file_location is undefined + - dns_result.file_contents is undefined + - dns_result.emails is undefined + + - name: Have ECS request a domain validation via web_server + ecs_domain: + domain_name: FILE.{{ common_name }} + verification_method: web_server + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: file_result + + - assert: + that: + - file_result is not failed + - file_result.changed + - file_result.domain_status == 'INITIAL_VERIFICATION' + - file_result.verification_method == 'web_server' + - file_result.dns_location is undefined + - file_result.dns_contents is undefined + - file_result.dns_resource_type is undefined + - file_result.file_location is string + - file_result.file_contents is string + - file_result.emails is undefined + + - name: Have ECS request a domain validation via email + ecs_domain: + domain_name: email.{{ common_name }} + verification_method: email + verification_email: admin@testcertificates.com + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: email_result + + - assert: + that: + - email_result is not failed + - email_result.changed + - email_result.domain_status == 'INITIAL_VERIFICATION' + - email_result.verification_method == 'email' + - email_result.dns_location is undefined + - email_result.dns_contents is undefined + - email_result.dns_resource_type is undefined + - email_result.file_location is undefined + - email_result.file_contents is undefined + - email_result.emails[0] == 'admin@testcertificates.com' + + - name: Have ECS request a domain validation via email with no address provided + ecs_domain: + domain_name: email2.{{ common_name }} + verification_method: email + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: email_result2 + + - assert: + that: + - email_result2 is not failed + - email_result2.changed + - email_result2.domain_status == 'INITIAL_VERIFICATION' + - email_result2.verification_method == 'email' + - email_result2.dns_location is undefined + - email_result2.dns_contents is undefined + - email_result2.dns_resource_type is undefined + - email_result2.file_location is undefined + - email_result2.file_contents is undefined + - email_result2.emails is defined + + - name: Have ECS request a domain validation via manual + ecs_domain: + domain_name: manual.{{ common_name }} + verification_method: manual + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: manual_result + + - assert: + that: + - manual_result is not failed + - manual_result.changed + - manual_result.domain_status == 'INITIAL_VERIFICATION' + - manual_result.verification_method == 'manual' + - manual_result.dns_location is undefined + - manual_result.dns_contents is undefined + - manual_result.dns_resource_type is undefined + - manual_result.file_location is undefined + - manual_result.file_contents is undefined + - manual_result.emails is undefined + + - name: Have ECS request a domain validation via dns that remains unchanged + ecs_domain: + domain_name: dns.{{ common_name }} + verification_method: dns + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: dns_result2 + + - assert: + that: + - dns_result2 is not failed + - not dns_result2.changed + - dns_result2.domain_status == 'INITIAL_VERIFICATION' + - dns_result2.verification_method == 'dns' + - dns_result2.dns_location is string + - dns_result2.dns_contents is string + - dns_result2.dns_resource_type is string + - dns_result2.file_location is undefined + - dns_result2.file_contents is undefined + - dns_result2.emails is undefined + + - name: Have ECS request a domain validation via FILE for dns, to change verification method + ecs_domain: + domain_name: dns.{{ common_name }} + verification_method: web_server + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: dns_result_now_file + + - assert: + that: + - dns_result_now_file is not failed + - dns_result_now_file.changed + - dns_result_now_file.domain_status == 'INITIAL_VERIFICATION' + - dns_result_now_file.verification_method == 'web_server' + - dns_result_now_file.dns_location is undefined + - dns_result_now_file.dns_contents is undefined + - dns_result_now_file.dns_resource_type is undefined + - dns_result_now_file.file_location is string + - dns_result_now_file.file_contents is string + - dns_result_now_file.emails is undefined + + - name: Request revalidation of an approved domain + ecs_domain: + domain_name: '{{ existing_domain_common_name }}' + verification_method: manual + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: manual_existing_domain + + - assert: + that: + - manual_existing_domain is not failed + - not manual_existing_domain.changed + - manual_existing_domain.domain_status == 'RE_VERIFICATION' + - manual_existing_domain.dns_location is undefined + - manual_existing_domain.dns_contents is undefined + - manual_existing_domain.dns_resource_type is undefined + - manual_existing_domain.file_location is undefined + - manual_existing_domain.file_contents is undefined + - manual_existing_domain.emails is undefined + + - name: Request revalidation of an approved domain + ecs_domain: + domain_name: '{{ existing_domain_common_name }}' + verification_method: web_server + entrust_api_user: '{{ entrust_api_user }}' + entrust_api_key: '{{ entrust_api_key }}' + entrust_api_client_cert_path: '{{ entrust_api_cert }}' + entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}' + register: file_existing_domain_revalidate + + - assert: + that: + - file_existing_domain_revalidate is not failed + - file_existing_domain_revalidate.changed + - file_existing_domain_revalidate.domain_status == 'RE_VERIFICATION' + - file_existing_domain_revalidate.verification_method == 'web_server' + - file_existing_domain_revalidate.dns_location is undefined + - file_existing_domain_revalidate.dns_contents is undefined + - file_existing_domain_revalidate.dns_resource_type is undefined + - file_existing_domain_revalidate.file_location is string + - file_existing_domain_revalidate.file_contents is string + - file_existing_domain_revalidate.emails is undefined + + + always: + - name: clean-up temporary folder + file: + path: '{{ tmpdir_path }}' + state: absent diff --git a/tests/integration/targets/ecs_domain/vars/main.yml b/tests/integration/targets/ecs_domain/vars/main.yml new file mode 100644 index 00000000..9cf9fdb7 --- /dev/null +++ b/tests/integration/targets/ecs_domain/vars/main.yml @@ -0,0 +1,15 @@ +--- +# vars file for test_ecs_certificate + +# Path on various hosts that cacerts need to be put as a prerequisite to API server cert validation. +# May need to be customized for some environments based on SSL implementations +# that ansible "urls" module utility is using as a backing. +cacerts_bundle_path: /etc/pki/tls/certs + +common_name: '{{ ansible_date_time.epoch }}.testcertificates.com' +existing_domain_common_name: 'testcertificates.com' + +tmpdir_path: /tmp/ecs_cert_test/{{ ansible_date_time.epoch }} + +entrust_api_cert: '{{ tmpdir_path }}/authcert.cer' +entrust_api_cert_key: '{{ tmpdir_path }}/authkey.cer' diff --git a/tests/integration/targets/get_certificate/aliases b/tests/integration/targets/get_certificate/aliases new file mode 100644 index 00000000..1dcd7efe --- /dev/null +++ b/tests/integration/targets/get_certificate/aliases @@ -0,0 +1,4 @@ +shippable/posix/group1 +destructive +needs/httptester +skip/aix diff --git a/tests/integration/targets/get_certificate/files/bogus_ca.pem b/tests/integration/targets/get_certificate/files/bogus_ca.pem new file mode 100644 index 00000000..16119c9e --- /dev/null +++ b/tests/integration/targets/get_certificate/files/bogus_ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+DCCAeACCQCWuDvGDH3otTANBgkqhkiG9w0BAQsFADA+MQswCQYDVQQGEwJV +UzEOMAwGA1UECAwFQm9ndXMxEDAOBgNVBAcMB0JhbG9uZXkxDTALBgNVBAoMBEFD +TUUwHhcNMTgwNzEyMTgxNDA0WhcNMjMwNzExMTgxNDA0WjA+MQswCQYDVQQGEwJV +UzEOMAwGA1UECAwFQm9ndXMxEDAOBgNVBAcMB0JhbG9uZXkxDTALBgNVBAoMBEFD +TUUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLTGCpn8b+/2qdpkvK +iwXU8PMOXBOmRa+GmzxsxMr1QZcY0m6pY3uuIvqErMFf4qp4BMxQF+VpDLVJUJX/ +1oKCM7J3hEfgmKRD4RmKhBlnWVv5YGZmvlXRJBl1AsDTONZy8iKJB5NYnB3ZyrJq +H2GAgyJ55aYckoU55vwjRzKp49dZmzX5YS04Kzzzw/SmOuW8kMypZV5TJH+NXqKc +pw3u3cJ4yJ9DHSU5pnhC5BeKl8XDMO42jRWt5/7C7JDiCbZ9lu5jQiv/4DhsRsHF +A8/Lgl47sNDaBMbha786I9laPHLlVycpYaP6pwtizhN9ZRTdDOHmWi/vjiamERLL +FjjLAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAA+1uj3tHaCai+A1H/kOgTN5e0eW +/wmaxu8gNK5eiHrecNJNAlFxVTrCwhvv4nUW7NXVcW/1WUqSO0QMiPJhCsSLVAMF +8MuYH73B+ctRqAGdeOAWF+ftCywZTEj5h5F0XiWB+TmkPlTVNShMiPFelDJpLy7u +9MfiPEJjo4sZotQl8/pZ6R9cY6GpEXWnttcuhLJCEuiB8fWO7epiWYCt/Ak+CVmZ +OzfI/euV6Upaen22lNu8V3ZwWEFtmU5CioKJ3S8DK5Mw/LJIJw1ZY9E+fTtn8x0k +xlI4e7urD2FYhTdv2fFUG8Z5arb/3bICgsUYQZ+G1c3wjWtJg9zcy8hpnZQ= +-----END CERTIFICATE----- diff --git a/tests/integration/targets/get_certificate/files/process_certs.py b/tests/integration/targets/get_certificate/files/process_certs.py new file mode 100644 index 00000000..8a21af71 --- /dev/null +++ b/tests/integration/targets/get_certificate/files/process_certs.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from sys import argv +from subprocess import Popen, PIPE, STDOUT + +p = Popen(["openssl", "s_client", "-host", argv[1], "-port", "443", "-prexit", "-showcerts"], stdin=PIPE, stdout=PIPE, stderr=STDOUT) +stdout = p.communicate(input=b'\n')[0] +data = stdout.decode() + +certs = [] +cert = "" +capturing = False +for line in data.split('\n'): + if line == '-----BEGIN CERTIFICATE-----': + capturing = True + + if capturing: + cert = "{0}{1}\n".format(cert, line) + + if line == '-----END CERTIFICATE-----': + capturing = False + certs.append(cert) + cert = "" + +with open(argv[2], 'w') as f: + for cert in set(certs): + f.write(cert) diff --git a/tests/integration/targets/get_certificate/meta/main.yml b/tests/integration/targets/get_certificate/meta/main.yml new file mode 100644 index 00000000..54be4e6d --- /dev/null +++ b/tests/integration/targets/get_certificate/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_openssl + - prepare_http_tests diff --git a/tests/integration/targets/get_certificate/tasks/main.yml b/tests/integration/targets/get_certificate/tasks/main.yml new file mode 100644 index 00000000..ac06e1f8 --- /dev/null +++ b/tests/integration/targets/get_certificate/tasks/main.yml @@ -0,0 +1,42 @@ +--- +- block: + + - name: Get servers certificate with backend auto-detection + get_certificate: + host: "{{ httpbin_host }}" + port: 443 + + when: | + pyopenssl_version.stdout is version('0.15', '>=') or + (cryptography_version.stdout is version('1.6', '>=') and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)) + +- block: + + - include_tasks: ../tests/validate.yml + vars: + select_crypto_backend: pyopenssl + + when: pyopenssl_version.stdout is version('0.15', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + + - include_tasks: ../tests/validate.yml + vars: + select_crypto_backend: cryptography + + # The module doesn't work with CentOS 6. Since the pyOpenSSL installed there is too old, + # we never noticed before. This becomes a problem with the new cryptography backend, + # since there is a new enough cryptography version... + when: | + cryptography_version.stdout is version('1.6', '>=') and + (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6) diff --git a/tests/integration/targets/get_certificate/tests/validate.yml b/tests/integration/targets/get_certificate/tests/validate.yml new file mode 100644 index 00000000..5dec56c8 --- /dev/null +++ b/tests/integration/targets/get_certificate/tests/validate.yml @@ -0,0 +1,106 @@ +--- +- name: Get servers certificate + get_certificate: + host: "{{ httpbin_host }}" + port: 443 + select_crypto_backend: "{{ select_crypto_backend }}" + register: result + +- debug: var=result + +- assert: + that: + # This module should never change anything + - result is not changed + - result is not failed + # We got the correct ST from the cert + - "'North Carolina' == result.subject.ST" + +- name: Connect to http port (will fail because there is no SSL cert to get) + get_certificate: + host: "{{ httpbin_host }}" + port: 80 + select_crypto_backend: "{{ select_crypto_backend }}" + register: result + ignore_errors: true + +- assert: + that: + - result is not changed + - result is failed + # We got the expected error message + - "'The handshake operation timed out' in result.msg or 'unknown protocol' in result.msg or 'wrong version number' in result.msg" + +- name: Test timeout option + get_certificate: + host: "{{ httpbin_host }}" + port: 1234 + timeout: 1 + select_crypto_backend: "{{ select_crypto_backend }}" + register: result + ignore_errors: true + +- assert: + that: + - result is not changed + - result is failed + # We got the expected error message + - "'Failed to get cert from port with error: timed out' == result.msg or 'Connection refused' in result.msg" + +- name: Test failure if ca_cert is not a valid file + get_certificate: + host: "{{ httpbin_host }}" + port: 443 + ca_cert: dn.e + select_crypto_backend: "{{ select_crypto_backend }}" + register: result + ignore_errors: true + +- assert: + that: + - result is not changed + - result is failed + # We got the correct response from the module + - "'ca_cert file does not exist' == result.msg" + +- name: Download CA Cert as pem from server + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "{{ output_dir }}/temp.pem" + +- name: Get servers certificate comparing it to its own ca_cert file + get_certificate: + ca_cert: '{{ output_dir }}/temp.pem' + host: "{{ httpbin_host }}" + port: 443 + select_crypto_backend: "{{ select_crypto_backend }}" + register: result + +- assert: + that: + - result is not changed + - result is not failed + +- name: Get a temp directory + tempfile: + state: directory + register: my_temp_dir + +- name: Deploy the bogus_ca.pem file + copy: + src: "bogus_ca.pem" + dest: "{{ my_temp_dir.path }}/bogus_ca.pem" + +- name: Get servers certificate comparing it to an invalid ca_cert file + get_certificate: + ca_cert: '{{ my_temp_dir.path }}/bogus_ca.pem' + host: "{{ httpbin_host }}" + port: 443 + select_crypto_backend: "{{ select_crypto_backend }}" + register: result + ignore_errors: true + +- assert: + that: + - result is not changed + - result.failed diff --git a/tests/integration/targets/incidental_setup_openssl/aliases b/tests/integration/targets/incidental_setup_openssl/aliases new file mode 100644 index 00000000..e5830e28 --- /dev/null +++ b/tests/integration/targets/incidental_setup_openssl/aliases @@ -0,0 +1,2 @@ +hidden + diff --git a/tests/integration/targets/incidental_setup_openssl/tasks/main.yml b/tests/integration/targets/incidental_setup_openssl/tasks/main.yml new file mode 100644 index 00000000..5a634458 --- /dev/null +++ b/tests/integration/targets/incidental_setup_openssl/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Include OS-specific variables + include_vars: '{{ ansible_os_family }}.yml' + when: not ansible_os_family == "Darwin" + +- name: Install OpenSSL + become: True + package: + name: '{{ openssl_package_name }}' + when: not ansible_os_family == 'Darwin' + +- name: Install pyOpenSSL (Python 3) + become: True + package: + name: '{{ pyopenssl_package_name_python3 }}' + when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '>=') + +- name: Install pyOpenSSL (Python 2) + become: True + package: + name: '{{ pyopenssl_package_name }}' + when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '<') + +- name: Install pyOpenSSL (Darwin) + become: True + pip: + name: pyOpenSSL + when: ansible_os_family == 'Darwin' + +- name: register pyOpenSSL version + command: "{{ ansible_python.executable }} -c 'import OpenSSL; print(OpenSSL.__version__)'" + register: pyopenssl_version + +- name: register openssl version + shell: "openssl version | cut -d' ' -f2" + register: openssl_version + +- name: register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: cryptography_version diff --git a/tests/integration/targets/incidental_setup_openssl/vars/Debian.yml b/tests/integration/targets/incidental_setup_openssl/vars/Debian.yml new file mode 100644 index 00000000..755c7a08 --- /dev/null +++ b/tests/integration/targets/incidental_setup_openssl/vars/Debian.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: python-openssl +pyopenssl_package_name_python3: python3-openssl +openssl_package_name: openssl diff --git a/tests/integration/targets/incidental_setup_openssl/vars/FreeBSD.yml b/tests/integration/targets/incidental_setup_openssl/vars/FreeBSD.yml new file mode 100644 index 00000000..60868915 --- /dev/null +++ b/tests/integration/targets/incidental_setup_openssl/vars/FreeBSD.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: py27-openssl +pyopenssl_package_name_python3: py36-openssl +openssl_package_name: openssl diff --git a/tests/integration/targets/incidental_setup_openssl/vars/RedHat.yml b/tests/integration/targets/incidental_setup_openssl/vars/RedHat.yml new file mode 100644 index 00000000..2959932c --- /dev/null +++ b/tests/integration/targets/incidental_setup_openssl/vars/RedHat.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: pyOpenSSL +pyopenssl_package_name_python3: python3-pyOpenSSL +openssl_package_name: openssl diff --git a/tests/integration/targets/incidental_setup_openssl/vars/Suse.yml b/tests/integration/targets/incidental_setup_openssl/vars/Suse.yml new file mode 100644 index 00000000..2d5200f3 --- /dev/null +++ b/tests/integration/targets/incidental_setup_openssl/vars/Suse.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: python-pyOpenSSL +pyopenssl_package_name_python3: python3-pyOpenSSL +openssl_package_name: openssl diff --git a/tests/integration/targets/incidental_x509_crl/aliases b/tests/integration/targets/incidental_x509_crl/aliases new file mode 100644 index 00000000..54b54aa5 --- /dev/null +++ b/tests/integration/targets/incidental_x509_crl/aliases @@ -0,0 +1,4 @@ +x509_crl_info +shippable/posix/incidental +destructive +skip/aix diff --git a/tests/integration/targets/incidental_x509_crl/meta/main.yml b/tests/integration/targets/incidental_x509_crl/meta/main.yml new file mode 100644 index 00000000..0b241de3 --- /dev/null +++ b/tests/integration/targets/incidental_x509_crl/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - incidental_setup_openssl diff --git a/tests/integration/targets/incidental_x509_crl/tasks/impl.yml b/tests/integration/targets/incidental_x509_crl/tasks/impl.yml new file mode 100644 index 00000000..eafb2dad --- /dev/null +++ b/tests/integration/targets/incidental_x509_crl/tasks/impl.yml @@ -0,0 +1,289 @@ +--- +- name: Create CRL 1 (check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_check +- name: Create CRL 1 + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1 +- name: Retrieve CRL 1 infos + x509_crl_info: + path: '{{ output_dir }}/ca-crl1.crl' + register: crl_1_info_1 +- name: Retrieve CRL 1 infos via file content + x509_crl_info: + content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") }}' + register: crl_1_info_2 +- name: Create CRL 1 (idempotent, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_idem_check +- name: Create CRL 1 (idempotent) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_idem +- name: Create CRL 1 (idempotent with content, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_content: "{{ lookup('file', output_dir ~ '/ca.key') }}" + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - content: "{{ lookup('file', output_dir ~ '/cert-1.pem') }}" + revocation_date: 20191013000000Z + - content: "{{ lookup('file', output_dir ~ '/cert-2.pem') }}" + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_idem_content_check +- name: Create CRL 1 (idempotent with content) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_content: "{{ lookup('file', output_dir ~ '/ca.key') }}" + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - content: "{{ lookup('file', output_dir ~ '/cert-1.pem') }}" + revocation_date: 20191013000000Z + - content: "{{ lookup('file', output_dir ~ '/cert-2.pem') }}" + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_idem_content + +- name: Create CRL 2 (check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + check_mode: yes + register: crl_2_check +- name: Create CRL 2 + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + register: crl_2 +- name: Create CRL 2 (idempotent, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + ignore_timestamps: yes + check_mode: yes + register: crl_2_idem_check +- name: Create CRL 2 (idempotent) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + ignore_timestamps: yes + register: crl_2_idem +- name: Create CRL 2 (idempotent update, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1235 + ignore_timestamps: yes + mode: update + check_mode: yes + register: crl_2_idem_update_change_check +- name: Create CRL 2 (idempotent update) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1235 + ignore_timestamps: yes + mode: update + register: crl_2_idem_update_change +- name: Create CRL 2 (idempotent update, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: yes + mode: update + check_mode: yes + register: crl_2_idem_update_check +- name: Create CRL 2 (idempotent update) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: yes + mode: update + register: crl_2_idem_update +- name: Create CRL 2 (changed timestamps, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: no + mode: update + check_mode: yes + register: crl_2_change_check +- name: Create CRL 2 (changed timestamps) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: no + mode: update + return_content: yes + register: crl_2_change diff --git a/tests/integration/targets/incidental_x509_crl/tasks/main.yml b/tests/integration/targets/incidental_x509_crl/tasks/main.yml new file mode 100644 index 00000000..1f82ff9e --- /dev/null +++ b/tests/integration/targets/incidental_x509_crl/tasks/main.yml @@ -0,0 +1,83 @@ +--- +- set_fact: + certificates: + - name: ca + subject: + commonName: Ansible + is_ca: yes + - name: ca-2 + subject: + commonName: Ansible Other CA + is_ca: yes + - name: cert-1 + subject_alt_name: + - DNS:ansible.com + - name: cert-2 + subject_alt_name: + - DNS:example.com + - name: cert-3 + subject_alt_name: + - DNS:example.org + - IP:1.2.3.4 + - name: cert-4 + subject_alt_name: + - DNS:test.ansible.com + - DNS:b64.ansible.com + +- name: Generate private keys + openssl_privatekey: + path: '{{ output_dir }}/{{ item.name }}.key' + type: ECC + curve: secp256r1 + loop: "{{ certificates }}" + +- name: Generate CSRs + openssl_csr: + path: '{{ output_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ output_dir }}/{{ item.name }}.key' + subject: "{{ item.subject | default(omit) }}" + subject_alt_name: "{{ item.subject_alt_name | default(omit) }}" + basic_constraints: "{{ 'CA:TRUE' if item.is_ca | default(false) else omit }}" + use_common_name_for_san: no + loop: "{{ certificates }}" + +- name: Generate CA certificates + openssl_certificate: + path: '{{ output_dir }}/{{ item.name }}.pem' + csr_path: '{{ output_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ output_dir }}/{{ item.name }}.key' + provider: selfsigned + loop: "{{ certificates }}" + when: item.is_ca | default(false) + +- name: Generate other certificates + openssl_certificate: + path: '{{ output_dir }}/{{ item.name }}.pem' + csr_path: '{{ output_dir }}/{{ item.name }}.csr' + provider: ownca + ownca_path: '{{ output_dir }}/ca.pem' + ownca_privatekey_path: '{{ output_dir }}/ca.key' + loop: "{{ certificates }}" + when: not (item.is_ca | default(false)) + +- name: Get certificate infos + openssl_certificate_info: + path: '{{ output_dir }}/{{ item }}.pem' + loop: + - cert-1 + - cert-2 + - cert-3 + - cert-4 + register: certificate_infos + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.2', '>=') diff --git a/tests/integration/targets/incidental_x509_crl/tests/validate.yml b/tests/integration/targets/incidental_x509_crl/tests/validate.yml new file mode 100644 index 00000000..17b31f34 --- /dev/null +++ b/tests/integration/targets/incidental_x509_crl/tests/validate.yml @@ -0,0 +1,61 @@ +--- +- name: Validate CRL 1 + assert: + that: + - crl_1_check is changed + - crl_1 is changed + - crl_1_idem_check is not changed + - crl_1_idem is not changed + - crl_1_idem_content_check is not changed + - crl_1_idem_content is not changed + +- name: Validate CRL 1 info + assert: + that: + - crl_1_info_1 == crl_1_info_2 + - crl_1_info_1.digest == 'ecdsa-with-SHA256' + - crl_1_info_1.issuer | length == 1 + - crl_1_info_1.issuer.commonName == 'Ansible' + - crl_1_info_1.issuer_ordered | length == 1 + - crl_1_info_1.last_update == '20191013000000Z' + - crl_1_info_1.next_update == '20191113000000Z' + - crl_1_info_1.revoked_certificates | length == 3 + - crl_1_info_1.revoked_certificates[0].invalidity_date is none + - crl_1_info_1.revoked_certificates[0].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[0].issuer is none + - crl_1_info_1.revoked_certificates[0].issuer_critical == false + - crl_1_info_1.revoked_certificates[0].reason is none + - crl_1_info_1.revoked_certificates[0].reason_critical == false + - crl_1_info_1.revoked_certificates[0].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[0].serial_number == certificate_infos.results[0].serial_number + - crl_1_info_1.revoked_certificates[1].invalidity_date == '20191012000000Z' + - crl_1_info_1.revoked_certificates[1].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[1].issuer is none + - crl_1_info_1.revoked_certificates[1].issuer_critical == false + - crl_1_info_1.revoked_certificates[1].reason == 'key_compromise' + - crl_1_info_1.revoked_certificates[1].reason_critical == true + - crl_1_info_1.revoked_certificates[1].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[1].serial_number == certificate_infos.results[1].serial_number + - crl_1_info_1.revoked_certificates[2].invalidity_date is none + - crl_1_info_1.revoked_certificates[2].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[2].issuer is none + - crl_1_info_1.revoked_certificates[2].issuer_critical == false + - crl_1_info_1.revoked_certificates[2].reason is none + - crl_1_info_1.revoked_certificates[2].reason_critical == false + - crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z' + - crl_1_info_1.revoked_certificates[2].serial_number == 1234 + +- name: Validate CRL 2 + assert: + that: + - crl_2_check is changed + - crl_2 is changed + - crl_2_idem_check is not changed + - crl_2_idem is not changed + - crl_2_idem_update_change_check is changed + - crl_2_idem_update_change is changed + - crl_2_idem_update_check is not changed + - crl_2_idem_update is not changed + - crl_2_change_check is changed + - crl_2_change is changed + - crl_2_change.crl == lookup('file', output_dir ~ '/ca-crl2.crl', rstrip=False) diff --git a/tests/integration/targets/luks_device/aliases b/tests/integration/targets/luks_device/aliases new file mode 100644 index 00000000..f9e4266f --- /dev/null +++ b/tests/integration/targets/luks_device/aliases @@ -0,0 +1,7 @@ +shippable/posix/group4 +skip/aix +skip/osx +skip/freebsd +skip/docker +needs/root +destructive diff --git a/tests/integration/targets/luks_device/files/keyfile1 b/tests/integration/targets/luks_device/files/keyfile1 new file mode 100644 index 00000000..5e40c087 --- /dev/null +++ b/tests/integration/targets/luks_device/files/keyfile1 @@ -0,0 +1 @@ +asdf \ No newline at end of file diff --git a/tests/integration/targets/luks_device/files/keyfile2 b/tests/integration/targets/luks_device/files/keyfile2 new file mode 100644 index 00000000..5e4f2565 --- /dev/null +++ b/tests/integration/targets/luks_device/files/keyfile2 @@ -0,0 +1 @@ +test1234 \ No newline at end of file diff --git a/tests/integration/targets/luks_device/tasks/main.yml b/tests/integration/targets/luks_device/tasks/main.yml new file mode 100644 index 00000000..d3ca75b6 --- /dev/null +++ b/tests/integration/targets/luks_device/tasks/main.yml @@ -0,0 +1,36 @@ +--- +- name: Make sure cryptsetup is installed + package: + name: cryptsetup + state: present + become: yes +- name: Create cryptfile + command: dd if=/dev/zero of={{ output_dir.replace('~', ansible_env.HOME) }}/cryptfile bs=1M count=32 +- name: Create lookback device + command: losetup -f {{ output_dir.replace('~', ansible_env.HOME) }}/cryptfile + become: yes +- name: Determine loop device name + command: losetup -j {{ output_dir.replace('~', ansible_env.HOME) }}/cryptfile --output name + become: yes + register: cryptfile_device_output +- set_fact: + cryptfile_device: "{{ cryptfile_device_output.stdout_lines[1] }}" + cryptfile_passphrase1: "uNiJ9vKG2mUOEWDiQVuBHJlfMHE" + cryptfile_passphrase2: "HW4Ak2HtE2vvne0qjJMPTtmbV4M" + cryptfile_passphrase3: "qQJqsjabO9pItV792k90VvX84MM" +- block: + - include_tasks: run-test.yml + with_fileglob: + - "tests/*.yml" + always: + - name: Make sure LUKS device is gone + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: yes + ignore_errors: yes + - command: losetup -d "{{ cryptfile_device }}" + become: yes + - file: + dest: "{{ output_dir }}/cryptfile" + state: absent diff --git a/tests/integration/targets/luks_device/tasks/run-test.yml b/tests/integration/targets/luks_device/tasks/run-test.yml new file mode 100644 index 00000000..a2ec73b2 --- /dev/null +++ b/tests/integration/targets/luks_device/tasks/run-test.yml @@ -0,0 +1,8 @@ +--- +- name: Make sure LUKS device is gone + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: yes +- name: "Loading tasks from {{ item }}" + include_tasks: "{{ item }}" diff --git a/tests/integration/targets/luks_device/tasks/tests/create-destroy.yml b/tests/integration/targets/luks_device/tasks/tests/create-destroy.yml new file mode 100644 index 00000000..6ac4d1c3 --- /dev/null +++ b/tests/integration/targets/luks_device/tasks/tests/create-destroy.yml @@ -0,0 +1,187 @@ +--- +- name: Create (check) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + check_mode: yes + become: yes + register: create_check +- name: Create + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + register: create +- name: Create (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + register: create_idem +- name: Create (idempotent, check) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + check_mode: yes + become: yes + register: create_idem_check +- assert: + that: + - create_check is changed + - create is changed + - create_idem is not changed + - create_idem_check is not changed + +- name: Open (check) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + check_mode: yes + become: yes + register: open_check +- name: Open + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + register: open +- name: Open (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + register: open_idem +- name: Open (idempotent, check) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + check_mode: yes + become: yes + register: open_idem_check +- assert: + that: + - open_check is changed + - open is changed + - open_idem is not changed + - open_idem_check is not changed + +- name: Closed (via name, check) + luks_device: + name: "{{ open.name }}" + state: closed + check_mode: yes + become: yes + register: close_check +- name: Closed (via name) + luks_device: + name: "{{ open.name }}" + state: closed + become: yes + register: close +- name: Closed (via name, idempotent) + luks_device: + name: "{{ open.name }}" + state: closed + become: yes + register: close_idem +- name: Closed (via name, idempotent, check) + luks_device: + name: "{{ open.name }}" + state: closed + check_mode: yes + become: yes + register: close_idem_check +- assert: + that: + - close_check is changed + - close is changed + - close_idem is not changed + - close_idem_check is not changed + +- name: Re-open + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + +- name: Closed (via device, check) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + check_mode: yes + become: yes + register: close_check +- name: Closed (via device) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + register: close +- name: Closed (via device, idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + register: close_idem +- name: Closed (via device, idempotent, check) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + check_mode: yes + become: yes + register: close_idem_check +- assert: + that: + - close_check is changed + - close is changed + - close_idem is not changed + - close_idem_check is not changed + +- name: Re-opened + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + +- name: Absent (check) + luks_device: + device: "{{ cryptfile_device }}" + state: absent + check_mode: yes + become: yes + register: absent_check +- name: Absent + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: yes + register: absent +- name: Absent (idempotence) + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: yes + register: absent_idem +- name: Absent (idempotence, check) + luks_device: + device: "{{ cryptfile_device }}" + state: absent + check_mode: yes + become: yes + register: absent_idem_check +- assert: + that: + - absent_check is changed + - absent is changed + - absent_idem is not changed + - absent_idem_check is not changed diff --git a/tests/integration/targets/luks_device/tasks/tests/device-check.yml b/tests/integration/targets/luks_device/tasks/tests/device-check.yml new file mode 100644 index 00000000..3682a33e --- /dev/null +++ b/tests/integration/targets/luks_device/tasks/tests/device-check.yml @@ -0,0 +1,48 @@ +--- +- name: Create with invalid device name (check) + luks_device: + device: /dev/asdfasdfasdf + state: present + keyfile: "{{ role_path }}/files/keyfile1" + check_mode: yes + ignore_errors: yes + become: yes + register: create_check +- name: Create with invalid device name + luks_device: + device: /dev/asdfasdfasdf + state: present + keyfile: "{{ role_path }}/files/keyfile1" + ignore_errors: yes + become: yes + register: create +- assert: + that: + - create_check is failed + - create is failed + - "'o such file or directory' in create_check.msg" + - "'o such file or directory' in create.msg" + +- name: Create with something which is not a device (check) + luks_device: + device: /tmp/ + state: present + keyfile: "{{ role_path }}/files/keyfile1" + check_mode: yes + ignore_errors: yes + become: yes + register: create_check +- name: Create with something which is not a device + luks_device: + device: /tmp/ + state: present + keyfile: "{{ role_path }}/files/keyfile1" + ignore_errors: yes + become: yes + register: create +- assert: + that: + - create_check is failed + - create is failed + - "'is not a device' in create_check.msg" + - "'is not a device' in create.msg" diff --git a/tests/integration/targets/luks_device/tasks/tests/key-management.yml b/tests/integration/targets/luks_device/tasks/tests/key-management.yml new file mode 100644 index 00000000..b89d505f --- /dev/null +++ b/tests/integration/targets/luks_device/tasks/tests/key-management.yml @@ -0,0 +1,168 @@ +--- +- name: Create with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + +# Access: keyfile1 + +- name: Try to open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile2" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ role_path }}/files/keyfile1" + new_keyfile: "{{ role_path }}/files/keyfile2" + become: yes + +# Access: keyfile1 and keyfile2 + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile2" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + +- name: Dump LUKS header + command: "cryptsetup luksDump {{ cryptfile_device }}" + become: yes + +- name: Remove access from keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ role_path }}/files/keyfile1" + remove_keyfile: "{{ role_path }}/files/keyfile1" + become: yes + +# Access: keyfile2 + +- name: Try to open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is failed + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile2" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + +- name: Dump LUKS header + command: "cryptsetup luksDump {{ cryptfile_device }}" + become: yes + +- name: Remove access from keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ role_path }}/files/keyfile2" + remove_keyfile: "{{ role_path }}/files/keyfile2" + become: yes + ignore_errors: yes + register: remove_last_key +- assert: + that: + - remove_last_key is failed + - "'force_remove_last_key' in remove_last_key.msg" + +# Access: keyfile2 + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile2" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + +- name: Remove access from keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ role_path }}/files/keyfile2" + remove_keyfile: "{{ role_path }}/files/keyfile2" + force_remove_last_key: yes + become: yes + +# Access: none + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile2" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is failed diff --git a/tests/integration/targets/luks_device/tasks/tests/options.yml b/tests/integration/targets/luks_device/tasks/tests/options.yml new file mode 100644 index 00000000..afee6667 --- /dev/null +++ b/tests/integration/targets/luks_device/tasks/tests/options.yml @@ -0,0 +1,41 @@ +--- +- name: Create with keysize + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + keysize: 256 + become: yes + register: create_with_keysize +- name: Create with keysize (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + keysize: 256 + become: yes + register: create_idem_with_keysize +- name: Create with different keysize (idempotent since we do not update keysize) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + keysize: 512 + become: yes + register: create_idem_with_diff_keysize +- name: Create with ambiguous arguments + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ role_path }}/files/keyfile1" + passphrase: "{{ cryptfile_passphrase1 }}" + ignore_errors: yes + become: yes + register: create_with_ambiguous + +- assert: + that: + - create_with_keysize is changed + - create_idem_with_keysize is not changed + - create_idem_with_diff_keysize is not changed + - create_with_ambiguous is failed diff --git a/tests/integration/targets/luks_device/tasks/tests/passphrase.yml b/tests/integration/targets/luks_device/tasks/tests/passphrase.yml new file mode 100644 index 00000000..56091525 --- /dev/null +++ b/tests/integration/targets/luks_device/tasks/tests/passphrase.yml @@ -0,0 +1,181 @@ +--- +- name: Create with passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + become: yes + +- name: Open with passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase1 }}" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + +- name: Give access with ambiguous new_ arguments + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + new_passphrase: "{{ cryptfile_passphrase2 }}" + new_keyfile: "{{ role_path }}/files/keyfile1" + become: yes + ignore_errors: yes + register: new_try +- assert: + that: + - new_try is failed + +- name: Try to open with passphrase2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase2 }}" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to passphrase2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + new_passphrase: "{{ cryptfile_passphrase2 }}" + become: yes + +- name: Open with passphrase2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase2 }}" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + +- name: Try to open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to keyfile1 from passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + new_keyfile: "{{ role_path }}/files/keyfile1" + become: yes + +- name: Remove access with ambiguous remove_ arguments + luks_device: + device: "{{ cryptfile_device }}" + state: closed + remove_keyfile: "{{ role_path }}/files/keyfile1" + remove_passphrase: "{{ cryptfile_passphrase1 }}" + become: yes + ignore_errors: yes + register: remove_try +- assert: + that: + - remove_try is failed + +- name: Open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ role_path }}/files/keyfile1" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes + +- name: Remove access for passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + remove_passphrase: "{{ cryptfile_passphrase1 }}" + become: yes + +- name: Try to open with passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase1 }}" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is failed + +- name: Try to open with passphrase3 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase3 }}" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to passphrase3 from keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ role_path }}/files/keyfile1" + new_passphrase: "{{ cryptfile_passphrase3 }}" + become: yes + +- name: Open with passphrase3 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase3 }}" + become: yes + ignore_errors: yes + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: yes diff --git a/tests/integration/targets/openssh_cert/aliases b/tests/integration/targets/openssh_cert/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssh_cert/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssh_cert/meta/main.yml b/tests/integration/targets/openssh_cert/meta/main.yml new file mode 100644 index 00000000..dc973f4e --- /dev/null +++ b/tests/integration/targets/openssh_cert/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_ssh_keygen diff --git a/tests/integration/targets/openssh_cert/tasks/main.yml b/tests/integration/targets/openssh_cert/tasks/main.yml new file mode 100644 index 00000000..5e4606b9 --- /dev/null +++ b/tests/integration/targets/openssh_cert/tasks/main.yml @@ -0,0 +1,408 @@ +- name: openssh_cert integration tests + when: not (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6") + block: + - name: Generate keypair (check mode) + openssh_keypair: + path: '{{ output_dir }}/id_key' + type: rsa + check_mode: yes + - name: Generate keypair + openssh_keypair: + path: '{{ output_dir }}/id_key' + type: rsa + - name: Generate keypair (idempotent) + openssh_keypair: + path: '{{ output_dir }}/id_key' + type: rsa + - name: Generate keypair (idempotent, check mode) + openssh_keypair: + path: '{{ output_dir }}/id_key' + type: rsa + check_mode: yes + - name: Generate always valid cert (check mode) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + check_mode: yes + - name: Generate always valid cert + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + - name: Generate always valid cert (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + - name: Generate always valid cert (idempotent, check mode) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + check_mode: yes + - name: Generate restricted validity cert with valid_at (check mode) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: +0s + valid_to: +32w + valid_at: +2w + check_mode: yes + - name: Generate restricted validity cert with valid_at + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: +0s + valid_to: +32w + valid_at: +2w + - name: Generate restricted validity cert with valid_at (idempotent) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: +0s + valid_to: +32w + valid_at: +2w + - name: Generate restricted validity cert with valid_at (idempotent, check mode) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: +0s + valid_to: +32w + valid_at: +2w + check_mode: yes + - name: Generate always valid cert only for example.com and examplehost (check mode) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + principals: + - example.com + - examplehost + check_mode: yes + - name: Generate always valid cert only for example.com and examplehost + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + principals: + - example.com + - examplehost + - name: Generate always valid cert only for example.com and examplehost (idempotent) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + principals: + - example.com + - examplehost + - name: Generate always valid cert only for example.com and examplehost (idempotent, check mode) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + principals: + - example.com + - examplehost + check_mode: yes + - name: Generate always valid cert only for example.com and examplehost (idempotent, switch) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: always + valid_to: forever + principals: + - examplehost + - example.com + - name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 (check mode) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: yes + - name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: "2001-01-21" + valid_to: "2019-01-21" + - name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 (idempotent) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: "2001-01-21" + valid_to: "2019-01-21" + - name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 (idempotent, check mode) + openssh_cert: + type: host + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: yes + - name: Generate an OpenSSH user Certificate with clear and force-command option (check mode) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + options: + - "clear" + - "force-command=/tmp/bla/foo" + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: yes + - name: Generate an OpenSSH user Certificate with clear and force-command option + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + options: + - "clear" + - "force-command=/tmp/bla/foo" + valid_from: "2001-01-21" + valid_to: "2019-01-21" + - name: Generate an OpenSSH user Certificate with clear and force-command option (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + options: + - "clear" + - "force-command=/tmp/bla/foo" + valid_from: "2001-01-21" + valid_to: "2019-01-21" + - name: Generate an OpenSSH user Certificate with clear and force-command option (idempotent, check mode) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + options: + - "clear" + - "force-command=/tmp/bla/foo" + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: yes + - name: Generate an OpenSSH user Certificate with clear and force-command option (idempotent, switch) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert' + options: + - "force-command=/tmp/bla/foo" + - "clear" + valid_from: "2001-01-21" + valid_to: "2019-01-21" + - name: Generate cert without serial + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_no_serial' + valid_from: always + valid_to: forever + register: rc_no_serial_number + - name: check default serial + assert: + that: + - "'Serial: 0' in rc_no_serial_number.info" + msg: OpenSSH user certificate contains the default serial number. + - name: Generate cert without serial (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_no_serial' + valid_from: always + valid_to: forever + register: rc_no_serial_number_idempotent + - name: check idempotent + assert: + that: + - rc_no_serial_number_idempotent is not changed + msg: OpenSSH certificate generation without serial number is idempotent. + - name: Generate cert with serial 42 + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 42 + register: rc_serial_number + - name: check serial 42 + assert: + that: + - "'Serial: 42' in rc_serial_number.info" + msg: OpenSSH user certificate contains the serial number from the params. + - name: Generate cert with serial 42 (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 42 + register: rc_serial_number_idempotent + - name: check idempotent + assert: + that: + - rc_serial_number_idempotent is not changed + msg: OpenSSH certificate generation with serial number is idempotent. + - name: Generate cert with changed serial number + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 1337 + register: rc_serial_number_changed + - name: check changed + assert: + that: + - rc_serial_number_changed is changed + msg: OpenSSH certificate regenerated upon serial number change. + - name: Generate cert with removed serial number + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 0 + register: rc_serial_number_removed + - name: check changed + assert: + that: + - rc_serial_number_removed is changed + msg: OpenSSH certificate regenerated upon serial number removal. + - name: Generate a new cert with serial number + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_ignore' + valid_from: always + valid_to: forever + serial_number: 42 + - name: Generate cert again, omitting the parameter serial_number (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_ignore' + valid_from: always + valid_to: forever + register: rc_serial_number_ignored + - name: check idempotent + assert: + that: + - rc_serial_number_ignored is not changed + msg: OpenSSH certificate generation with omitted serial number is idempotent. + - name: Remove certificate (check mode) + openssh_cert: + state: absent + path: '{{ output_dir }}/id_cert' + #type: user + #signing_key: '{{ output_dir }}/id_key' + #public_key: '{{ output_dir }}/id_key.pub' + #valid_from: "2001-01-21" + #valid_to: "2019-01-21" + check_mode: yes + - name: Remove certificate + openssh_cert: + state: absent + path: '{{ output_dir }}/id_cert' + #type: user + #signing_key: '{{ output_dir }}/id_key' + #public_key: '{{ output_dir }}/id_key.pub' + #valid_from: "2001-01-21" + #valid_to: "2019-01-21" + - name: Remove certificate (idempotent) + openssh_cert: + state: absent + path: '{{ output_dir }}/id_cert' + #type: user + #signing_key: '{{ output_dir }}/id_key' + #public_key: '{{ output_dir }}/id_key.pub' + #valid_from: "2001-01-21" + #valid_to: "2019-01-21" + - name: Remove certificate (idempotent, check mode) + openssh_cert: + state: absent + path: '{{ output_dir }}/id_cert' + #type: user + #signing_key: '{{ output_dir }}/id_key' + #public_key: '{{ output_dir }}/id_key.pub' + #valid_from: "2001-01-21" + #valid_to: "2019-01-21" + check_mode: yes + - name: Remove keypair (check mode) + openssh_keypair: + path: '{{ output_dir }}/id_key' + state: absent + check_mode: yes + - name: Remove keypair + openssh_keypair: + path: '{{ output_dir }}/id_key' + state: absent + - name: Remove keypair (idempotent) + openssh_keypair: + path: '{{ output_dir }}/id_key' + state: absent + - name: Remove keypair (idempotent, check mode) + openssh_keypair: + path: '{{ output_dir }}/id_key' + state: absent + check_mode: yes diff --git a/tests/integration/targets/openssh_keypair/aliases b/tests/integration/targets/openssh_keypair/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssh_keypair/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssh_keypair/meta/main.yml b/tests/integration/targets/openssh_keypair/meta/main.yml new file mode 100644 index 00000000..dc973f4e --- /dev/null +++ b/tests/integration/targets/openssh_keypair/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_ssh_keygen diff --git a/tests/integration/targets/openssh_keypair/tasks/main.yml b/tests/integration/targets/openssh_keypair/tasks/main.yml new file mode 100644 index 00000000..3458f8ec --- /dev/null +++ b/tests/integration/targets/openssh_keypair/tasks/main.yml @@ -0,0 +1,375 @@ +--- +- name: Generate privatekey1 - standard + openssh_keypair: + path: '{{ output_dir }}/privatekey1' + register: privatekey1_result + +- name: Generate privatekey1 - standard (idempotent) + openssh_keypair: + path: '{{ output_dir }}/privatekey1' + register: privatekey1_idem_result + +- name: Generate privatekey2 - size 2048 + openssh_keypair: + path: '{{ output_dir }}/privatekey2' + size: 2048 + +- name: Generate privatekey3 - type dsa + openssh_keypair: + path: '{{ output_dir }}/privatekey3' + type: dsa + +- name: Generate privatekey4 - standard + openssh_keypair: + path: '{{ output_dir }}/privatekey4' + +- name: Delete privatekey4 - standard + openssh_keypair: + state: absent + path: '{{ output_dir }}/privatekey4' + +- name: Generate privatekey5 - standard + openssh_keypair: + path: '{{ output_dir }}/privatekey5' + register: publickey_gen + +- name: Generate privatekey6 + openssh_keypair: + path: '{{ output_dir }}/privatekey6' + type: rsa + +- name: Regenerate privatekey6 via force + openssh_keypair: + path: '{{ output_dir }}/privatekey6' + type: rsa + force: yes + register: output_regenerated_via_force + +- name: Create broken key + copy: + dest: '{{ item }}' + content: '' + mode: '0700' + loop: + - '{{ output_dir }}/privatekeybroken' + - '{{ output_dir }}/privatekeybroken.pub' + +- name: Regenerate broken key - should fail + openssh_keypair: + path: '{{ output_dir }}/privatekeybroken' + type: rsa + register: output_broken + ignore_errors: yes + +- name: Regenerate broken key with force + openssh_keypair: + path: '{{ output_dir }}/privatekeybroken' + type: rsa + force: yes + register: output_broken_force + +- name: Generate read-only private key + openssh_keypair: + path: '{{ output_dir }}/privatekeyreadonly' + type: rsa + mode: '0200' + +- name: Regenerate read-only private key via force + openssh_keypair: + path: '{{ output_dir }}/privatekeyreadonly' + type: rsa + force: yes + register: output_read_only + +- name: Generate privatekey7 - standard with comment + openssh_keypair: + path: '{{ output_dir }}/privatekey7' + comment: 'test@privatekey7' + register: privatekey7_result + +- name: Modify privatekey7 comment + openssh_keypair: + path: '{{ output_dir }}/privatekey7' + comment: 'test_modified@privatekey7' + register: privatekey7_modified_result + +- name: Generate password protected key + command: 'ssh-keygen -f {{ output_dir }}/privatekey8 -N password' + +- name: Try to modify the password protected key - should fail + openssh_keypair: + path: '{{ output_dir }}/privatekey8' + register: privatekey8_result + ignore_errors: yes + +- name: Try to modify the password protected key with force=yes + openssh_keypair: + path: '{{ output_dir }}/privatekey8' + force: yes + register: privatekey8_result_force + +- import_tasks: ../tests/validate.yml + + +# Test regenerate option + +- name: Regenerate - setup simple keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + loop: "{{ regenerate_values }}" +- name: Regenerate - setup password protected keys + command: 'ssh-keygen -f {{ output_dir }}/regenerate-b-{{ item }} -N password' + loop: "{{ regenerate_values }}" +- name: Regenerate - setup broken keys + copy: + dest: '{{ output_dir }}/regenerate-c-{{ item.0 }}{{ item.1 }}' + content: 'broken key' + mode: '0700' + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + +- name: Regenerate - modify broken keys (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-c-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify broken keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-c-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-b-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-b-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - adjust key size (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1048 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key size + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1048 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always{{ item.1 }}' + dest: '{{ output_dir }}/regenerate-a-{{ item.0 }}{{ item.1 }}' + remote_src: true + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + when: "item.0 != 'always'" + +- name: Regenerate - adjust key type (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key type + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always{{ item.1 }}' + dest: '{{ output_dir }}/regenerate-a-{{ item.0 }}{{ item.1 }}' + remote_src: true + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + when: "item.0 != 'always'" + +- name: Regenerate - adjust comment (check mode) + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + comment: test comment + regenerate: '{{ item }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result is changed + +- name: Regenerate - adjust comment + openssh_keypair: + path: '{{ output_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + comment: test comment + regenerate: '{{ item }}' + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result is changed + # for all values but 'always', the key should have not been regenerated. + # verify this by comparing fingerprints: + - result.results[0].fingerprint == result.results[1].fingerprint + - result.results[0].fingerprint == result.results[2].fingerprint + - result.results[0].fingerprint == result.results[3].fingerprint + - result.results[0].fingerprint != result.results[4].fingerprint diff --git a/tests/integration/targets/openssh_keypair/tests/validate.yml b/tests/integration/targets/openssh_keypair/tests/validate.yml new file mode 100644 index 00000000..9f34ab22 --- /dev/null +++ b/tests/integration/targets/openssh_keypair/tests/validate.yml @@ -0,0 +1,132 @@ +--- +- name: Log privatekey1 return values + debug: + var: privatekey1_result + +- name: Validate privatekey1 return fingerprint + assert: + that: + - privatekey1_result["fingerprint"] is string + - privatekey1_result["fingerprint"].startswith("SHA256:") + # only distro old enough that it still gives md5 with no prefix + when: ansible_distribution != 'CentOS' and ansible_distribution_major_version != '6' + +- name: Validate privatekey1 return public_key + assert: + that: + - privatekey1_result["public_key"] is string + - privatekey1_result["public_key"].startswith("ssh-rsa ") + +- name: Validate privatekey1 return size value + assert: + that: + - privatekey1_result["size"]|type_debug == 'int' + - privatekey1_result["size"] == 4096 + +- name: Validate privatekey1 return key type + assert: + that: + - privatekey1_result["type"] is string + - privatekey1_result["type"] == "rsa" + +- name: Validate privatekey1 (test - RSA key with size 4096 bits) + shell: "ssh-keygen -lf {{ output_dir }}/privatekey1 | grep -o -E '^[0-9]+'" + register: privatekey1 + +- name: Validate privatekey1 (assert - RSA key with size 4096 bits) + assert: + that: + - privatekey1.stdout == '4096' + +- name: Validate privatekey1 idempotence + assert: + that: + - privatekey1_idem_result is not changed + + +- name: Validate privatekey2 (test - RSA key with size 2048 bits) + shell: "ssh-keygen -lf {{ output_dir }}/privatekey2 | grep -o -E '^[0-9]+'" + register: privatekey2 + +- name: Validate privatekey2 (assert - RSA key with size 2048 bits) + assert: + that: + - privatekey2.stdout == '2048' + + +- name: Validate privatekey3 (test - DSA key with size 1024 bits) + shell: "ssh-keygen -lf {{ output_dir }}/privatekey3 | grep -o -E '^[0-9]+'" + register: privatekey3 + +- name: Validate privatekey3 (assert - DSA key with size 4096 bits) + assert: + that: + - privatekey3.stdout == '1024' + + +- name: Validate privatekey4 (test - Ensure key has been removed) + stat: + path: '{{ output_dir }}/privatekey4' + register: privatekey4 + +- name: Validate privatekey4 (assert - Ensure key has been removed) + assert: + that: + - privatekey4.stat.exists == False + + +- name: Validate privatekey5 (assert - Public key module output equal to the public key on host) + assert: + that: + - "publickey_gen.public_key == lookup('file', output_dir ~ '/privatekey5.pub').strip('\n')" + +- name: Verify that privatekey6 will be regenerated via force + assert: + that: + - output_regenerated_via_force is changed + + +- name: Verify that broken key will cause failure + assert: + that: + - output_broken is failed + - "'Unable to read the key. The key is protected with a passphrase or broken.' in output_broken.msg" + + +- name: Verify that broken key will be regenerated if force=yes is specified + assert: + that: + - output_broken_force is changed + + +- name: Verify that read-only key will be regenerated + assert: + that: + - output_read_only is changed + + +- name: Validate privatekey7 (assert - Public key remains the same after comment change) + assert: + that: + - privatekey7_result.public_key == privatekey7_modified_result.public_key + +- name: Validate privatekey7 comment on creation + assert: + that: + - privatekey7_result.comment == 'test@privatekey7' + +- name: Validate privatekey7 comment update + assert: + that: + - privatekey7_modified_result.comment == 'test_modified@privatekey7' + +- name: Check that password protected key made module fail + assert: + that: + - privatekey8_result is failed + - "'Unable to read the key. The key is protected with a passphrase or broken.' in privatekey8_result.msg" + +- name: Check that password protected key was regenerated with force=yes + assert: + that: + - privatekey8_result_force is changed diff --git a/tests/integration/targets/openssh_keypair/vars/main.yml b/tests/integration/targets/openssh_keypair/vars/main.yml new file mode 100644 index 00000000..81eb611f --- /dev/null +++ b/tests/integration/targets/openssh_keypair/vars/main.yml @@ -0,0 +1,7 @@ +--- +regenerate_values: + - never + - fail + - partial_idempotence + - full_idempotence + - always diff --git a/tests/integration/targets/openssl_certificate/aliases b/tests/integration/targets/openssl_certificate/aliases new file mode 100644 index 00000000..d339371c --- /dev/null +++ b/tests/integration/targets/openssl_certificate/aliases @@ -0,0 +1,3 @@ +shippable/posix/group5 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_certificate/meta/main.yml b/tests/integration/targets/openssl_certificate/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_certificate/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_certificate/tasks/assertonly.yml b/tests/integration/targets/openssl_certificate/tasks/assertonly.yml new file mode 100644 index 00000000..994e00df --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tasks/assertonly.yml @@ -0,0 +1,152 @@ +--- +- name: (Assertonly, {{select_crypto_backend}}) - Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey.pem' + +- name: (Assertonly, {{select_crypto_backend}}) - Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: (Assertonly, {{select_crypto_backend}}) - Generate CSR (no extensions) + openssl_csr: + path: '{{ output_dir }}/csr_noext.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + useCommonNameForSAN: no + +- name: (Assertonly, {{select_crypto_backend}}) - Generate CSR (with SANs) + openssl_csr: + path: '{{ output_dir }}/csr_sans.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + subject_alt_name: + - "DNS:ansible.com" + - "IP:127.0.0.1" + - "IP:::1" + useCommonNameForSAN: no + +- name: (Assertonly, {{select_crypto_backend}}) - Generate selfsigned certificate (no extensions) + openssl_certificate: + path: '{{ output_dir }}/cert_noext.pem' + csr_path: '{{ output_dir }}/csr_noext.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Assertonly, {{select_crypto_backend}}) - Generate selfsigned certificate (with SANs) + openssl_certificate: + path: '{{ output_dir }}/cert_sans.pem' + csr_path: '{{ output_dir }}/csr_sans.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Assertonly, {{select_crypto_backend}}) - Assert that subject_alt_name is there (should fail) + openssl_certificate: + path: '{{ output_dir }}/cert_noext.pem' + provider: assertonly + subject_alt_name: + - "DNS:example.com" + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: extension_missing_san + +- name: (Assertonly, {{select_crypto_backend}}) - Assert that subject_alt_name is there + openssl_certificate: + path: '{{ output_dir }}/cert_sans.pem' + provider: assertonly + subject_alt_name: + - "DNS:ansible.com" + - "IP:127.0.0.1" + - "IP:::1" + select_crypto_backend: '{{ select_crypto_backend }}' + register: extension_san + +- name: (Assertonly, {{select_crypto_backend}}) - Assert that subject_alt_name is there (strict) + openssl_certificate: + path: '{{ output_dir }}/cert_sans.pem' + provider: assertonly + subject_alt_name: + - "DNS:ansible.com" + - "IP:127.0.0.1" + - "IP:::1" + subject_alt_name_strict: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: extension_san_strict + +- name: (Assertonly, {{select_crypto_backend}}) - Assert that key_usage is there (should fail) + openssl_certificate: + path: '{{ output_dir }}/cert_noext.pem' + provider: assertonly + key_usage: + - digitalSignature + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: extension_missing_ku + +- name: (Assertonly, {{select_crypto_backend}}) - Assert that extended_key_usage is there (should fail) + openssl_certificate: + path: '{{ output_dir }}/cert_noext.pem' + provider: assertonly + extended_key_usage: + - biometricInfo + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: extension_missing_eku + +- assert: + that: + - extension_missing_san is failed + - "'Found no subjectAltName extension' in extension_missing_san.msg" + - extension_san is succeeded + - extension_san_strict is succeeded + - extension_missing_ku is failed + - "'Found no keyUsage extension' in extension_missing_ku.msg" + - extension_missing_eku is failed + - "'Found no extendedKeyUsage extension' in extension_missing_eku.msg" + +- name: (Assertonly, {{select_crypto_backend}}) - Check private key passphrase fail 1 + openssl_certificate: + path: '{{ output_dir }}/cert_noext.pem' + privatekey_path: '{{ output_dir }}/privatekey.pem' + privatekey_passphrase: hunter2 + provider: assertonly + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_1 + +- name: (Assertonly, {{select_crypto_backend}}) - Check private key passphrase fail 2 + openssl_certificate: + path: '{{ output_dir }}/cert_noext.pem' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + provider: assertonly + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_2 + +- name: (Assertonly, {{select_crypto_backend}}) - Check private key passphrase fail 3 + openssl_certificate: + path: '{{ output_dir }}/cert_noext.pem' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + provider: assertonly + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_3 + +- name: (Assertonly, {{select_crypto_backend}}) - + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" diff --git a/tests/integration/targets/openssl_certificate/tasks/expired.yml b/tests/integration/targets/openssl_certificate/tasks/expired.yml new file mode 100644 index 00000000..248ce4a2 --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tasks/expired.yml @@ -0,0 +1,48 @@ +--- +- name: (Expired, {{select_crypto_backend}}) Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/has_expired_privatekey.pem' + +- name: (Expired, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ output_dir }}/has_expired_csr.csr' + privatekey_path: '{{ output_dir }}/has_expired_privatekey.pem' + subject: + commonName: www.example.com + +- name: (Expired, {{select_crypto_backend}}) Generate expired selfsigned certificate + openssl_certificate: + path: '{{ output_dir }}/has_expired_cert.pem' + csr_path: '{{ output_dir }}/has_expired_csr.csr' + privatekey_path: '{{ output_dir }}/has_expired_privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_not_after: "-1s" + selfsigned_not_before: "-100s" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend == 'pyopenssl' # cryptography won't allow creating expired certificates + +- name: (Expired, {{select_crypto_backend}}) Generate expired selfsigned certificate + command: "openssl x509 -req -days -1 -in {{ output_dir }}/has_expired_csr.csr -signkey {{ output_dir }}/has_expired_privatekey.pem -out {{ output_dir }}/has_expired_cert.pem" + when: select_crypto_backend == 'cryptography' # So we create it with 'command' + +- name: "(Expired) Check task fails because cert is expired (has_expired: false)" + openssl_certificate: + provider: assertonly + path: "{{ output_dir }}/has_expired_cert.pem" + has_expired: false + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: expired_cert_check + +- name: (Expired, {{select_crypto_backend}}) Ensure previous task failed + assert: + that: expired_cert_check is failed + +- name: "(Expired) Check expired cert check is ignored (has_expired: true)" + openssl_certificate: + provider: assertonly + path: "{{ output_dir }}/has_expired_cert.pem" + has_expired: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: expired_cert_skip diff --git a/tests/integration/targets/openssl_certificate/tasks/impl.yml b/tests/integration/targets/openssl_certificate/tasks/impl.yml new file mode 100644 index 00000000..f215591f --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tasks/impl.yml @@ -0,0 +1,8 @@ +--- +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" +- import_tasks: assertonly.yml +- import_tasks: expired.yml +- import_tasks: selfsigned.yml +- import_tasks: ownca.yml +- import_tasks: removal.yml diff --git a/tests/integration/targets/openssl_certificate/tasks/main.yml b/tests/integration/targets/openssl_certificate/tasks/main.yml new file mode 100644 index 00000000..f8c1c5fe --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + when: pyopenssl_version.stdout is version('0.15', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.6', '>=') diff --git a/tests/integration/targets/openssl_certificate/tasks/ownca.yml b/tests/integration/targets/openssl_certificate/tasks/ownca.yml new file mode 100644 index 00000000..0d1381ff --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tasks/ownca.yml @@ -0,0 +1,578 @@ +--- +- name: (OwnCA, {{select_crypto_backend}}) Generate CA privatekey + openssl_privatekey: + path: '{{ output_dir }}/ca_privatekey.pem' + +- name: (OwnCA, {{select_crypto_backend}}) Generate CA privatekey with passphrase + openssl_privatekey: + path: '{{ output_dir }}/ca_privatekey_pw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: (OwnCA, {{select_crypto_backend}}) Generate CA CSR + openssl_csr: + path: '{{ output_dir }}/ca_csr.csr' + privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + subject: + commonName: Example CA + useCommonNameForSAN: no + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: yes + +- name: (OwnCA, {{select_crypto_backend}}) Generate CA CSR (privatekey passphrase) + openssl_csr: + path: '{{ output_dir }}/ca_csr_pw.csr' + privatekey_path: '{{ output_dir }}/ca_privatekey_pw.pem' + privatekey_passphrase: hunter2 + subject: + commonName: Example CA + useCommonNameForSAN: no + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: yes + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate + openssl_certificate: + path: '{{ output_dir }}/ca_cert.pem' + csr_path: '{{ output_dir }}/ca_csr.csr' + privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate (privatekey passphrase) + openssl_certificate: + path: '{{ output_dir }}/ca_cert_pw.pem' + csr_path: '{{ output_dir }}/ca_csr_pw.csr' + privatekey_path: '{{ output_dir }}/ca_privatekey_pw.pem' + privatekey_passphrase: hunter2 + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate + openssl_certificate: + path: '{{ output_dir }}/ownca_cert.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: ownca_certificate + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: ownca_certificate_idempotence + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (check mode) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + +- name: (OwnCA, {{select_crypto_backend}}) Check ownca certificate + openssl_certificate: + path: '{{ output_dir }}/ownca_cert.pem' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: assertonly + has_expired: False + version: 3 + signature_algorithms: + - sha256WithRSAEncryption + - sha256WithECDSAEncryption + subject: + commonName: www.example.com + issuer: + commonName: Example CA + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca v2 certificate + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_v2.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_version: 2 + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_v2_certificate + ignore_errors: true + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate2 + openssl_certificate: + path: '{{ output_dir }}/ownca_cert2.pem' + csr_path: '{{ output_dir }}/csr2.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Check ownca certificate2 + openssl_certificate: + path: '{{ output_dir }}/ownca_cert2.pem' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + provider: assertonly + has_expired: False + version: 3 + signature_algorithms: + - sha256WithRSAEncryption + - sha256WithECDSAEncryption + subject: + commonName: www.example.com + C: US + ST: California + L: Los Angeles + O: ACME Inc. + OU: + - Roadrunner pest control + - Pyrotechnics + keyUsage: + - digitalSignature + extendedKeyUsage: + - ipsecUser + - biometricInfo + issuer: + commonName: Example CA + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with notBefore and notAfter + openssl_certificate: + provider: ownca + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + path: "{{ output_dir }}/ownca_cert3.pem" + csr_path: "{{ output_dir }}/csr.csr" + privatekey_path: "{{ output_dir }}/privatekey3.pem" + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with relative notBefore and notAfter + openssl_certificate: + provider: ownca + ownca_not_before: +1s + ownca_not_after: +52w + path: "{{ output_dir }}/ownca_cert4.pem" + csr_path: "{{ output_dir }}/csr.csr" + privatekey_path: "{{ output_dir }}/privatekey3.pem" + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca ECC certificate + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ecc.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_certificate_ecc + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned certificate (privatekey passphrase) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ecc_2.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert_pw.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey_pw.pem' + ownca_privatekey_passphrase: hunter2 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_certificate_passphrase + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 1) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_pw1.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + ownca_privatekey_passphrase: hunter2 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_1 + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 2) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_pw2.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekeypw.pem' + ownca_privatekey_passphrase: wrong_password + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_2 + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 3) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_pw3.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekeypw.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_3 + +- name: Create broken certificate + copy: + dest: "{{ output_dir }}/ownca_broken.pem" + content: "broken" +- name: Regenerate broken cert + openssl_certificate: + path: '{{ output_dir }}/ownca_broken.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + register: ownca_broken + +- name: (OwnCA, {{select_crypto_backend}}) Backup test + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_backup.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_1 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (idempotent) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_backup.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_2 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (change) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_backup.pem' + csr_path: '{{ output_dir }}/csr.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_3 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (remove) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_backup.pem' + state: absent + provider: ownca + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_4 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (remove, idempotent) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_backup.pem' + state: absent + provider: ownca + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_5 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_1 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_2 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_3 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_4 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (re-enable) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_5 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_1 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_2 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: no + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_3 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: no + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_4 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (re-add) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_5 + +- name: (OwnCA, {{select_crypto_backend}}) Ed25519 and Ed448 tests (for cryptography >= 2.6) + block: + - name: (OwnCA, {{select_crypto_backend}}) Generate privatekeys + openssl_privatekey: + path: '{{ output_dir }}/privatekey_{{ item }}.pem' + type: '{{ item }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_privatekey + ignore_errors: yes + + - name: (OwnCA, {{select_crypto_backend}}) Generate CSR etc. if private key generation succeeded + when: ownca_certificate_ed25519_ed448_privatekey is not failed + block: + + - name: (OwnCA, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + ignore_errors: yes + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_{{ item }}.pem' + csr_path: '{{ output_dir }}/csr_{{ item }}.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448 + ignore_errors: yes + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_{{ item }}.pem' + csr_path: '{{ output_dir }}/csr_{{ item }}.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_idempotence + ignore_errors: yes + + - name: (OwnCA, {{select_crypto_backend}}) Generate CA privatekey + openssl_privatekey: + path: '{{ output_dir }}/ca_privatekey_{{ item }}.pem' + type: '{{ item }}' + cipher: auto + passphrase: Test123 + ignore_errors: yes + loop: + - Ed25519 + - Ed448 + + - name: (OwnCA, {{select_crypto_backend}}) Generate CA CSR + openssl_csr: + path: '{{ output_dir }}/ca_csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/ca_privatekey_{{ item }}.pem' + privatekey_passphrase: Test123 + subject: + commonName: Example CA + useCommonNameForSAN: no + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: yes + key_usage: + - cRLSign + - keyCertSign + loop: + - Ed25519 + - Ed448 + ignore_errors: yes + + - name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate + openssl_certificate: + path: '{{ output_dir }}/ca_cert_{{ item }}.pem' + csr_path: '{{ output_dir }}/ca_csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/ca_privatekey_{{ item }}.pem' + privatekey_passphrase: Test123 + provider: selfsigned + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + ignore_errors: yes + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_{{ item }}_2.pem' + csr_path: '{{ output_dir }}/csr.csr' + ownca_path: '{{ output_dir }}/ca_cert_{{ item }}.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey_{{ item }}.pem' + ownca_privatekey_passphrase: Test123 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_2 + ignore_errors: yes + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_{{ item }}_2.pem' + csr_path: '{{ output_dir }}/csr.csr' + ownca_path: '{{ output_dir }}/ca_cert_{{ item }}.pem' + ownca_privatekey_path: '{{ output_dir }}/ca_privatekey_{{ item }}.pem' + ownca_privatekey_passphrase: Test123 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_2_idempotence + ignore_errors: yes + + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') + +- import_tasks: ../tests/validate_ownca.yml diff --git a/tests/integration/targets/openssl_certificate/tasks/removal.yml b/tests/integration/targets/openssl_certificate/tasks/removal.yml new file mode 100644 index 00000000..e45e952d --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tasks/removal.yml @@ -0,0 +1,52 @@ +--- +- name: (Removal, {{select_crypto_backend}}) Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/removal_privatekey.pem' + +- name: (Removal, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ output_dir }}/removal_csr.csr' + privatekey_path: '{{ output_dir }}/removal_privatekey.pem' + +- name: (Removal, {{select_crypto_backend}}) Generate selfsigned certificate + openssl_certificate: + path: '{{ output_dir }}/removal_cert.pem' + csr_path: '{{ output_dir }}/removal_csr.csr' + privatekey_path: '{{ output_dir }}/removal_privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "(Removal, {{select_crypto_backend}}) Check that file is not gone" + stat: + path: "{{ output_dir }}/removal_cert.pem" + register: removal_1_prestat + +- name: "(Removal, {{select_crypto_backend}}) Remove certificate" + openssl_certificate: + path: "{{ output_dir }}/removal_cert.pem" + state: absent + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: removal_1 + +- name: "(Removal, {{select_crypto_backend}}) Check that file is gone" + stat: + path: "{{ output_dir }}/removal_cert.pem" + register: removal_1_poststat + +- name: "(Removal, {{select_crypto_backend}}) Remove certificate (idempotent)" + openssl_certificate: + path: "{{ output_dir }}/removal_cert.pem" + state: absent + select_crypto_backend: '{{ select_crypto_backend }}' + register: removal_2 + +- name: (Removal, {{select_crypto_backend}}) Ensure removal worked + assert: + that: + - removal_1_prestat.stat.exists + - removal_1 is changed + - not removal_1_poststat.stat.exists + - removal_2 is not changed + - removal_1.certificate is none diff --git a/tests/integration/targets/openssl_certificate/tasks/selfsigned.yml b/tests/integration/targets/openssl_certificate/tasks/selfsigned.yml new file mode 100644 index 00000000..5455d40b --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tasks/selfsigned.yml @@ -0,0 +1,431 @@ +--- +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey.pem' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr_minimal_change.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.org + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate + openssl_certificate: + path: '{{ output_dir }}/cert.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: selfsigned_certificate + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - idempotency + openssl_certificate: + path: '{{ output_dir }}/cert.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: selfsigned_certificate_idempotence + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (check mode) + openssl_certificate: + path: '{{ output_dir }}/cert.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (check mode, other CSR) + openssl_certificate: + path: '{{ output_dir }}/cert.pem' + csr_path: '{{ output_dir }}/csr_minimal_change.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: selfsigned_certificate_csr_minimal_change + +- name: (Selfsigned, {{select_crypto_backend}}) Check selfsigned certificate + openssl_certificate: + path: '{{ output_dir }}/cert.pem' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: assertonly + has_expired: False + version: 3 + signature_algorithms: + - sha256WithRSAEncryption + - sha256WithECDSAEncryption + subject: + commonName: www.example.com + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned v2 certificate + openssl_certificate: + path: '{{ output_dir }}/cert_v2.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_version: 2 + select_crypto_backend: "{{ select_crypto_backend }}" + register: selfsigned_v2_cert + ignore_errors: true + +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey2 + openssl_privatekey: + path: '{{ output_dir }}/privatekey2.pem' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR2 + openssl_csr: + subject: + CN: www.example.com + C: US + ST: California + L: Los Angeles + O: ACME Inc. + OU: + - Roadrunner pest control + - Pyrotechnics + path: '{{ output_dir }}/csr2.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + keyUsage: + - digitalSignature + extendedKeyUsage: + - ipsecUser + - biometricInfo + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate2 + openssl_certificate: + path: '{{ output_dir }}/cert2.pem' + csr_path: '{{ output_dir }}/csr2.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Check selfsigned certificate2 + openssl_certificate: + path: '{{ output_dir }}/cert2.pem' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + provider: assertonly + has_expired: False + version: 3 + signature_algorithms: + - sha256WithRSAEncryption + - sha256WithECDSAEncryption + subject: + commonName: www.example.com + C: US + ST: California + L: Los Angeles + O: ACME Inc. + OU: + - Roadrunner pest control + - Pyrotechnics + keyUsage: + - digitalSignature + extendedKeyUsage: + - ipsecUser + - biometricInfo + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Create private key 3 + openssl_privatekey: + path: "{{ output_dir }}/privatekey3.pem" + +- name: (Selfsigned, {{select_crypto_backend}}) Create CSR 3 + openssl_csr: + subject: + CN: www.example.com + privatekey_path: "{{ output_dir }}/privatekey3.pem" + path: "{{ output_dir }}/csr3.pem" + +- name: (Selfsigned, {{select_crypto_backend}}) Create certificate3 with notBefore and notAfter + openssl_certificate: + provider: selfsigned + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + path: "{{ output_dir }}/cert3.pem" + csr_path: "{{ output_dir }}/csr3.pem" + privatekey_path: "{{ output_dir }}/privatekey3.pem" + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey_ecc.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + subject: + commonName: www.example.com + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate + openssl_certificate: + path: '{{ output_dir }}/cert_ecc.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_certificate_ecc + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR (privatekey passphrase) + openssl_csr: + path: '{{ output_dir }}/csr_pass.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + subject: + commonName: www.example.com + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (privatekey passphrase) + openssl_certificate: + path: '{{ output_dir }}/cert_pass.pem' + csr_path: '{{ output_dir }}/csr_pass.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_certificate_passphrase + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 1) + openssl_certificate: + path: '{{ output_dir }}/cert_pw1.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + privatekey_passphrase: hunter2 + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_1 + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 2) + openssl_certificate: + path: '{{ output_dir }}/cert_pw2.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_2 + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 3) + openssl_certificate: + path: '{{ output_dir }}/cert_pw3.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_3 + +- name: Create broken certificate + copy: + dest: "{{ output_dir }}/cert_broken.pem" + content: "broken" +- name: Regenerate broken cert + openssl_certificate: + path: '{{ output_dir }}/cert_broken.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + register: selfsigned_broken + +- name: (Selfsigned, {{select_crypto_backend}}) Backup test + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_backup.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_1 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (idempotent) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_backup.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_2 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (change) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_backup.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_3 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (remove) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_backup.pem' + state: absent + provider: selfsigned + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_4 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (remove, idempotent) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_backup.pem' + state: absent + provider: selfsigned + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_5 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_1 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (idempotency) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_2 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_3 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove idempotency) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_4 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (re-enable) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_5 + +- name: (Selfsigned, {{select_crypto_backend}}) Ed25519 and Ed448 tests (for cryptography >= 2.6) + block: + - name: (Selfsigned, {{select_crypto_backend}}) Generate privatekeys + openssl_privatekey: + path: '{{ output_dir }}/privatekey_{{ item }}.pem' + type: '{{ item }}' + loop: + - Ed25519 + - Ed448 + register: selfsigned_certificate_ed25519_ed448_privatekey + ignore_errors: yes + + - name: (Selfsigned, {{select_crypto_backend}}) Generate CSR etc. if private key generation succeeded + when: selfsigned_certificate_ed25519_ed448_privatekey is not failed + block: + + - name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + ignore_errors: yes + + - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate + openssl_certificate: + path: '{{ output_dir }}/cert_{{ item }}.pem' + csr_path: '{{ output_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: selfsigned_certificate_ed25519_ed448 + ignore_errors: yes + + - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - idempotency + openssl_certificate: + path: '{{ output_dir }}/cert_{{ item }}.pem' + csr_path: '{{ output_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: selfsigned_certificate_ed25519_ed448_idempotence + ignore_errors: yes + + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') + +- import_tasks: ../tests/validate_selfsigned.yml diff --git a/tests/integration/targets/openssl_certificate/tests/validate_ownca.yml b/tests/integration/targets/openssl_certificate/tests/validate_ownca.yml new file mode 100644 index 00000000..19ab6198 --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tests/validate_ownca.yml @@ -0,0 +1,178 @@ +--- +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - verify CA) + shell: 'openssl verify -CAfile {{ output_dir }}/ca_cert.pem {{ output_dir }}/ownca_cert.pem | sed "s/.*: \(.*\)/\1/g"' + register: ownca_verify_ca + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - ownca certificate modulus) + shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/ownca_cert.pem' + register: ownca_cert_modulus + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - ownca issuer value) + shell: 'openssl x509 -noout -in {{ output_dir}}/ownca_cert.pem -text | grep "Issuer" | sed "s/.*: \(.*\)/\1/g"' + register: ownca_cert_issuer + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - ownca certficate version == default == 3) + shell: 'openssl x509 -noout -in {{ output_dir}}/ownca_cert.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert_version + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (assert) + assert: + that: + - ownca_verify_ca.stdout == 'OK' + - ownca_cert_modulus.stdout == privatekey_modulus.stdout + - ownca_cert_version.stdout == '3' + # openssl 1.1.x adds a space between the output + - ownca_cert_issuer.stdout in ['CN=Example CA', 'CN = Example CA'] + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate idempotence + assert: + that: + - ownca_certificate.serial_number == ownca_certificate_idempotence.serial_number + - ownca_certificate.notBefore == ownca_certificate_idempotence.notBefore + - ownca_certificate.notAfter == ownca_certificate_idempotence.notAfter + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca data return + assert: + that: + - ownca_certificate.certificate == lookup('file', output_dir ~ '/ownca_cert.pem', rstrip=False) + - ownca_certificate.certificate == ownca_certificate_idempotence.certificate + +- block: + - name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate v2 (test - ownca certificate version == 2) + shell: 'openssl x509 -noout -in {{ output_dir}}/ownca_cert_v2.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert_v2_version + + - name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate version 2 (assert) + assert: + that: + - ownca_cert_v2_version.stdout == '2' + when: "select_crypto_backend != 'cryptography'" + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate v2 (test - ownca certificate version == 2) + assert: + that: + - ownca_v2_certificate is failed + - "'The cryptography backend does not support v2 certificates' in ownca_v2_certificate.msg" + when: "select_crypto_backend == 'cryptography'" + + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate2 (test - ownca certificate modulus) + shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/ownca_cert2.pem' + register: ownca_cert2_modulus + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate2 (assert) + assert: + that: + - ownca_cert2_modulus.stdout == privatekey2_modulus.stdout + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate owncal certificate3 (test - notBefore) + shell: 'openssl x509 -noout -in {{ output_dir }}/ownca_cert3.pem -text | grep "Not Before" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert3_notBefore + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate3 (test - notAfter) + shell: 'openssl x509 -noout -in {{ output_dir }}/ownca_cert3.pem -text | grep "Not After" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert3_notAfter + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate3 (assert - notBefore) + assert: + that: + - ownca_cert3_notBefore.stdout == 'Oct 23 13:37:42 2018' + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate3 (assert - notAfter) + assert: + that: + - ownca_cert3_notAfter.stdout == 'Oct 23 13:37:42 2019' + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca ECC certificate (test - ownca certificate pubkey) + shell: 'openssl x509 -noout -pubkey -in {{ output_dir }}/ownca_cert_ecc.pem' + register: ownca_cert_ecc_pubkey + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca ECC certificate (test - ownca issuer value) + shell: 'openssl x509 -noout -in {{ output_dir}}/ownca_cert_ecc.pem -text | grep "Issuer" | sed "s/.*: \(.*\)/\1/g"' + register: ownca_cert_ecc_issuer + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca ECC certificate (assert) + assert: + that: + - ownca_cert_ecc_pubkey.stdout == privatekey_ecc_pubkey.stdout + # openssl 1.1.x adds a space between the output + - ownca_cert_ecc_issuer.stdout in ['CN=Example CA', 'CN = Example CA'] + +- name: (OwnCA validation, {{select_crypto_backend}}) + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: (OwnCA validation, {{select_crypto_backend}})Verify that broken certificate will be regenerated + assert: + that: + - ownca_broken is changed + +- name: (OwnCA validation, {{select_crypto_backend}}) Check backup + assert: + that: + - ownca_backup_1 is changed + - ownca_backup_1.backup_file is undefined + - ownca_backup_2 is not changed + - ownca_backup_2.backup_file is undefined + - ownca_backup_3 is changed + - ownca_backup_3.backup_file is string + - ownca_backup_4 is changed + - ownca_backup_4.backup_file is string + - ownca_backup_5 is not changed + - ownca_backup_5.backup_file is undefined + +- name: (OwnCA validation, {{select_crypto_backend}}) Check create subject key identifier + assert: + that: + - ownca_subject_key_identifier_1 is changed + - ownca_subject_key_identifier_2 is not changed + - ownca_subject_key_identifier_3 is changed + - ownca_subject_key_identifier_4 is not changed + - ownca_subject_key_identifier_5 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: (OwnCA validation, {{select_crypto_backend}}) Check create authority key identifier + assert: + that: + - ownca_authority_key_identifier_1 is changed + - ownca_authority_key_identifier_2 is not changed + - ownca_authority_key_identifier_3 is changed + - ownca_authority_key_identifier_4 is not changed + - ownca_authority_key_identifier_5 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: (OwnCA validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8) + assert: + that: + - ownca_certificate_ed25519_ed448.results[0] is failed + - ownca_certificate_ed25519_ed448.results[1] is failed + - ownca_certificate_ed25519_ed448_idempotence.results[0] is failed + - ownca_certificate_ed25519_ed448_idempotence.results[1] is failed + - ownca_certificate_ed25519_ed448_2.results[0] is failed + - ownca_certificate_ed25519_ed448_2.results[1] is failed + - ownca_certificate_ed25519_ed448_2_idempotence.results[0] is failed + - ownca_certificate_ed25519_ed448_2_idempotence.results[1] is failed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') and cryptography_version.stdout is version('2.8', '<') and ownca_certificate_ed25519_ed448_privatekey is not failed + +- name: (OwnCA validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.8) + assert: + that: + - ownca_certificate_ed25519_ed448 is succeeded + - ownca_certificate_ed25519_ed448.results[0] is changed + - ownca_certificate_ed25519_ed448.results[1] is changed + - ownca_certificate_ed25519_ed448_idempotence is succeeded + - ownca_certificate_ed25519_ed448_idempotence.results[0] is not changed + - ownca_certificate_ed25519_ed448_idempotence.results[1] is not changed + - ownca_certificate_ed25519_ed448_2 is succeeded + - ownca_certificate_ed25519_ed448_2.results[0] is changed + - ownca_certificate_ed25519_ed448_2.results[1] is changed + - ownca_certificate_ed25519_ed448_2_idempotence is succeeded + - ownca_certificate_ed25519_ed448_2_idempotence.results[0] is not changed + - ownca_certificate_ed25519_ed448_2_idempotence.results[1] is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.8', '>=') and ownca_certificate_ed25519_ed448_privatekey is not failed diff --git a/tests/integration/targets/openssl_certificate/tests/validate_selfsigned.yml b/tests/integration/targets/openssl_certificate/tests/validate_selfsigned.yml new file mode 100644 index 00000000..85df20c5 --- /dev/null +++ b/tests/integration/targets/openssl_certificate/tests/validate_selfsigned.yml @@ -0,0 +1,164 @@ +--- +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - privatekey modulus) + shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - certificate modulus) + shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/cert.pem' + register: cert_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - issuer value) + shell: 'openssl x509 -noout -in {{ output_dir}}/cert.pem -text | grep "Issuer" | sed "s/.*: \(.*\)/\1/g; s/ //g;"' + register: cert_issuer + + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - certficate version == default == 3) + shell: 'openssl x509 -noout -in {{ output_dir}}/cert.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert_version + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (assert) + assert: + that: + - cert_modulus.stdout == privatekey_modulus.stdout + - cert_version.stdout == '3' + - cert_issuer.stdout == 'CN=www.example.com' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate idempotence + assert: + that: + - selfsigned_certificate.serial_number == selfsigned_certificate_idempotence.serial_number + - selfsigned_certificate.notBefore == selfsigned_certificate_idempotence.notBefore + - selfsigned_certificate.notAfter == selfsigned_certificate_idempotence.notAfter + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate data retrieval + assert: + that: + - selfsigned_certificate.certificate == lookup('file', output_dir ~ '/cert.pem', rstrip=False) + - selfsigned_certificate.certificate == selfsigned_certificate_idempotence.certificate + +- name: Make sure that changes in CSR are detected even if private key is specified + assert: + that: + - selfsigned_certificate_csr_minimal_change is changed + +- block: + - name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate v2 (test - certificate version == 2) + shell: 'openssl x509 -noout -in {{ output_dir}}/cert_v2.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert_v2_version + + - name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate version 2 (assert) + assert: + that: + - cert_v2_version.stdout == '2' + when: select_crypto_backend != 'cryptography' + +- block: + - name: (Selfsigned validateion, {{ select_crypto_backend }} Validate certificate v2 is failed + assert: + that: + - selfsigned_v2_cert is failed + - "'The cryptography backend does not support v2 certificates' in selfsigned_v2_cert.msg" + when: select_crypto_backend == 'cryptography' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate2 (test - privatekey modulus) + shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey2.pem' + register: privatekey2_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate2 (test - certificate modulus) + shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/cert2.pem' + register: cert2_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate2 (assert) + assert: + that: + - cert2_modulus.stdout == privatekey2_modulus.stdout + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (test - notBefore) + shell: 'openssl x509 -noout -in {{ output_dir }}/cert3.pem -text | grep "Not Before" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert3_notBefore + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (test - notAfter) + shell: 'openssl x509 -noout -in {{ output_dir }}/cert3.pem -text | grep "Not After" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert3_notAfter + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (assert - notBefore) + assert: + that: + - cert3_notBefore.stdout == 'Oct 23 13:37:42 2018' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (assert - notAfter) + assert: + that: + - cert3_notAfter.stdout == 'Oct 23 13:37:42 2019' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate ECC certificate (test - privatekey's pubkey) + shell: 'openssl ec -pubout -in {{ output_dir }}/privatekey_ecc.pem' + register: privatekey_ecc_pubkey + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate ECC certificate (test - certificate pubkey) + shell: 'openssl x509 -noout -pubkey -in {{ output_dir }}/cert_ecc.pem' + register: cert_ecc_pubkey + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate ECC certificate (assert) + assert: + that: + - cert_ecc_pubkey.stdout == privatekey_ecc_pubkey.stdout + +- name: (Selfsigned validation, {{select_crypto_backend}}) + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: (Selfsigned validation, {{select_crypto_backend}}) Verify that broken certificate will be regenerated + assert: + that: + - selfsigned_broken is changed + +- name: (Selfsigned validation, {{select_crypto_backend}}) Check backup + assert: + that: + - selfsigned_backup_1 is changed + - selfsigned_backup_1.backup_file is undefined + - selfsigned_backup_2 is not changed + - selfsigned_backup_2.backup_file is undefined + - selfsigned_backup_3 is changed + - selfsigned_backup_3.backup_file is string + - selfsigned_backup_4 is changed + - selfsigned_backup_4.backup_file is string + - selfsigned_backup_5 is not changed + - selfsigned_backup_5.backup_file is undefined + +- name: (Selfsigned validation, {{select_crypto_backend}}) Check create subject key identifier + assert: + that: + - selfsigned_subject_key_identifier_1 is changed + - selfsigned_subject_key_identifier_2 is not changed + - selfsigned_subject_key_identifier_3 is changed + - selfsigned_subject_key_identifier_4 is not changed + - selfsigned_subject_key_identifier_5 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8) + assert: + that: + - selfsigned_certificate_ed25519_ed448.results[0] is failed + - selfsigned_certificate_ed25519_ed448.results[1] is failed + - selfsigned_certificate_ed25519_ed448_idempotence.results[0] is failed + - selfsigned_certificate_ed25519_ed448_idempotence.results[1] is failed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') and cryptography_version.stdout is version('2.8', '<') and selfsigned_certificate_ed25519_ed448_privatekey is not failed + +- name: (Selfsigned validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.8) + assert: + that: + - selfsigned_certificate_ed25519_ed448 is succeeded + - selfsigned_certificate_ed25519_ed448.results[0] is changed + - selfsigned_certificate_ed25519_ed448.results[1] is changed + - selfsigned_certificate_ed25519_ed448_idempotence is succeeded + - selfsigned_certificate_ed25519_ed448_idempotence.results[0] is not changed + - selfsigned_certificate_ed25519_ed448_idempotence.results[1] is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.8', '>=') and selfsigned_certificate_ed25519_ed448_privatekey is not failed diff --git a/tests/integration/targets/openssl_certificate_info/aliases b/tests/integration/targets/openssl_certificate_info/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssl_certificate_info/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_certificate_info/files/cert1.pem b/tests/integration/targets/openssl_certificate_info/files/cert1.pem new file mode 100644 index 00000000..834eedc4 --- /dev/null +++ b/tests/integration/targets/openssl_certificate_info/files/cert1.pem @@ -0,0 +1,45 @@ +-----BEGIN CERTIFICATE----- +MIIH5jCCBs6gAwIBAgISA2gSCm/BtvCR2e2bIap5YbXaMA0GCSqGSIb3DQEBCwUA +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODA3MjcxNzMxMjdaFw0x +ODEwMjUxNzMxMjdaMB4xHDAaBgNVBAMTE3d3dy5sZXRzZW5jcnlwdC5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpL8ZjVL0MUkUAIbYO9+ZCni+c +ghGd9WhM2Ztaay6Wyh6lNoCdltdqTwUhE4O+d7UFModjM3G/KMyfuujr06c5iGKL +3saPmIzLaRPIEOUlB2rKgasKhe8mDRyRLzQSXXgnsaKcTBBuhIHvtP51ZMr05nJJ +sX/5FGjj96w+KJel6E/Ux1a1ZDOFkAYNSIrJJhA5jjIvUPr+Ri6Oc6UlhF9oueKI +uWBILxQpC778tBWdHoZeBCNTHA1VvtwC53OeuHvdZm1jB/e30Mgf5DtVizYpFXVD +mztkrd6z/3B6ZwPyfCE4KgzSf70/byOz971OJxNKTUVWedKHHDlrMxfsPclbAgMB +AAGjggTwMIIE7DAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFG1w4j/KDrYSFu7m9DPE +xRR0E5gzMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUF +BwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNy +eXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNy +eXB0Lm9yZy8wggHxBgNVHREEggHoMIIB5IIbY2VydC5pbnQteDEubGV0c2VuY3J5 +cHQub3JnghtjZXJ0LmludC14Mi5sZXRzZW5jcnlwdC5vcmeCG2NlcnQuaW50LXgz +LmxldHNlbmNyeXB0Lm9yZ4IbY2VydC5pbnQteDQubGV0c2VuY3J5cHQub3Jnghxj +ZXJ0LnJvb3QteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0YWdpbmcteDEubGV0 +c2VuY3J5cHQub3Jngh9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JngiBj +ZXJ0LnN0Zy1yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ISY3AubGV0c2VuY3J5cHQu +b3JnghpjcC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ITY3BzLmxldHNlbmNyeXB0 +Lm9yZ4IbY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3Jnghtjcmwucm9vdC14MS5s +ZXRzZW5jcnlwdC5vcmeCD2xldHNlbmNyeXB0Lm9yZ4IWb3JpZ2luLmxldHNlbmNy +eXB0Lm9yZ4IXb3JpZ2luMi5sZXRzZW5jcnlwdC5vcmeCFnN0YXR1cy5sZXRzZW5j +cnlwdC5vcmeCE3d3dy5sZXRzZW5jcnlwdC5vcmcwgf4GA1UdIASB9jCB8zAIBgZn +gQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3Bz +LmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmlj +YXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBh +bmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGlj +eSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzCC +AQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AMEWSuCnctLUOS3ICsEHcNTwxJvemRpI +QMH6B1Fk9jNgAAABZN0ChToAAAQDAEcwRQIgblal8oXnfoopr1+dWVhvBx+sqHT0 +eLYxJHBTaRp3j1QCIQDhFQqMk6DDXUgcU12K36zLVFwJTdAJI4RBisnX+g+W0AB2 +ACk8UZZUyDlluqpQ/FgH1Ldvv1h6KXLcpMMM9OVFR/R4AAABZN0Chz4AAAQDAEcw +RQIhAImOjvkritUNKJZB7dcUtjoyIbfNwdCspvRiEzXuvVQoAiAZryoyg3TcMun5 +Gb2dEn1cttMnPW9u670/JdRjvjU/wTANBgkqhkiG9w0BAQsFAAOCAQEAGepCmckP +Tn9Sz268FEwkdD+6wWaPfeYlh+9nacFh90nQ35EYQMOK8a+X7ixHGbRz19On3Wt4 +1fcbPa9SefocTjAintMwwreCxpRTmwGACYojd7vRWEmA6q7+/HO2BfZahWzclOjw +mSDBycDEm8R0ZK52vYjzVno8x0mrsmSO0403S/6syYB/guH6P17kIBw+Tgx6/i/c +I1C6MoFkuaAKUUcZmgGGBgE+L/7cWtWjbkVXyA3ZQQy9G7rcBT+N/RrDfBh4iZDq +jAN5UIIYL8upBhjiMYVuoJrH2nklzEwr5SWKcccJX5eWkGLUwlcY9LGAA8+17l2I +l1Ou20Dm9TxnNw== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/openssl_certificate_info/meta/main.yml b/tests/integration/targets/openssl_certificate_info/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_certificate_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_certificate_info/tasks/impl.yml b/tests/integration/targets/openssl_certificate_info/tasks/impl.yml new file mode 100644 index 00000000..2ae89ed7 --- /dev/null +++ b/tests/integration/targets/openssl_certificate_info/tasks/impl.yml @@ -0,0 +1,120 @@ +--- +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" + +- name: ({{select_crypto_backend}}) Get certificate info + openssl_certificate_info: + path: '{{ output_dir }}/cert_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check whether issuer and subject behave as expected + assert: + that: + - result.issuer.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.issuer_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.issuer_ordered" + - result.subject.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" + +- name: Check SubjectKeyIdentifier and AuthorityKeyIdentifier + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" + +- name: ({{select_crypto_backend}}) Get certificate info directly + openssl_certificate_info: + content: '{{ lookup("file", output_dir ~ "/cert_1.pem") }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_direct + +- name: ({{select_crypto_backend}}) Compare output of direct and loaded info + assert: + that: + - result == result_direct + +- name: ({{select_crypto_backend}}) Get certificate info + openssl_certificate_info: + path: '{{ output_dir }}/cert_2.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + valid_at: + today: "+0d" + past: "20190101235901Z" + twentydays: "+20d" + register: result +- assert: + that: + - result.valid_at.today + - not result.valid_at.past + - not result.valid_at.twentydays + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" + +- name: ({{select_crypto_backend}}) Get certificate info + openssl_certificate_info: + path: '{{ output_dir }}/cert_3.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" + +- name: ({{select_crypto_backend}}) Get certificate info + openssl_certificate_info: + path: '{{ output_dir }}/cert_4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" + +- name: ({{select_crypto_backend}}) Get certificate info for packaged cert 1 + openssl_certificate_info: + path: '{{ role_path }}/files/cert1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result +- assert: + that: + - "'ocsp_uri' in result" + - "result.ocsp_uri == 'http://ocsp.int-x3.letsencrypt.org'" + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" diff --git a/tests/integration/targets/openssl_certificate_info/tasks/main.yml b/tests/integration/targets/openssl_certificate_info/tasks/main.yml new file mode 100644 index 00000000..8fc2636c --- /dev/null +++ b/tests/integration/targets/openssl_certificate_info/tasks/main.yml @@ -0,0 +1,176 @@ +--- +- name: Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey.pem' + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: Generate CSR 1 + openssl_csr: + path: '{{ output_dir }}/csr_1.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: + - Crypto Department + - ACME Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: no + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: yes + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: yes + ocsp_must_staple: yes + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 2 + openssl_csr: + path: '{{ output_dir }}/csr_2.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + useCommonNameForSAN: no + basic_constraints: + - "CA:TRUE" + +- name: Generate CSR 3 + openssl_csr: + path: '{{ output_dir }}/csr_3.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + useCommonNameForSAN: no + subject_alt_name: + - "DNS:*.ansible.com" + - "DNS:*.example.org" + - "IP:DEAD:BEEF::1" + basic_constraints: + - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 4 + openssl_csr: + path: '{{ output_dir }}/csr_4.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + useCommonNameForSAN: no + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + +- name: Generate selfsigned certificates + openssl_certificate: + path: '{{ output_dir }}/cert_{{ item }}.pem' + csr_path: '{{ output_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_not_after: "+10d" + selfsigned_not_before: "-3d" + loop: + - 1 + - 2 + - 3 + - 4 + +- name: Prepare result list + set_fact: + info_results: [] + +- name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + when: pyopenssl_version.stdout is version('0.15', '>=') + +- name: Prepare result list + set_fact: + pyopenssl_info_results: "{{ info_results }}" + info_results: [] + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.6', '>=') + +- name: Prepare result list + set_fact: + cryptography_info_results: "{{ info_results }}" + +- block: + - name: Dump pyOpenSSL results + debug: + var: pyopenssl_info_results + - name: Dump cryptography results + debug: + var: cryptography_info_results + - name: Compare results + assert: + that: + - ' (item.0 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict) + == (item.1 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict)' + quiet: yes + loop: "{{ pyopenssl_info_results | zip(cryptography_info_results) | list }}" + when: pyopenssl_version.stdout is version('0.15', '>=') and cryptography_version.stdout is version('1.6', '>=') + vars: + keys_to_ignore: + - deprecations + - subject_key_identifier + - authority_key_identifier + - authority_cert_issuer + - authority_cert_serial_number diff --git a/tests/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py b/tests/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py new file mode 100644 index 00000000..fc2b5f0f --- /dev/null +++ b/tests/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +def compatibility_in_test(a, b): + return a in b + + +class TestModule: + ''' Ansible math jinja2 tests ''' + + def tests(self): + return { + 'in': compatibility_in_test, + } diff --git a/tests/integration/targets/openssl_csr/aliases b/tests/integration/targets/openssl_csr/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssl_csr/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_csr/meta/main.yml b/tests/integration/targets/openssl_csr/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_csr/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_csr/tasks/impl.yml b/tests/integration/targets/openssl_csr/tasks/impl.yml new file mode 100644 index 00000000..77d23ff5 --- /dev/null +++ b/tests/integration/targets/openssl_csr/tasks/impl.yml @@ -0,0 +1,767 @@ +--- +- name: Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey.pem' + +- name: Generate CSR (check mode) + openssl_csr: + path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + check_mode: yes + register: generate_csr_check + +- name: Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: generate_csr + +- name: Generate CSR (idempotent) + openssl_csr: + path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: generate_csr_idempotent + +- name: Generate CSR (idempotent, check mode) + openssl_csr: + path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + check_mode: yes + register: generate_csr_idempotent_check + +- name: Generate CSR without SAN (check mode) + openssl_csr: + path: '{{ output_dir }}/csr-nosan.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: no + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: generate_csr_nosan_check + +- name: Generate CSR without SAN + openssl_csr: + path: '{{ output_dir }}/csr-nosan.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: no + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_nosan + +- name: Generate CSR without SAN (idempotent) + openssl_csr: + path: '{{ output_dir }}/csr-nosan.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: no + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_nosan_check_idempotent + +- name: Generate CSR without SAN (idempotent, check mode) + openssl_csr: + path: '{{ output_dir }}/csr-nosan.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: no + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: generate_csr_nosan_check_idempotent_check + +# keyUsage longname and shortname should be able to be used +# interchangeably. Hence the long name is specified here +# but the short name is used to test idempotency for ipsecuser +# and vice-versa for biometricInfo +- name: Generate CSR with KU and XKU + openssl_csr: + path: '{{ output_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + CN: www.ansible.com + keyUsage: + - digitalSignature + - keyAgreement + extendedKeyUsage: + - qcStatements + - DVCS + - IPSec User + - biometricInfo + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate CSR with KU and XKU (test idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: 'www.ansible.com' + keyUsage: + - Key Agreement + - digitalSignature + extendedKeyUsage: + - ipsecUser + - qcStatements + - DVCS + - Biometric Info + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ku_xku + +- name: Generate CSR with KU and XKU (test XKU change) + openssl_csr: + path: '{{ output_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: 'www.ansible.com' + keyUsage: + - digitalSignature + - keyAgreement + extendedKeyUsage: + - ipsecUser + - qcStatements + - Biometric Info + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ku_xku_change + +- name: Generate CSR with KU and XKU (test KU change) + openssl_csr: + path: '{{ output_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: 'www.ansible.com' + keyUsage: + - digitalSignature + extendedKeyUsage: + - ipsecUser + - qcStatements + - Biometric Info + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ku_xku_change_2 + +- name: Generate CSR with old API + openssl_csr: + path: '{{ output_dir }}/csr_oldapi.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate CSR with invalid SAN + openssl_csr: + path: '{{ output_dir }}/csrinvsan.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject_alt_name: invalid-san.example.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_invalid_san + ignore_errors: yes + +- name: Generate CSR with OCSP Must Staple + openssl_csr: + path: '{{ output_dir }}/csr_ocsp.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject_alt_name: "DNS:www.ansible.com" + ocsp_must_staple: true + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate CSR with OCSP Must Staple (test idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_ocsp.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject_alt_name: "DNS:www.ansible.com" + ocsp_must_staple: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ocsp_idempotency + +- name: Generate ECC privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey2.pem' + type: ECC + curve: secp384r1 + +- name: Generate CSR with ECC privatekey + openssl_csr: + path: '{{ output_dir }}/csr2.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate CSR with text common name + openssl_csr: + path: '{{ output_dir }}/csr3.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + subject: + commonName: This is for Ansible + useCommonNameForSAN: no + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate CSR with country name + openssl_csr: + path: '{{ output_dir }}/csr4.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + country_name: de + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_idempotent_1 + +- name: Generate CSR with country name (idempotent) + openssl_csr: + path: '{{ output_dir }}/csr4.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + country_name: de + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_idempotent_2 + +- name: Generate CSR with country name (idempotent 2) + openssl_csr: + path: '{{ output_dir }}/csr4.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + subject: + C: de + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_idempotent_3 + +- name: Generate CSR with country name (bad country name) + openssl_csr: + path: '{{ output_dir }}/csr4.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + subject: + C: dex + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_fail_4 + ignore_errors: yes + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: Generate CSR with privatekey passphrase + openssl_csr: + path: '{{ output_dir }}/csr_pw.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + select_crypto_backend: '{{ select_crypto_backend }}' + register: passphrase_1 + +- name: Generate CSR (failed passphrase 1) + openssl_csr: + path: '{{ output_dir }}/csr_pw1.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + privatekey_passphrase: hunter2 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_1 + +- name: Generate CSR (failed passphrase 2) + openssl_csr: + path: '{{ output_dir }}/csr_pw2.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_2 + +- name: Generate CSR (failed passphrase 3) + openssl_csr: + path: '{{ output_dir }}/csr_pw3.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_3 + +- name: Create broken CSR + copy: + dest: "{{ output_dir }}/csrbroken.csr" + content: "broken" +- name: Regenerate broken CSR + openssl_csr: + path: '{{ output_dir }}/csrbroken.csr' + privatekey_path: '{{ output_dir }}/privatekey2.pem' + subject: + commonName: This is for Ansible + useCommonNameForSAN: no + select_crypto_backend: '{{ select_crypto_backend }}' + register: output_broken + +- name: Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr_backup.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_1 +- name: Generate CSR (idempotent) + openssl_csr: + path: '{{ output_dir }}/csr_backup.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_2 +- name: Generate CSR (change) + openssl_csr: + path: '{{ output_dir }}/csr_backup.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: ansible.com + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_3 +- name: Generate CSR (remove) + openssl_csr: + path: '{{ output_dir }}/csr_backup.csr' + state: absent + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: csr_backup_4 +- name: Generate CSR (remove, idempotent) + openssl_csr: + path: '{{ output_dir }}/csr_backup.csr' + state: absent + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_5 + +- name: Generate CSR with subject key identifier + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_1 + +- name: Generate CSR with subject key identifier (idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_2 + +- name: Generate CSR with subject key identifier (change) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "44:55:66:77:88" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_3 + +- name: Generate CSR with subject key identifier (auto-create) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + create_subject_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_4 + +- name: Generate CSR with subject key identifier (auto-create idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + create_subject_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_5 + +- name: Generate CSR with subject key identifier (remove) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_6 + +- name: Generate CSR with authority key identifier + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_1 + +- name: Generate CSR with authority key identifier (idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_2 + +- name: Generate CSR with authority key identifier (change) + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "44:55:66:77:88" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_3 + +- name: Generate CSR with authority key identifier (remove) + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_4 + +- name: Generate CSR with authority cert issuer / serial number + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_1 + +- name: Generate CSR with authority cert issuer / serial number (idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_2 + +- name: Generate CSR with authority cert issuer / serial number (change issuer) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "IP:1.2.3.4" + - "DNS:ca.example.org" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_3 + +- name: Generate CSR with authority cert issuer / serial number (change serial number) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "IP:1.2.3.4" + - "DNS:ca.example.org" + authority_cert_serial_number: 54321 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_4 + +- name: Generate CSR with authority cert issuer / serial number (remove) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_5 + +- name: Generate CSR with everything + openssl_csr: + path: '{{ output_dir }}/csr_everything.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: Crypto Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: no + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: yes + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: yes + ocsp_must_staple: yes + subject_key_identifier: '{{ "00:11:22:33" if select_crypto_backend != "pyopenssl" else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_serial_number: '{{ 12345 if select_crypto_backend != "pyopenssl" else omit }}' + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + register: everything_1 + +- name: Generate CSR with everything (idempotent, check mode) + openssl_csr: + path: '{{ output_dir }}/csr_everything.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: Crypto Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: no + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: yes + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: yes + ocsp_must_staple: yes + subject_key_identifier: '{{ "00:11:22:33" if select_crypto_backend != "pyopenssl" else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_serial_number: '{{ 12345 if select_crypto_backend != "pyopenssl" else omit }}' + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + check_mode: yes + register: everything_2 + +- name: Generate CSR with everything (idempotent) + openssl_csr: + path: '{{ output_dir }}/csr_everything.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: Crypto Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: no + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: yes + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: yes + ocsp_must_staple: yes + subject_key_identifier: '{{ "00:11:22:33" if select_crypto_backend != "pyopenssl" else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_serial_number: '{{ 12345 if select_crypto_backend != "pyopenssl" else omit }}' + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + register: everything_3 + +- name: Ed25519 and Ed448 tests (for cryptography >= 2.6) + block: + - name: Generate privatekeys + openssl_privatekey: + path: '{{ output_dir }}/privatekey_{{ item }}.pem' + type: '{{ item }}' + loop: + - Ed25519 + - Ed448 + register: generate_csr_ed25519_ed448_privatekey + ignore_errors: yes + + - name: Generate CSR if private key generation succeeded + when: generate_csr_ed25519_ed448_privatekey is not failed + block: + + - name: Generate CSR + openssl_csr: + path: '{{ output_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: generate_csr_ed25519_ed448 + ignore_errors: yes + + - name: Generate CSR (idempotent) + openssl_csr: + path: '{{ output_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: generate_csr_ed25519_ed448_idempotent + ignore_errors: yes + + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') diff --git a/tests/integration/targets/openssl_csr/tasks/main.yml b/tests/integration/targets/openssl_csr/tasks/main.yml new file mode 100644 index 00000000..e3cc0efb --- /dev/null +++ b/tests/integration/targets/openssl_csr/tasks/main.yml @@ -0,0 +1,44 @@ +--- +- name: Prepare private key for backend autodetection test + openssl_privatekey: + path: '{{ output_dir }}/privatekey_backend_selection.pem' +- name: Run module with backend autodetection + openssl_csr: + path: '{{ output_dir }}/csr_backend_selection.csr' + privatekey_path: '{{ output_dir }}/privatekey_backend_selection.pem' + subject: + commonName: www.ansible.com + +- block: + - name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: pyopenssl + + when: pyopenssl_version.stdout is version('0.15', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.3', '>=') diff --git a/tests/integration/targets/openssl_csr/tests/validate.yml b/tests/integration/targets/openssl_csr/tests/validate.yml new file mode 100644 index 00000000..bdf4b859 --- /dev/null +++ b/tests/integration/targets/openssl_csr/tests/validate.yml @@ -0,0 +1,208 @@ +--- +- name: Validate CSR (test - privatekey modulus) + shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: Validate CSR (test - Common Name) + shell: "openssl req -noout -subject -in {{ output_dir }}/csr.csr -nameopt oneline,-space_eq" + register: csr_cn + +- name: Validate CSR (test - csr modulus) + shell: 'openssl req -noout -modulus -in {{ output_dir }}/csr.csr' + register: csr_modulus + +- name: Validate CSR (assert) + assert: + that: + - csr_cn.stdout.split('=')[-1] == 'www.ansible.com' + - csr_modulus.stdout == privatekey_modulus.stdout + +- name: Validate CSR (check mode, idempotency) + assert: + that: + - generate_csr_check is changed + - generate_csr is changed + - generate_csr_idempotent is not changed + - generate_csr_idempotent_check is not changed + +- name: Validate CSR (data retrieval) + assert: + that: + - generate_csr_check.csr is none + - generate_csr.csr == lookup('file', output_dir ~ '/csr.csr', rstrip=False) + - generate_csr.csr == generate_csr_idempotent.csr + - generate_csr.csr == generate_csr_idempotent_check.csr + +- name: Validate CSR without SAN (check mode, idempotency) + assert: + that: + - generate_csr_nosan_check is changed + - generate_csr_nosan is changed + - generate_csr_nosan_check_idempotent is not changed + - generate_csr_nosan_check_idempotent_check is not changed + +- name: Validate CSR_KU_XKU (assert idempotency, change) + assert: + that: + - csr_ku_xku is not changed + - csr_ku_xku_change is changed + - csr_ku_xku_change_2 is changed + +- name: Validate old_API CSR (test - Common Name) + shell: "openssl req -noout -subject -in {{ output_dir }}/csr_oldapi.csr -nameopt oneline,-space_eq" + register: csr_oldapi_cn + +- name: Validate old_API CSR (test - csr modulus) + shell: 'openssl req -noout -modulus -in {{ output_dir }}/csr_oldapi.csr' + register: csr_oldapi_modulus + +- name: Validate old_API CSR (assert) + assert: + that: + - csr_oldapi_cn.stdout.split('=')[-1] == 'www.ansible.com' + - csr_oldapi_modulus.stdout == privatekey_modulus.stdout + +- name: Validate invalid SAN + assert: + that: + - generate_csr_invalid_san is failed + - "'Subject Alternative Name' in generate_csr_invalid_san.msg" + +- name: Validate OCSP Must Staple CSR (test - everything) + shell: "openssl req -noout -in {{ output_dir }}/csr_ocsp.csr -text" + register: csr_ocsp + +- name: Validate OCSP Must Staple CSR (assert) + assert: + that: + - "(csr_ocsp.stdout is search('\\s+TLS Feature:\\s*\\n\\s+status_request\\s+')) or + (csr_ocsp.stdout is search('\\s+1.3.6.1.5.5.7.1.24:\\s*\\n\\s+0\\.\\.\\.\\.\\s+'))" + +- name: Validate OCSP Must Staple CSR (assert idempotency) + assert: + that: + - csr_ocsp_idempotency is not changed + +- name: Validate ECC CSR (test - privatekey's public key) + shell: 'openssl ec -pubout -in {{ output_dir }}/privatekey2.pem' + register: privatekey_ecc_key + +- name: Validate ECC CSR (test - Common Name) + shell: "openssl req -noout -subject -in {{ output_dir }}/csr2.csr -nameopt oneline,-space_eq" + register: csr_ecc_cn + +- name: Validate ECC CSR (test - CSR pubkey) + shell: 'openssl req -noout -pubkey -in {{ output_dir }}/csr2.csr' + register: csr_ecc_pubkey + +- name: Validate ECC CSR (assert) + assert: + that: + - csr_ecc_cn.stdout.split('=')[-1] == 'www.ansible.com' + - csr_ecc_pubkey.stdout == privatekey_ecc_key.stdout + +- name: Validate CSR (text common name - Common Name) + shell: "openssl req -noout -subject -in {{ output_dir }}/csr3.csr -nameopt oneline,-space_eq" + register: csr3_cn + +- name: Validate CSR (assert) + assert: + that: + - csr3_cn.stdout.split('=')[-1] == 'This is for Ansible' + +- name: Validate country name idempotency and validation + assert: + that: + - country_idempotent_1 is changed + - country_idempotent_2 is not changed + - country_idempotent_3 is not changed + - country_fail_4 is failed + +- name: + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: Verify that broken CSR will be regenerated + assert: + that: + - output_broken is changed + +- name: Verify that subject key identifier handling works + assert: + that: + - subject_key_identifier_1 is changed + - subject_key_identifier_2 is not changed + - subject_key_identifier_3 is changed + - subject_key_identifier_4 is changed + - subject_key_identifier_5 is not changed + - subject_key_identifier_6 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: Verify that authority key identifier handling works + assert: + that: + - authority_key_identifier_1 is changed + - authority_key_identifier_2 is not changed + - authority_key_identifier_3 is changed + - authority_key_identifier_4 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: Verify that authority cert issuer / serial number handling works + assert: + that: + - authority_cert_issuer_sn_1 is changed + - authority_cert_issuer_sn_2 is not changed + - authority_cert_issuer_sn_3 is changed + - authority_cert_issuer_sn_4 is changed + - authority_cert_issuer_sn_5 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: Check backup + assert: + that: + - csr_backup_1 is changed + - csr_backup_1.backup_file is undefined + - csr_backup_2 is not changed + - csr_backup_2.backup_file is undefined + - csr_backup_3 is changed + - csr_backup_3.backup_file is string + - csr_backup_4 is changed + - csr_backup_4.backup_file is string + - csr_backup_5 is not changed + - csr_backup_5.backup_file is undefined + - csr_backup_4.csr is none + +- name: Check CSR with everything + assert: + that: + - everything_1 is changed + - everything_2 is not changed + - everything_3 is not changed + +- name: Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8) + assert: + that: + - generate_csr_ed25519_ed448.results[0] is failed + - generate_csr_ed25519_ed448.results[1] is failed + - generate_csr_ed25519_ed448.results[0].msg == 'Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.' + - generate_csr_ed25519_ed448.results[1].msg == 'Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.' + - generate_csr_ed25519_ed448_idempotent.results[0] is failed + - generate_csr_ed25519_ed448_idempotent.results[1] is failed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') and cryptography_version.stdout is version('2.8', '<') and generate_csr_ed25519_ed448_privatekey is not failed + +- name: Verify Ed25519 and Ed448 tests (for cryptography >= 2.8) + assert: + that: + - generate_csr_ed25519_ed448 is succeeded + - generate_csr_ed25519_ed448.results[0] is changed + - generate_csr_ed25519_ed448.results[1] is changed + - generate_csr_ed25519_ed448_idempotent is succeeded + - generate_csr_ed25519_ed448_idempotent.results[0] is not changed + - generate_csr_ed25519_ed448_idempotent.results[1] is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.8', '>=') and generate_csr_ed25519_ed448_privatekey is not failed diff --git a/tests/integration/targets/openssl_csr_info/aliases b/tests/integration/targets/openssl_csr_info/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssl_csr_info/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_csr_info/meta/main.yml b/tests/integration/targets/openssl_csr_info/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_csr_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_csr_info/tasks/impl.yml b/tests/integration/targets/openssl_csr_info/tasks/impl.yml new file mode 100644 index 00000000..2ab8b87b --- /dev/null +++ b/tests/integration/targets/openssl_csr_info/tasks/impl.yml @@ -0,0 +1,94 @@ +--- +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" + +- name: ({{select_crypto_backend}}) Get CSR info + openssl_csr_info: + path: '{{ output_dir }}/csr_1.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check whether subject behaves as expected + assert: + that: + - result.subject.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" + +- name: Check SubjectKeyIdentifier and AuthorityKeyIdentifier + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" + +- name: ({{select_crypto_backend}}) Get CSR info directly + openssl_csr_info: + content: '{{ lookup("file", output_dir ~ "/csr_1.csr") }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_direct + +- name: ({{select_crypto_backend}}) Compare output of direct and loaded info + assert: + that: + - result == result_direct + +- name: ({{select_crypto_backend}}) Get CSR info + openssl_csr_info: + path: '{{ output_dir }}/csr_2.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" + +- name: ({{select_crypto_backend}}) Get CSR info + openssl_csr_info: + path: '{{ output_dir }}/csr_3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" + +- name: ({{select_crypto_backend}}) Get CSR info + openssl_csr_info: + path: '{{ output_dir }}/csr_4.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + +- name: Update result list + set_fact: + info_results: "{{ info_results + [result] }}" diff --git a/tests/integration/targets/openssl_csr_info/tasks/main.yml b/tests/integration/targets/openssl_csr_info/tasks/main.yml new file mode 100644 index 00000000..e1794ad4 --- /dev/null +++ b/tests/integration/targets/openssl_csr_info/tasks/main.yml @@ -0,0 +1,161 @@ +--- +- name: Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey.pem' + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: Generate CSR 1 + openssl_csr: + path: '{{ output_dir }}/csr_1.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: + - Crypto Department + - ACME Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: no + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: yes + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: yes + ocsp_must_staple: yes + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 2 + openssl_csr: + path: '{{ output_dir }}/csr_2.csr' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + useCommonNameForSAN: no + basic_constraints: + - "CA:TRUE" + +- name: Generate CSR 3 + openssl_csr: + path: '{{ output_dir }}/csr_3.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + useCommonNameForSAN: no + subject_alt_name: + - "DNS:*.ansible.com" + - "DNS:*.example.org" + - "IP:DEAD:BEEF::1" + basic_constraints: + - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 4 + openssl_csr: + path: '{{ output_dir }}/csr_4.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + useCommonNameForSAN: no + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + +- name: Prepare result list + set_fact: + info_results: [] + +- name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + when: pyopenssl_version.stdout is version('0.15', '>=') + +- name: Prepare result list + set_fact: + pyopenssl_info_results: "{{ info_results }}" + info_results: [] + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.3', '>=') + +- name: Prepare result list + set_fact: + cryptography_info_results: "{{ info_results }}" + +- block: + - name: Dump pyOpenSSL results + debug: + var: pyopenssl_info_results + - name: Dump cryptography results + debug: + var: cryptography_info_results + - name: Compare results + assert: + that: + - ' (item.0 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict) + == (item.1 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict)' + quiet: yes + loop: "{{ pyopenssl_info_results | zip(cryptography_info_results) | list }}" + when: pyopenssl_version.stdout is version('0.15', '>=') and cryptography_version.stdout is version('1.3', '>=') + vars: + keys_to_ignore: + - deprecations + - subject_key_identifier + - authority_key_identifier + - authority_cert_issuer + - authority_cert_serial_number diff --git a/tests/integration/targets/openssl_csr_info/test_plugins/jinja_compatibility.py b/tests/integration/targets/openssl_csr_info/test_plugins/jinja_compatibility.py new file mode 100644 index 00000000..fc2b5f0f --- /dev/null +++ b/tests/integration/targets/openssl_csr_info/test_plugins/jinja_compatibility.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +def compatibility_in_test(a, b): + return a in b + + +class TestModule: + ''' Ansible math jinja2 tests ''' + + def tests(self): + return { + 'in': compatibility_in_test, + } diff --git a/tests/integration/targets/openssl_dhparam/aliases b/tests/integration/targets/openssl_dhparam/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssl_dhparam/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_dhparam/meta/main.yml b/tests/integration/targets/openssl_dhparam/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_dhparam/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_dhparam/tasks/impl.yml b/tests/integration/targets/openssl_dhparam/tasks/impl.yml new file mode 100644 index 00000000..d367436d --- /dev/null +++ b/tests/integration/targets/openssl_dhparam/tasks/impl.yml @@ -0,0 +1,101 @@ +--- +# The tests for this module generate unsafe parameters for testing purposes; +# otherwise tests would be too slow. Use sizes of at least 2048 in production! +- name: "[{{ select_crypto_backend }}] Generate parameter" + openssl_dhparam: + size: 768 + path: '{{ output_dir }}/dh768.pem' + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: yes + register: dhparam + +- name: "[{{ select_crypto_backend }}] Don't regenerate parameters with no change" + openssl_dhparam: + size: 768 + path: '{{ output_dir }}/dh768.pem' + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: yes + register: dhparam_changed + +- name: "[{{ select_crypto_backend }}] Generate parameters with size option" + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + select_crypto_backend: "{{ select_crypto_backend }}" + +- name: "[{{ select_crypto_backend }}] Don't regenerate parameters with size option and no change" + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_changed_512 + +- copy: + src: '{{ output_dir }}/dh768.pem' + remote_src: yes + dest: '{{ output_dir }}/dh512.pem' + +- name: "[{{ select_crypto_backend }}] Re-generate if size is different" + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_changed_to_512 + +- name: "[{{ select_crypto_backend }}] Force re-generate parameters with size option" + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + force: yes + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_changed_force + +- name: "[{{ select_crypto_backend }}] Create broken params" + copy: + dest: "{{ output_dir }}/dhbroken.pem" + content: "broken" +- name: "[{{ select_crypto_backend }}] Regenerate broken params" + openssl_dhparam: + path: '{{ output_dir }}/dhbroken.pem' + size: 512 + force: yes + select_crypto_backend: "{{ select_crypto_backend }}" + register: output_broken + +- name: "[{{ select_crypto_backend }}] Generate params" + openssl_dhparam: + path: '{{ output_dir }}/dh_backup.pem' + size: 512 + backup: yes + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_1 +- name: "[{{ select_crypto_backend }}] Generate params (idempotent)" + openssl_dhparam: + path: '{{ output_dir }}/dh_backup.pem' + size: 512 + backup: yes + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_2 +- name: "[{{ select_crypto_backend }}] Generate params (change)" + openssl_dhparam: + path: '{{ output_dir }}/dh_backup.pem' + size: 512 + force: yes + backup: yes + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_3 +- name: "[{{ select_crypto_backend }}] Generate params (remove)" + openssl_dhparam: + path: '{{ output_dir }}/dh_backup.pem' + state: absent + backup: yes + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: yes + register: dhparam_backup_4 +- name: "[{{ select_crypto_backend }}] Generate params (remove, idempotent)" + openssl_dhparam: + path: '{{ output_dir }}/dh_backup.pem' + state: absent + backup: yes + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_5 diff --git a/tests/integration/targets/openssl_dhparam/tasks/main.yml b/tests/integration/targets/openssl_dhparam/tasks/main.yml new file mode 100644 index 00000000..d8c106b3 --- /dev/null +++ b/tests/integration/targets/openssl_dhparam/tasks/main.yml @@ -0,0 +1,38 @@ +--- +# The tests for this module generate unsafe parameters for testing purposes; +# otherwise tests would be too slow. Use sizes of at least 2048 in production! + +- name: Run module with backend autodetection + openssl_dhparam: + path: '{{ output_dir }}/dh_backend_selection.pem' + size: 512 + +- block: + - name: Running tests with OpenSSL backend + include_tasks: impl.yml + + - include_tasks: ../tests/validate.yml + + vars: + select_crypto_backend: openssl + # when: openssl_version.stdout is version('1.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + + - include_tasks: ../tests/validate.yml + + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('2.0', '>=') diff --git a/tests/integration/targets/openssl_dhparam/tests/validate.yml b/tests/integration/targets/openssl_dhparam/tests/validate.yml new file mode 100644 index 00000000..e7d3951a --- /dev/null +++ b/tests/integration/targets/openssl_dhparam/tests/validate.yml @@ -0,0 +1,58 @@ +--- +- name: "[{{ select_crypto_backend }}] Validate generated params" + shell: 'openssl dhparam -in {{ output_dir }}/{{ item }}.pem -noout -check' + with_items: + - dh768 + - dh512 + +- name: "[{{ select_crypto_backend }}] Get bit size of 768" + shell: 'openssl dhparam -noout -in {{ output_dir }}/dh768.pem -text | head -n1 | sed -ne "s@.*(\\([[:digit:]]\{1,\}\\) bit).*@\\1@p"' + register: bit_size_dhparam + +- name: "[{{ select_crypto_backend }}] Check bit size of default" + assert: + that: + - bit_size_dhparam.stdout == "768" + +- name: "[{{ select_crypto_backend }}] Get bit size of 512" + shell: 'openssl dhparam -noout -in {{ output_dir }}/dh512.pem -text | head -n1 | sed -ne "s@.*(\\([[:digit:]]\{1,\}\\) bit).*@\\1@p"' + register: bit_size_dhparam_512 + +- name: "[{{ select_crypto_backend }}] Check bit size of default" + assert: + that: + - bit_size_dhparam_512.stdout == "512" + +- name: "[{{ select_crypto_backend }}] Check if changed works correctly" + assert: + that: + - dhparam_changed is not changed + - dhparam_changed_512 is not changed + - dhparam_changed_to_512 is changed + - dhparam_changed_force is changed + +- name: "[{{ select_crypto_backend }}] Make sure correct values are returned" + assert: + that: + - dhparam.dhparams == lookup('file', output_dir ~ '/dh768.pem', rstrip=False) + - dhparam.dhparams == dhparam_changed.dhparams + +- name: "[{{ select_crypto_backend }}] Verify that broken params will be regenerated" + assert: + that: + - output_broken is changed + +- name: "[{{ select_crypto_backend }}] Check backup" + assert: + that: + - dhparam_backup_1 is changed + - dhparam_backup_1.backup_file is undefined + - dhparam_backup_2 is not changed + - dhparam_backup_2.backup_file is undefined + - dhparam_backup_3 is changed + - dhparam_backup_3.backup_file is string + - dhparam_backup_4 is changed + - dhparam_backup_4.backup_file is string + - dhparam_backup_5 is not changed + - dhparam_backup_5.backup_file is undefined + - dhparam_backup_4.dhparams is none diff --git a/tests/integration/targets/openssl_pkcs12/aliases b/tests/integration/targets/openssl_pkcs12/aliases new file mode 100644 index 00000000..80cc8d7d --- /dev/null +++ b/tests/integration/targets/openssl_pkcs12/aliases @@ -0,0 +1,4 @@ +destructive +needs/root +shippable/posix/group1 +skip/aix diff --git a/tests/integration/targets/openssl_pkcs12/meta/main.yml b/tests/integration/targets/openssl_pkcs12/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_pkcs12/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_pkcs12/tasks/impl.yml b/tests/integration/targets/openssl_pkcs12/tasks/impl.yml new file mode 100644 index 00000000..c56b165e --- /dev/null +++ b/tests/integration/targets/openssl_pkcs12/tasks/impl.yml @@ -0,0 +1,228 @@ +- block: + - name: Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/ansible_pkey.pem' + - name: Generate privatekey2 + openssl_privatekey: + path: '{{ output_dir }}/ansible_pkey2.pem' + - name: Generate privatekey3 + openssl_privatekey: + path: '{{ output_dir }}/ansible_pkey3.pem' + - name: Generate CSR + openssl_csr: + path: '{{ output_dir }}/ansible.csr' + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + commonName: www.ansible.com + - name: Generate CSR 2 + openssl_csr: + path: '{{ output_dir }}/ansible2.csr' + privatekey_path: '{{ output_dir }}/ansible_pkey2.pem' + commonName: www2.ansible.com + - name: Generate CSR 3 + openssl_csr: + path: '{{ output_dir }}/ansible3.csr' + privatekey_path: '{{ output_dir }}/ansible_pkey3.pem' + commonName: www3.ansible.com + - name: Generate certificate + openssl_certificate: + path: '{{ output_dir }}/{{ item.name }}.crt' + privatekey_path: '{{ output_dir }}/{{ item.pkey }}' + csr_path: '{{ output_dir }}/{{ item.name }}.csr' + provider: selfsigned + loop: + - name: ansible + pkey: ansible_pkey.pem + - name: ansible2 + pkey: ansible_pkey2.pem + - name: ansible3 + pkey: ansible_pkey3.pem + - name: Generate PKCS#12 file + openssl_pkcs12: + path: '{{ output_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + return_content: true + register: p12_standard + - name: Generate PKCS#12 file again, idempotency + openssl_pkcs12: + path: '{{ output_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + return_content: true + register: p12_standard_idempotency + - name: Read ansible.p12 + slurp: + src: '{{ output_dir }}/ansible.p12' + register: ansible_p12_content + - name: Validate PKCS#12 + assert: + that: + - p12_standard.pkcs12 == ansible_p12_content.content + - p12_standard_idempotency.pkcs12 == p12_standard.pkcs12 + - name: Generate PKCS#12 file (force) + openssl_pkcs12: + path: '{{ output_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + force: true + register: p12_force + - name: Generate PKCS#12 file (force + change mode) + openssl_pkcs12: + path: '{{ output_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + force: true + mode: '0644' + register: p12_force_and_mode + - name: Dump PKCS#12 + openssl_pkcs12: + src: '{{ output_dir }}/ansible.p12' + path: '{{ output_dir }}/ansible_parse.pem' + action: parse + state: present + - name: Generate PKCS#12 file with multiple certs + openssl_pkcs12: + path: '{{ output_dir }}/ansible_multi_certs.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + ca_certificates: + - '{{ output_dir }}/ansible2.crt' + - '{{ output_dir }}/ansible3.crt' + state: present + register: p12_multiple_certs + - name: Generate PKCS#12 file with multiple certs, again (idempotency) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_multi_certs.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + ca_certificates: + - '{{ output_dir }}/ansible2.crt' + - '{{ output_dir }}/ansible3.crt' + state: present + register: p12_multiple_certs_idempotency + - name: Dump PKCS#12 with multiple certs + openssl_pkcs12: + src: '{{ output_dir }}/ansible_multi_certs.p12' + path: '{{ output_dir }}/ansible_parse_multi_certs.pem' + action: parse + state: present + - name: Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + - name: Generate PKCS#12 file (password fail 1) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_pw1.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + privatekey_passphrase: hunter2 + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + ignore_errors: true + register: passphrase_error_1 + - name: Generate PKCS#12 file (password fail 2) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_pw2.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + ignore_errors: true + register: passphrase_error_2 + - name: Generate PKCS#12 file (password fail 3) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_pw3.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + ignore_errors: true + register: passphrase_error_3 + - name: Generate PKCS#12 file, no privatekey + openssl_pkcs12: + path: '{{ output_dir }}/ansible_no_pkey.p12' + friendly_name: abracadabra + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + register: p12_no_pkey + - name: Create broken PKCS#12 + copy: + dest: '{{ output_dir }}/broken.p12' + content: broken + - name: Regenerate broken PKCS#12 + openssl_pkcs12: + path: '{{ output_dir }}/broken.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + force: true + mode: '0644' + register: output_broken + - name: Generate PKCS#12 file + openssl_pkcs12: + path: '{{ output_dir }}/ansible_backup.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + backup: true + register: p12_backup_1 + - name: Generate PKCS#12 file (idempotent) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_backup.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + backup: true + register: p12_backup_2 + - name: Generate PKCS#12 file (change) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_backup.p12' + friendly_name: abra + privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + certificate_path: '{{ output_dir }}/ansible.crt' + state: present + force: true + backup: true + register: p12_backup_3 + - name: Generate PKCS#12 file (remove) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_backup.p12' + state: absent + backup: true + return_content: true + register: p12_backup_4 + - name: Generate PKCS#12 file (remove, idempotent) + openssl_pkcs12: + path: '{{ output_dir }}/ansible_backup.p12' + state: absent + backup: true + register: p12_backup_5 + - import_tasks: ../tests/validate.yml + always: + - name: Delete PKCS#12 file + openssl_pkcs12: + state: absent + path: '{{ output_dir }}/{{ item }}.p12' + loop: + - ansible + - ansible_no_pkey + - ansible_multi_certs + - ansible_pw1 + - ansible_pw2 + - ansible_pw3 diff --git a/tests/integration/targets/openssl_pkcs12/tasks/main.yml b/tests/integration/targets/openssl_pkcs12/tasks/main.yml new file mode 100644 index 00000000..a11ba75a --- /dev/null +++ b/tests/integration/targets/openssl_pkcs12/tasks/main.yml @@ -0,0 +1,4 @@ +--- +- name: Run tests + include_tasks: impl.yml + when: pyopenssl_version.stdout is version('17.1.0', '>=') diff --git a/tests/integration/targets/openssl_pkcs12/tests/validate.yml b/tests/integration/targets/openssl_pkcs12/tests/validate.yml new file mode 100644 index 00000000..baff2282 --- /dev/null +++ b/tests/integration/targets/openssl_pkcs12/tests/validate.yml @@ -0,0 +1,57 @@ +--- +- name: 'Validate PKCS#12' + command: "openssl pkcs12 -info -in {{ output_dir }}/ansible.p12 -nodes -passin pass:''" + register: p12 + +- name: 'Validate PKCS#12 with no private key' + command: "openssl pkcs12 -info -in {{ output_dir }}/ansible_no_pkey.p12 -nodes -passin pass:''" + register: p12_validate_no_pkey + +- name: 'Validate PKCS#12 with multiple certs' + shell: "openssl pkcs12 -info -in {{ output_dir }}/ansible_multi_certs.p12 -nodes -passin pass:'' | grep subject" + register: p12_validate_multi_certs + +- name: 'Validate PKCS#12 (assert)' + assert: + that: + - p12.stdout_lines[2].split(':')[-1].strip() == 'abracadabra' + - p12_standard.mode == '0400' + - p12_no_pkey.changed + - p12_validate_no_pkey.stdout_lines[-1] == '-----END CERTIFICATE-----' + - p12_force.changed + - p12_force_and_mode.mode == '0644' and p12_force_and_mode.changed + - not p12_standard_idempotency.changed + - not p12_multiple_certs_idempotency.changed + - "'www.' in p12_validate_multi_certs.stdout" + - "'www2.' in p12_validate_multi_certs.stdout" + - "'www3.' in p12_validate_multi_certs.stdout" + +- name: Check passphrase on private key + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: "Verify that broken PKCS#12 will be regenerated" + assert: + that: + - output_broken is changed + +- name: Check backup + assert: + that: + - p12_backup_1 is changed + - p12_backup_1.backup_file is undefined + - p12_backup_2 is not changed + - p12_backup_2.backup_file is undefined + - p12_backup_3 is changed + - p12_backup_3.backup_file is string + - p12_backup_4 is changed + - p12_backup_4.backup_file is string + - p12_backup_5 is not changed + - p12_backup_5.backup_file is undefined + - p12_backup_4.pkcs12 is none diff --git a/tests/integration/targets/openssl_privatekey/aliases b/tests/integration/targets/openssl_privatekey/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssl_privatekey/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_privatekey/meta/main.yml b/tests/integration/targets/openssl_privatekey/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_privatekey/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_privatekey/tasks/impl.yml b/tests/integration/targets/openssl_privatekey/tasks/impl.yml new file mode 100644 index 00000000..a1e501cd --- /dev/null +++ b/tests/integration/targets/openssl_privatekey/tasks/impl.yml @@ -0,0 +1,820 @@ +--- +- name: Generate privatekey1 - standard + openssl_privatekey: + path: '{{ output_dir }}/privatekey1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: privatekey1 + +- name: Generate privatekey1 - standard (idempotence) + openssl_privatekey: + path: '{{ output_dir }}/privatekey1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: privatekey1_idempotence + +- name: Generate privatekey2 - size 2048 + openssl_privatekey: + path: '{{ output_dir }}/privatekey2.pem' + size: 2048 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate privatekey3 - type DSA + openssl_privatekey: + path: '{{ output_dir }}/privatekey3.pem' + type: DSA + size: 3072 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate privatekey4 - standard + openssl_privatekey: + path: '{{ output_dir }}/privatekey4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Delete privatekey4 - standard + openssl_privatekey: + state: absent + path: '{{ output_dir }}/privatekey4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: privatekey4_delete + +- name: Delete privatekey4 - standard (idempotence) + openssl_privatekey: + state: absent + path: '{{ output_dir }}/privatekey4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey4_delete_idempotence + +- name: Generate privatekey5 - standard - with passphrase + openssl_privatekey: + path: '{{ output_dir }}/privatekey5.pem' + passphrase: ansible + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate privatekey5 - standard - idempotence + openssl_privatekey: + path: '{{ output_dir }}/privatekey5.pem' + passphrase: ansible + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_idempotence + +- name: Generate privatekey6 - standard - with non-ASCII passphrase + openssl_privatekey: + path: '{{ output_dir }}/privatekey6.pem' + passphrase: ànsïblé + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + +- set_fact: + ecc_types: [] + when: select_crypto_backend == 'pyopenssl' +- set_fact: + ecc_types: + - curve: secp384r1 + openssl_name: secp384r1 + min_cryptography_version: "0.5" + - curve: secp521r1 + openssl_name: secp521r1 + min_cryptography_version: "0.5" + - curve: secp224r1 + openssl_name: secp224r1 + min_cryptography_version: "0.5" + - curve: secp192r1 + openssl_name: prime192v1 + min_cryptography_version: "0.5" + - curve: secp256r1 + openssl_name: secp256r1 + min_cryptography_version: "0.5" + - curve: secp256k1 + openssl_name: secp256k1 + min_cryptography_version: "0.9" + - curve: brainpoolP256r1 + openssl_name: brainpoolP256r1 + min_cryptography_version: "2.2" + - curve: brainpoolP384r1 + openssl_name: brainpoolP384r1 + min_cryptography_version: "2.2" + - curve: brainpoolP512r1 + openssl_name: brainpoolP512r1 + min_cryptography_version: "2.2" + - curve: sect571k1 + openssl_name: sect571k1 + min_cryptography_version: "0.5" + - curve: sect409k1 + openssl_name: sect409k1 + min_cryptography_version: "0.5" + - curve: sect283k1 + openssl_name: sect283k1 + min_cryptography_version: "0.5" + - curve: sect233k1 + openssl_name: sect233k1 + min_cryptography_version: "0.5" + - curve: sect163k1 + openssl_name: sect163k1 + min_cryptography_version: "0.5" + - curve: sect571r1 + openssl_name: sect571r1 + min_cryptography_version: "0.5" + - curve: sect409r1 + openssl_name: sect409r1 + min_cryptography_version: "0.5" + - curve: sect283r1 + openssl_name: sect283r1 + min_cryptography_version: "0.5" + - curve: sect233r1 + openssl_name: sect233r1 + min_cryptography_version: "0.5" + - curve: sect163r2 + openssl_name: sect163r2 + min_cryptography_version: "0.5" + when: select_crypto_backend == 'cryptography' + +- name: Test ECC key generation + openssl_privatekey: + path: '{{ output_dir }}/privatekey-{{ item.curve }}.pem' + type: ECC + curve: "{{ item.curve }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: | + cryptography_version.stdout is version(item.min_cryptography_version, '>=') and + item.openssl_name in openssl_ecc_list + loop: "{{ ecc_types }}" + loop_control: + label: "{{ item.curve }}" + register: privatekey_ecc_generate + +- name: Test ECC key generation (idempotency) + openssl_privatekey: + path: '{{ output_dir }}/privatekey-{{ item.curve }}.pem' + type: ECC + curve: "{{ item.curve }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: | + cryptography_version.stdout is version(item.min_cryptography_version, '>=') and + item.openssl_name in openssl_ecc_list + loop: "{{ ecc_types }}" + loop_control: + label: "{{ item.curve }}" + register: privatekey_ecc_idempotency + +- block: + - name: Test other type generation + openssl_privatekey: + path: '{{ output_dir }}/privatekey-{{ item.type }}.pem' + type: "{{ item.type }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: cryptography_version.stdout is version(item.min_version, '>=') + loop: "{{ types }}" + loop_control: + label: "{{ item.type }}" + ignore_errors: yes + register: privatekey_t1_generate + + - name: Test other type generation (idempotency) + openssl_privatekey: + path: '{{ output_dir }}/privatekey-{{ item.type }}.pem' + type: "{{ item.type }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: cryptography_version.stdout is version(item.min_version, '>=') + loop: "{{ types }}" + loop_control: + label: "{{ item.type }}" + ignore_errors: yes + register: privatekey_t1_idempotency + + when: select_crypto_backend == 'cryptography' + vars: + types: + - type: X25519 + min_version: '2.5' + - type: Ed25519 + min_version: '2.6' + - type: Ed448 + min_version: '2.6' + - type: X448 + min_version: '2.6' + +- name: Generate privatekey with passphrase + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + backup: yes + register: passphrase_1 + +- name: Generate privatekey with passphrase (idempotent) + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + backup: yes + register: passphrase_2 + +- name: Regenerate privatekey without passphrase + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: yes + register: passphrase_3 + +- name: Regenerate privatekey without passphrase (idempotent) + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: yes + register: passphrase_4 + +- name: Regenerate privatekey with passphrase + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + backup: yes + register: passphrase_5 + +- name: Create broken key + copy: + dest: "{{ output_dir }}/broken" + content: "broken" +- name: Regenerate broken key + openssl_privatekey: + path: '{{ output_dir }}/broken.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: output_broken + +- name: Remove module + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + backup: yes + state: absent + register: remove_1 + +- name: Remove module (idempotent) + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + backup: yes + state: absent + register: remove_2 + +- name: Generate privatekey_mode (mode 0400) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_mode.pem' + mode: '0400' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_mode_1 +- name: Stat for privatekey_mode + stat: + path: '{{ output_dir }}/privatekey_mode.pem' + register: privatekey_mode_1_stat + +- name: Generate privatekey_mode (mode 0400, idempotency) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_mode.pem' + mode: '0400' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_mode_2 + +- name: Generate privatekey_mode (mode 0400, force) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_mode.pem' + mode: '0400' + force: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_mode_3 +- name: Stat for privatekey_mode + stat: + path: '{{ output_dir }}/privatekey_mode.pem' + register: privatekey_mode_3_stat + +- block: + - name: Generate privatekey_fmt_1 - auto format + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: auto + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_1 + + - name: Generate privatekey_fmt_1 - auto format (idempotent) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: auto + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_2 + + - name: Generate privatekey_fmt_1 - PKCS1 format + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: pkcs1 + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_3 + + - name: Generate privatekey_fmt_1 - PKCS8 format + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: pkcs8 + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_4 + + - name: Generate privatekey_fmt_1 - PKCS8 format (idempotent) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: pkcs8 + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_5 + + - name: Generate privatekey_fmt_1 - auto format (ignore) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: auto_ignore + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_6 + + - name: Generate privatekey_fmt_1 - auto format (no ignore) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: auto + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_7 + + - name: Generate privatekey_fmt_1 - raw format (fail) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: raw + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: privatekey_fmt_1_step_8 + + - name: Generate privatekey_fmt_1 - PKCS8 format (convert) + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_9_before + + - name: Generate privatekey_fmt_1 - PKCS8 format (convert) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + format: pkcs8 + format_mismatch: convert + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_9 + + - name: Generate privatekey_fmt_1 - PKCS8 format (convert) + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_fmt_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_9_after + + when: 'select_crypto_backend == "cryptography"' + +- block: + - name: Generate privatekey_fmt_2 - PKCS8 format + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_2.pem' + type: X448 + format: pkcs8 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: privatekey_fmt_2_step_1 + + - name: Generate privatekey_fmt_2 - PKCS8 format (idempotent) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_2.pem' + type: X448 + format: pkcs8 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: privatekey_fmt_2_step_2 + + - name: Generate privatekey_fmt_2 - raw format + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_2.pem' + type: X448 + format: raw + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + ignore_errors: yes + register: privatekey_fmt_2_step_3 + + - name: Read privatekey_fmt_2.pem + slurp: + src: "{{ output_dir }}/privatekey_fmt_2.pem" + ignore_errors: yes + register: content + + - name: Generate privatekey_fmt_2 - verify that returned content is base64 encoded + assert: + that: + - privatekey_fmt_2_step_3.privatekey == content.content + when: privatekey_fmt_2_step_1 is not failed + + - name: Generate privatekey_fmt_2 - raw format (idempotent) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_2.pem' + type: X448 + format: raw + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + ignore_errors: yes + register: privatekey_fmt_2_step_4 + + - name: Read privatekey_fmt_2.pem + slurp: + src: "{{ output_dir }}/privatekey_fmt_2.pem" + ignore_errors: yes + register: content + + - name: Generate privatekey_fmt_2 - verify that returned content is base64 encoded + assert: + that: + - privatekey_fmt_2_step_4.privatekey == content.content + when: privatekey_fmt_2_step_1 is not failed + + - name: Generate privatekey_fmt_2 - auto format (ignore) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_2.pem' + type: X448 + format: auto_ignore + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + ignore_errors: yes + register: privatekey_fmt_2_step_5 + + - name: Read privatekey_fmt_2.pem + slurp: + src: "{{ output_dir }}/privatekey_fmt_2.pem" + ignore_errors: yes + register: content + + - name: Generate privatekey_fmt_2 - verify that returned content is base64 encoded + assert: + that: + - privatekey_fmt_2_step_5.privatekey == content.content + when: privatekey_fmt_2_step_1 is not failed + + - name: Generate privatekey_fmt_2 - auto format (no ignore) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_fmt_2.pem' + type: X448 + format: auto + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + ignore_errors: yes + register: privatekey_fmt_2_step_6 + + - name: Generate privatekey_fmt_2 - verify that returned content is not base64 encoded + assert: + that: + - privatekey_fmt_2_step_6.privatekey == lookup('file', output_dir ~ '/privatekey_fmt_2.pem', rstrip=False) + when: privatekey_fmt_2_step_1 is not failed + + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=")' + + + +# Test regenerate option + +- name: Regenerate - setup simple keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1024 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" +- name: Regenerate - setup password protected keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: 1024 + passphrase: hunter2 + cipher: "{{ 'aes256' if select_crypto_backend == 'pyopenssl' else 'auto' }}" + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" +- name: Regenerate - setup broken keys + copy: + dest: '{{ output_dir }}/regenerate-c-{{ item }}.pem' + content: 'broken key' + mode: '0700' + loop: "{{ regenerate_values }}" + +- name: Regenerate - modify broken keys (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-c-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg or 'Cannot load raw key' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg or 'Cannot load raw key' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg or 'Cannot load raw key' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify broken keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-c-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg or 'Cannot load raw key' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg or 'Cannot load raw key' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg or 'Cannot load raw key' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - modify password protected keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - not modify regular keys + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: Regenerate - adjust key size (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1048 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key size + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: 1048 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always.pem' + dest: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + +- name: Regenerate - adjust key type (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: Regenerate - adjust key type + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- block: + - name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always.pem' + dest: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + + - name: Regenerate - format mismatch (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs8 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result + - assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong format. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: Regenerate - format mismatch + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs8 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: yes + register: result + - assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong format. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: Regenerate - redistribute keys + copy: + src: '{{ output_dir }}/regenerate-a-always.pem' + dest: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + + - name: Regenerate - convert format (check mode) + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs1 + format_mismatch: convert + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + loop: "{{ regenerate_values }}" + register: result + - assert: + that: + - result.results[0] is changed + - result.results[1] is changed + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: Regenerate - convert format + openssl_privatekey: + path: '{{ output_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: 1024 + format: pkcs1 + format_mismatch: convert + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + register: result + - assert: + that: + - result.results[0] is changed + - result.results[1] is changed + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + # for all values but 'always', the key should have not been regenerated. + # verify this by comparing fingerprints: + - result.results[0].fingerprint == result.results[1].fingerprint + - result.results[0].fingerprint == result.results[2].fingerprint + - result.results[0].fingerprint == result.results[3].fingerprint + - result.results[0].fingerprint != result.results[4].fingerprint + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=")' diff --git a/tests/integration/targets/openssl_privatekey/tasks/main.yml b/tests/integration/targets/openssl_privatekey/tasks/main.yml new file mode 100644 index 00000000..f0a8e32b --- /dev/null +++ b/tests/integration/targets/openssl_privatekey/tasks/main.yml @@ -0,0 +1,108 @@ +--- +- name: Find out which elliptic curves are supported by installed OpenSSL + command: openssl ecparam -list_curves + register: openssl_ecc + +- name: Compile list of elliptic curves supported by OpenSSL + set_fact: + openssl_ecc_list: | + {{ + openssl_ecc.stdout_lines + | map('regex_search', '^ *([a-zA-Z0-9_-]+) *: .*$') + | select() + | map('regex_replace', '^ *([a-zA-Z0-9_-]+) *: .*$', '\1') + | list + }} + when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6' + # CentOS comes with a very old jinja2 which does not include the map() filter... +- name: Compile list of elliptic curves supported by OpenSSL (CentOS 6) + set_fact: + openssl_ecc_list: + - secp384r1 + - secp521r1 + - prime256v1 + when: ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6' + +- name: List of elliptic curves supported by OpenSSL + debug: var=openssl_ecc_list + +- name: Run module with backend autodetection + openssl_privatekey: + path: '{{ output_dir }}/privatekey_backend_selection.pem' + +- block: + - name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: pyopenssl + + # FIXME: minimal pyOpenSSL version?! + when: pyopenssl_version.stdout is version('0.6', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('0.5', '>=') + +- name: Check that fingerprints do not depend on the backend + block: + - name: "Fingerprint comparison: pyOpenSSL" + openssl_privatekey: + path: '{{ output_dir }}/fingerprint-{{ item }}.pem' + type: "{{ item }}" + size: 1024 + select_crypto_backend: pyopenssl + loop: + - RSA + - DSA + register: fingerprint_pyopenssl + + - name: "Fingerprint comparison: cryptography" + openssl_privatekey: + path: '{{ output_dir }}/fingerprint-{{ item }}.pem' + type: "{{ item }}" + size: 1024 + select_crypto_backend: cryptography + loop: + - RSA + - DSA + register: fingerprint_cryptography + + - name: Verify that keys were not regenerated + assert: + that: + - fingerprint_cryptography is not changed + + - name: Verify that fingerprints match + assert: + that: item.0.fingerprint[item.2] == item.1.fingerprint[item.2] + when: item.0 is not skipped and item.1 is not skipped + loop: | + {{ query('nested', + fingerprint_pyopenssl.results | zip(fingerprint_cryptography.results), + fingerprint_pyopenssl.results[0].fingerprint.keys() + ) if fingerprint_pyopenssl.results[0].fingerprint else [] }} + loop_control: + label: "{{ [item.0.item, item.2] }}" + when: pyopenssl_version.stdout is version('0.6', '>=') and cryptography_version.stdout is version('0.5', '>=') diff --git a/tests/integration/targets/openssl_privatekey/tests/validate.yml b/tests/integration/targets/openssl_privatekey/tests/validate.yml new file mode 100644 index 00000000..56649715 --- /dev/null +++ b/tests/integration/targets/openssl_privatekey/tests/validate.yml @@ -0,0 +1,215 @@ +--- +- set_fact: + system_potentially_has_no_algorithm_support: "{{ ansible_os_family == 'FreeBSD' }}" + +- name: Validate privatekey1 idempotency and content returned + assert: + that: + - privatekey1_idempotence is not changed + - privatekey1.privatekey == lookup('file', output_dir ~ '/privatekey1.pem', rstrip=False) + - privatekey1.privatekey == privatekey1_idempotence.privatekey + + +- name: Validate privatekey1 (test - RSA key with size 4096 bits) + shell: "openssl rsa -noout -text -in {{ output_dir }}/privatekey1.pem | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey1 + +- name: Validate privatekey1 (assert - RSA key with size 4096 bits) + assert: + that: + - privatekey1.stdout == '4096' + + +- name: Validate privatekey2 (test - RSA key with size 2048 bits) + shell: "openssl rsa -noout -text -in {{ output_dir }}/privatekey2.pem | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey2 + +- name: Validate privatekey2 (assert - RSA key with size 2048 bits) + assert: + that: + - privatekey2.stdout == '2048' + + +- name: Validate privatekey3 (test - DSA key with size 3072 bits) + shell: "openssl dsa -noout -text -in {{ output_dir }}/privatekey3.pem | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey3 + +- name: Validate privatekey3 (assert - DSA key with size 3072 bits) + assert: + that: + - privatekey3.stdout == '3072' + + +- name: Validate privatekey4 (test - Ensure key has been removed) + stat: + path: '{{ output_dir }}/privatekey4.pem' + register: privatekey4 + +- name: Validate privatekey4 (assert - Ensure key has been removed) + assert: + that: + - privatekey4.stat.exists == False + +- name: Validate privatekey4 removal behavior + assert: + that: + - privatekey4_delete is changed + - privatekey4_delete.privatekey is none + - privatekey4_delete_idempotence is not changed + + +- name: Validate privatekey5 (test - Passphrase protected key + idempotence) + shell: "openssl rsa -noout -text -in {{ output_dir }}/privatekey5.pem -passin pass:ansible | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey5 + # Current version of OS/X that runs in the CI (10.11) does not have an up to date version of the OpenSSL library + # leading to this test to fail when run in the CI. However, this test has been run for 10.12 and has returned succesfully. + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate privatekey5 (assert - Passphrase protected key + idempotence) + assert: + that: + - privatekey5.stdout == '4096' + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate privatekey5 idempotence (assert - Passphrase protected key + idempotence) + assert: + that: + - privatekey5_idempotence is not changed + + +- name: Validate privatekey6 (test - Passphrase protected key with non ascii character) + shell: "openssl rsa -noout -text -in {{ output_dir }}/privatekey6.pem -passin pass:ànsïblé | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey6 + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate privatekey6 (assert - Passphrase protected key with non ascii character) + assert: + that: + - privatekey6.stdout == '4096' + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate ECC generation (dump with OpenSSL) + shell: "openssl ec -in {{ output_dir }}/privatekey-{{ item.item.curve }}.pem -noout -text | grep 'ASN1 OID: ' | sed 's/ASN1 OID: \\([^ ]*\\)/\\1/'" + loop: "{{ privatekey_ecc_generate.results }}" + register: privatekey_ecc_dump + when: openssl_version.stdout is version('0.9.8zh', '>=') and 'skip_reason' not in item + loop_control: + label: "{{ item.item.curve }}" + +- name: Validate ECC generation + assert: + that: + - item is changed + loop: "{{ privatekey_ecc_generate.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.curve }}" + +- name: Validate ECC generation (curve type) + assert: + that: + - "'skip_reason' in item or item.item.item.openssl_name == item.stdout" + loop: "{{ privatekey_ecc_dump.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.item }} - {{ item.stdout if 'stdout' in item else '' }}" + +- name: Validate ECC generation idempotency + assert: + that: + - item is not changed + loop: "{{ privatekey_ecc_idempotency.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.curve }}" + +- name: Validate other type generation (just check changed) + assert: + that: + - (item is succeeded and item is changed) or + (item is failed and 'Cryptography backend does not support the algorithm required for ' in item.msg and system_potentially_has_no_algorithm_support) + loop: "{{ privatekey_t1_generate.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.type }}" + +- name: Validate other type generation idempotency + assert: + that: + - (item is succeeded and item is not changed) or + (item is failed and 'Cryptography backend does not support the algorithm required for ' in item.msg and system_potentially_has_no_algorithm_support) + loop: "{{ privatekey_t1_idempotency.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.type }}" + +- name: Validate passphrase changing + assert: + that: + - passphrase_1 is changed + - passphrase_2 is not changed + - passphrase_3 is changed + - passphrase_4 is not changed + - passphrase_5 is changed + - passphrase_1.backup_file is undefined + - passphrase_2.backup_file is undefined + - passphrase_3.backup_file is string + - passphrase_4.backup_file is undefined + - passphrase_5.backup_file is string + +- name: Verify that broken key will be regenerated + assert: + that: + - output_broken is changed + +- name: Validate remove + assert: + that: + - remove_1 is changed + - remove_2 is not changed + - remove_1.backup_file is string + - remove_2.backup_file is undefined + +- name: Validate mode + assert: + that: + - privatekey_mode_1 is changed + - privatekey_mode_1_stat.stat.mode == '0400' + - privatekey_mode_2 is not changed + - privatekey_mode_3 is changed + - privatekey_mode_3_stat.stat.mode == '0400' + - privatekey_mode_1_stat.stat.mtime != privatekey_mode_3_stat.stat.mtime + +- name: Validate format 1 + assert: + that: + - privatekey_fmt_1_step_1 is changed + - privatekey_fmt_1_step_2 is not changed + - privatekey_fmt_1_step_3 is not changed + - privatekey_fmt_1_step_4 is changed + - privatekey_fmt_1_step_5 is not changed + - privatekey_fmt_1_step_6 is not changed + - privatekey_fmt_1_step_7 is changed + - privatekey_fmt_1_step_8 is failed + - privatekey_fmt_1_step_9 is changed + - privatekey_fmt_1_step_9_before.public_key == privatekey_fmt_1_step_9_after.public_key + when: 'select_crypto_backend == "cryptography"' + +- name: Validate format 2 (failed) + assert: + that: + - system_potentially_has_no_algorithm_support + - privatekey_fmt_2_step_1 is failed + - "'Cryptography backend does not support the algorithm required for ' in privatekey_fmt_2_step_1.msg" + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=") and privatekey_fmt_2_step_1 is failed' + +- name: Validate format 2 + assert: + that: + - privatekey_fmt_2_step_1 is succeeded and privatekey_fmt_2_step_1 is changed + - privatekey_fmt_2_step_2 is succeeded and privatekey_fmt_2_step_2 is not changed + - privatekey_fmt_2_step_3 is succeeded and privatekey_fmt_2_step_3 is changed + - privatekey_fmt_2_step_4 is succeeded and privatekey_fmt_2_step_4 is not changed + - privatekey_fmt_2_step_5 is succeeded and privatekey_fmt_2_step_5 is not changed + - privatekey_fmt_2_step_6 is succeeded and privatekey_fmt_2_step_6 is changed + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=") and privatekey_fmt_2_step_1 is not failed' diff --git a/tests/integration/targets/openssl_privatekey/vars/main.yml b/tests/integration/targets/openssl_privatekey/vars/main.yml new file mode 100644 index 00000000..81eb611f --- /dev/null +++ b/tests/integration/targets/openssl_privatekey/vars/main.yml @@ -0,0 +1,7 @@ +--- +regenerate_values: + - never + - fail + - partial_idempotence + - full_idempotence + - always diff --git a/tests/integration/targets/openssl_privatekey_info/aliases b/tests/integration/targets/openssl_privatekey_info/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_info/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_privatekey_info/meta/main.yml b/tests/integration/targets/openssl_privatekey_info/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_privatekey_info/tasks/impl.yml b/tests/integration/targets/openssl_privatekey_info/tasks/impl.yml new file mode 100644 index 00000000..c74acdec --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_info/tasks/impl.yml @@ -0,0 +1,178 @@ +--- +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" + +- name: ({{select_crypto_backend}}) Get key 1 info + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' not in result" + +- name: Update result list + set_fact: + info_results: "{{ info_results | combine({'key1': result}) }}" + +- name: ({{select_crypto_backend}}) Get key 1 info directly + openssl_privatekey_info: + content: '{{ lookup("file", output_dir ~ "/privatekey_1.pem") }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_direct + +- name: ({{select_crypto_backend}}) Compare output of direct and loaded info + assert: + that: + - result == result_direct + +- name: ({{select_crypto_backend}}) Get key 2 info + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_2.pem' + return_private_key_data: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "result.public_data.size == 2048" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: Update result list + set_fact: + info_results: "{{ info_results | combine({'key2': result}) }}" + +- name: ({{select_crypto_backend}}) Get key 3 info (without passphrase) + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_3.pem' + return_private_key_data: yes + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: result + +- name: Check that loading passphrase protected key without passphrase failed + assert: + that: + - result is failed + # Check that return values are there + - result.can_load_key is defined + - result.can_parse_key is defined + # Check that return values are correct + - result.can_load_key + - not result.can_parse_key + # Check that additional data isn't there + - "'pulic_key' not in result" + - "'pulic_key_fingerprints' not in result" + - "'type' not in result" + - "'public_data' not in result" + - "'private_data' not in result" + +- name: ({{select_crypto_backend}}) Get key 3 info (with passphrase) + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_3.pem' + passphrase: hunter2 + return_private_key_data: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: Update result list + set_fact: + info_results: "{{ info_results | combine({'key3': result}) }}" + +- name: ({{select_crypto_backend}}) Get key 4 info + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_4.pem' + return_private_key_data: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- block: + - name: Check that ECC key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'ECC'" + - "'public_data' in result" + - "result.public_data.curve is string" + - "result.public_data.x != 0" + - "result.public_data.y != 0" + - "result.public_data.exponent_size == (521 if (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') else 256)" + - "'private_data' in result" + - "result.private_data.multiplier > 1024" + + - name: Update result list + set_fact: + info_results: "{{ info_results | combine({'key4': result}) }}" + when: select_crypto_backend != 'pyopenssl' or (pyopenssl_version.stdout is version('16.1.0', '>=') and cryptography_version.stdout is version('0.0', '>')) + +- name: Check that ECC key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type.startswith('unknown ')" + - "'public_data' in result" + - "'private_data' in result" + when: select_crypto_backend == 'pyopenssl' and not (pyopenssl_version.stdout is version('16.1.0', '>=') and cryptography_version.stdout is version('0.0', '>')) + +- name: ({{select_crypto_backend}}) Get key 5 info + openssl_privatekey_info: + path: '{{ output_dir }}/privatekey_5.pem' + return_private_key_data: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that DSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'DSA'" + - "'public_data' in result" + - "result.public_data.p > 2" + - "result.public_data.q > 2" + - "result.public_data.g >= 2" + - "result.public_data.y > 2" + - "'private_data' in result" + - "result.private_data.x > 2" + +- name: Update result list + set_fact: + info_results: "{{ info_results | combine({'key5': result}) }}" diff --git a/tests/integration/targets/openssl_privatekey_info/tasks/main.yml b/tests/integration/targets/openssl_privatekey_info/tasks/main.yml new file mode 100644 index 00000000..167b88fa --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_info/tasks/main.yml @@ -0,0 +1,71 @@ +--- +- name: Generate privatekey 1 + openssl_privatekey: + path: '{{ output_dir }}/privatekey_1.pem' + +- name: Generate privatekey 2 (less bits) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_2.pem' + type: RSA + size: 2048 + +- name: Generate privatekey 3 (with password) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_3.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: Generate privatekey 4 (ECC) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_4.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + select_crypto_backend: cryptography + +- name: Generate privatekey 5 (DSA) + openssl_privatekey: + path: '{{ output_dir }}/privatekey_5.pem' + type: DSA + size: 1024 + +- name: Prepare result list + set_fact: + info_results: {} + +- name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + when: pyopenssl_version.stdout is version('0.15', '>=') + +- name: Prepare result list + set_fact: + pyopenssl_info_results: "{{ info_results }}" + info_results: {} + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.2.3', '>=') + +- name: Prepare result list + set_fact: + cryptography_info_results: "{{ info_results }}" + +- block: + - name: Dump pyOpenSSL results + debug: + var: pyopenssl_info_results + - name: Dump cryptography results + debug: + var: cryptography_info_results + - name: Compare results + assert: + that: + - ' (pyopenssl_info_results[item] | dict2items | rejectattr("key", "equalto", "deprecations") | list | items2dict) + == (cryptography_info_results[item] | dict2items | rejectattr("key", "equalto", "deprecations") | list | items2dict)' + loop: "{{ pyopenssl_info_results.keys() | intersect(cryptography_info_results.keys()) | list }}" + when: pyopenssl_version.stdout is version('0.15', '>=') and cryptography_version.stdout is version('1.2.3', '>=') diff --git a/tests/integration/targets/openssl_publickey/aliases b/tests/integration/targets/openssl_publickey/aliases new file mode 100644 index 00000000..0b484bba --- /dev/null +++ b/tests/integration/targets/openssl_publickey/aliases @@ -0,0 +1,3 @@ +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/openssl_publickey/meta/main.yml b/tests/integration/targets/openssl_publickey/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/openssl_publickey/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_publickey/tasks/impl.yml b/tests/integration/targets/openssl_publickey/tasks/impl.yml new file mode 100644 index 00000000..738013ff --- /dev/null +++ b/tests/integration/targets/openssl_publickey/tasks/impl.yml @@ -0,0 +1,186 @@ +--- +- name: Generate privatekey + openssl_privatekey: + path: '{{ output_dir }}/privatekey.pem' + +- name: Generate publickey - PEM format + openssl_publickey: + path: '{{ output_dir }}/publickey.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: publickey + +- name: Generate publickey - PEM format (idempotence) + openssl_publickey: + path: '{{ output_dir }}/publickey.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: publickey_idempotence + +- name: Generate publickey - OpenSSH format + openssl_publickey: + path: '{{ output_dir }}/publickey-ssh.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + format: OpenSSH + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: Generate publickey - OpenSSH format - test idempotence (issue 33256) + openssl_publickey: + path: '{{ output_dir }}/publickey-ssh.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + format: OpenSSH + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + register: publickey_ssh_idempotence + +- name: Generate publickey2 - standard + openssl_publickey: + path: '{{ output_dir }}/publickey2.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Delete publickey2 - standard + openssl_publickey: + state: absent + path: '{{ output_dir }}/publickey2.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: publickey2_absent + +- name: Delete publickey2 - standard (idempotence) + openssl_publickey: + state: absent + path: '{{ output_dir }}/publickey2.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: publickey2_absent_idempotence + +- name: Generate privatekey3 - with passphrase + openssl_privatekey: + path: '{{ output_dir }}/privatekey3.pem' + passphrase: ansible + cipher: aes256 + +- name: Generate publickey3 - with passphrase protected privatekey + openssl_publickey: + path: '{{ output_dir }}/publickey3.pub' + privatekey_path: '{{ output_dir }}/privatekey3.pem' + privatekey_passphrase: ansible + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate publickey3 - with passphrase protected privatekey - idempotence + openssl_publickey: + path: '{{ output_dir }}/publickey3.pub' + privatekey_path: '{{ output_dir }}/privatekey3.pem' + privatekey_passphrase: ansible + select_crypto_backend: '{{ select_crypto_backend }}' + register: publickey3_idempotence + +- name: Generate empty file that will hold a public key (issue 33072) + file: + path: '{{ output_dir }}/publickey4.pub' + state: touch + +- name: Generate publickey in empty existing file (issue 33072) + openssl_publickey: + path: '{{ output_dir }}/publickey4.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: Generate privatekey 5 (ECC) + openssl_privatekey: + path: '{{ output_dir }}/privatekey5.pem' + type: ECC + curve: secp256r1 + +- name: Generate publickey 5 - PEM format + openssl_publickey: + path: '{{ output_dir }}/publickey5.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_1 +- name: Generate publickey 5 - PEM format (idempotent) + openssl_publickey: + path: '{{ output_dir }}/publickey5.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_2 +- name: Generate publickey 5 - PEM format (different private key) + openssl_publickey: + path: '{{ output_dir }}/publickey5.pub' + privatekey_path: '{{ output_dir }}/privatekey5.pem' + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_3 + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + +- name: Generate publickey - PEM format (failed passphrase 1) + openssl_publickey: + path: '{{ output_dir }}/publickey_pw1.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + privatekey_passphrase: hunter2 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_1 + +- name: Generate publickey - PEM format (failed passphrase 2) + openssl_publickey: + path: '{{ output_dir }}/publickey_pw2.pub' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_2 + +- name: Generate publickey - PEM format (failed passphrase 3) + openssl_publickey: + path: '{{ output_dir }}/publickey_pw3.pub' + privatekey_path: '{{ output_dir }}/privatekeypw.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: yes + register: passphrase_error_3 + +- name: Create broken key + copy: + dest: "{{ output_dir }}/publickeybroken.pub" + content: "broken" +- name: Regenerate broken key + openssl_publickey: + path: '{{ output_dir }}/publickeybroken.pub' + privatekey_path: '{{ output_dir }}/privatekey5.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: output_broken + +- name: Generate publickey - PEM format (for removal) + openssl_publickey: + path: '{{ output_dir }}/publickey_removal.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' +- name: Generate publickey - PEM format (removal) + openssl_publickey: + state: absent + path: '{{ output_dir }}/publickey_removal.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: remove_1 +- name: Generate publickey - PEM format (removal, idempotent) + openssl_publickey: + state: absent + path: '{{ output_dir }}/publickey_removal.pub' + privatekey_path: '{{ output_dir }}/privatekey.pem' + backup: yes + select_crypto_backend: '{{ select_crypto_backend }}' + register: remove_2 diff --git a/tests/integration/targets/openssl_publickey/tasks/main.yml b/tests/integration/targets/openssl_publickey/tasks/main.yml new file mode 100644 index 00000000..b7369c03 --- /dev/null +++ b/tests/integration/targets/openssl_publickey/tasks/main.yml @@ -0,0 +1,48 @@ +--- +- block: + - name: Generate privatekey1 - standard + openssl_privatekey: + path: '{{ output_dir }}/privatekey_autodetect.pem' + + - name: Run module with backend autodetection + openssl_publickey: + path: '{{ output_dir }}/privatekey_autodetect_public.pem' + privatekey_path: '{{ output_dir }}/privatekey_autodetect.pem' + + when: | + pyopenssl_version.stdout is version('16.0.0', '>=') or + cryptography_version.stdout is version('1.2.3', '>=') + +- block: + - name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: pyopenssl + + when: pyopenssl_version.stdout is version('16.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.2.3', '>=') diff --git a/tests/integration/targets/openssl_publickey/tests/validate.yml b/tests/integration/targets/openssl_publickey/tests/validate.yml new file mode 100644 index 00000000..aaf9c100 --- /dev/null +++ b/tests/integration/targets/openssl_publickey/tests/validate.yml @@ -0,0 +1,146 @@ +--- +- name: Validate publickey 1 idempotence and result behavior + assert: + that: + - publickey is changed + - publickey_idempotence is not changed + - publickey.publickey == lookup('file', output_dir ~ '/publickey.pub', rstrip=False) + - publickey.publickey == publickey_idempotence.publickey + +- name: Validate public key (test - privatekey modulus) + shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: Validate public key (test - publickey modulus) + shell: 'openssl rsa -pubin -noout -modulus < {{ output_dir }}/publickey.pub' + register: publickey_modulus + +- name: Validate public key (assert) + assert: + that: + - publickey_modulus.stdout == privatekey_modulus.stdout + +- name: Validate public key - OpenSSH format (test - privatekey's publickey) + shell: 'ssh-keygen -y -f {{ output_dir }}/privatekey.pem' + register: privatekey_publickey + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: Validate public key - OpenSSH format (test - publickey) + slurp: + src: '{{ output_dir }}/publickey-ssh.pub' + register: publickey + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: Validate public key - OpenSSH format (assert) + assert: + that: + - privatekey_publickey.stdout == '{{ publickey.content|b64decode }}' + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: Validate public key - OpenSSH format - test idempotence (issue 33256) + assert: + that: + - publickey_ssh_idempotence is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: Validate publickey2 (test - Ensure key has been removed) + stat: + path: '{{ output_dir }}/publickey2.pub' + register: publickey2 + +- name: Validate publickey2 (assert - Ensure key has been removed) + assert: + that: + - publickey2.stat.exists == False + +- name: Validate publickey2 removal behavior + assert: + that: + - publickey2_absent is changed + - publickey2_absent_idempotence is not changed + - publickey2_absent.publickey is none + + +- name: Validate publickey3 (test - privatekey modulus) + shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey3.pem -passin pass:ansible' + register: privatekey3_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate publickey3 (test - publickey modulus) + shell: 'openssl rsa -pubin -noout -modulus < {{ output_dir }}/publickey3.pub' + register: publickey3_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate publickey3 (assert) + assert: + that: + - publickey3_modulus.stdout == privatekey3_modulus.stdout + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate publickey3 idempotence (assert) + assert: + that: + - publickey3_idempotence is not changed + +- name: Validate publickey4 (test - privatekey modulus) + shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey.pem' + register: privatekey4_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate publickey4 (test - publickey modulus) + shell: 'openssl rsa -pubin -noout -modulus < {{ output_dir }}/publickey4.pub' + register: publickey4_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate publickey4 (assert) + assert: + that: + - publickey4_modulus.stdout == privatekey4_modulus.stdout + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: Validate idempotency and backup + assert: + that: + - privatekey5_1 is changed + - privatekey5_1.backup_file is undefined + - privatekey5_2 is not changed + - privatekey5_2.backup_file is undefined + - privatekey5_3 is changed + - privatekey5_3.backup_file is string + +- name: Validate public key 5 (test - privatekey's pubkey) + command: 'openssl ec -in {{ output_dir }}/privatekey5.pem -pubout' + register: privatekey5_pubkey + +- name: Validate public key 5 (test - publickey pubkey) + # Fancy way of writing "cat {{ output_dir }}/publickey5.pub" + command: 'openssl ec -pubin -in {{ output_dir }}/publickey5.pub -pubout' + register: publickey5_pubkey + +- name: Validate public key 5 (assert) + assert: + that: + - publickey5_pubkey.stdout == privatekey5_pubkey.stdout + +- name: + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: Verify that broken key will be regenerated + assert: + that: + - output_broken is changed + +- name: Validate remove + assert: + that: + - remove_1 is changed + - remove_2 is not changed + - remove_1.backup_file is string + - remove_2.backup_file is undefined diff --git a/tests/integration/targets/prepare_http_tests/defaults/main.yml b/tests/integration/targets/prepare_http_tests/defaults/main.yml new file mode 100644 index 00000000..a1e5b8d1 --- /dev/null +++ b/tests/integration/targets/prepare_http_tests/defaults/main.yml @@ -0,0 +1,4 @@ +badssl_host: wrong.host.badssl.com +httpbin_host: httpbin.org +sni_host: ci-files.testing.ansible.com +badssl_host_substring: wrong.host.badssl.com diff --git a/tests/integration/targets/prepare_http_tests/meta/main.yml b/tests/integration/targets/prepare_http_tests/meta/main.yml new file mode 100644 index 00000000..1810d4be --- /dev/null +++ b/tests/integration/targets/prepare_http_tests/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_remote_tmp_dir diff --git a/tests/integration/targets/prepare_http_tests/tasks/default.yml b/tests/integration/targets/prepare_http_tests/tasks/default.yml new file mode 100644 index 00000000..5f098e72 --- /dev/null +++ b/tests/integration/targets/prepare_http_tests/tasks/default.yml @@ -0,0 +1,59 @@ +- name: RedHat - Enable the dynamic CA configuration feature + command: update-ca-trust force-enable + when: ansible_os_family == 'RedHat' + +- name: RedHat - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/etc/pki/ca-trust/source/anchors/ansible.pem" + when: ansible_os_family == 'RedHat' + +- name: Get client cert/key + get_url: + url: "http://ansible.http.tests/{{ item }}" + dest: "{{ remote_tmp_dir }}/{{ item }}" + with_items: + - client.pem + - client.key + +- name: Suse - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/etc/pki/trust/anchors/ansible.pem" + when: ansible_os_family == 'Suse' + +- name: Debian - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/usr/local/share/ca-certificates/ansible.crt" + when: ansible_os_family == 'Debian' + +- name: Redhat - Update ca trust + command: update-ca-trust extract + when: ansible_os_family == 'RedHat' + +- name: Debian/Suse - Update ca certificates + command: update-ca-certificates + when: ansible_os_family == 'Debian' or ansible_os_family == 'Suse' + +- name: FreeBSD - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/tmp/ansible.pem" + when: ansible_os_family == 'FreeBSD' + +- name: FreeBSD - Add cacert to root certificate store + blockinfile: + path: "/etc/ssl/cert.pem" + block: "{{ lookup('file', '/tmp/ansible.pem') }}" + when: ansible_os_family == 'FreeBSD' + +- name: MacOS - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/usr/local/etc/openssl/certs/ansible.pem" + when: ansible_os_family == 'Darwin' + +- name: MacOS - Update ca certificates + command: /usr/local/opt/openssl/bin/c_rehash + when: ansible_os_family == 'Darwin' diff --git a/tests/integration/targets/prepare_http_tests/tasks/main.yml b/tests/integration/targets/prepare_http_tests/tasks/main.yml new file mode 100644 index 00000000..86e350c2 --- /dev/null +++ b/tests/integration/targets/prepare_http_tests/tasks/main.yml @@ -0,0 +1,24 @@ +# The docker --link functionality gives us an ENV var we can key off of to see if we have access to +# the httptester container +- set_fact: + has_httptester: "{{ lookup('env', 'HTTPTESTER') != '' }}" + +- name: make sure we have the ansible_os_family and ansible_distribution_version facts + setup: + gather_subset: distribution + when: ansible_facts == {} + +# If we are running with access to a httptester container, grab it's cacert and install it +- block: + # Override hostname defaults with httptester linked names + - include_vars: httptester.yml + + - include_tasks: "{{ lookup('first_found', files)}}" + vars: + files: + - "{{ ansible_os_family | lower }}.yml" + - "default.yml" + when: + - has_httptester|bool + # skip the setup if running on Windows Server 2008 as httptester is not available + - ansible_os_family != 'Windows' or (ansible_os_family == 'Windows' and not ansible_distribution_version.startswith("6.0.")) diff --git a/tests/integration/targets/prepare_http_tests/tasks/windows.yml b/tests/integration/targets/prepare_http_tests/tasks/windows.yml new file mode 100644 index 00000000..d2a0fb31 --- /dev/null +++ b/tests/integration/targets/prepare_http_tests/tasks/windows.yml @@ -0,0 +1,26 @@ +- name: Windows - make sure the port forwarder is active + ansible.windows.win_wait_for: + host: ansible.http.tests + port: 80 + state: started + timeout: 300 +- name: Windows - Get client cert/key + register: win_download + retries: 5 + until: win_download is successful + with_items: + - client.pem + - client.key + ansible.windows.win_get_url: + url: http://ansible.http.tests/{{ item }} + dest: '{{ remote_tmp_dir }}\{{ item }}' +- name: Windows - Retrieve test cacert + ansible.windows.win_get_url: + url: http://ansible.http.tests/cacert.pem + dest: '{{ remote_tmp_dir }}\cacert.pem' +- name: Windows - Update ca trust + ansible.windows.win_certificate_store: + path: '{{ remote_tmp_dir }}\cacert.pem' + state: present + store_location: LocalMachine + store_name: Root diff --git a/tests/integration/targets/prepare_http_tests/vars/httptester.yml b/tests/integration/targets/prepare_http_tests/vars/httptester.yml new file mode 100644 index 00000000..0e23ae93 --- /dev/null +++ b/tests/integration/targets/prepare_http_tests/vars/httptester.yml @@ -0,0 +1,5 @@ +# these are fake hostnames provided by docker link for the httptester container +badssl_host: fail.ansible.http.tests +httpbin_host: ansible.http.tests +sni_host: sni1.ansible.http.tests +badssl_host_substring: HTTP Client Testing Service diff --git a/tests/integration/targets/prepare_tests/tasks/main.yml b/tests/integration/targets/prepare_tests/tasks/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/targets/setup_acme/tasks/main.yml b/tests/integration/targets/setup_acme/tasks/main.yml new file mode 100644 index 00000000..6f4f2ec0 --- /dev/null +++ b/tests/integration/targets/setup_acme/tasks/main.yml @@ -0,0 +1,10 @@ +--- +- name: register openssl version + shell: "openssl version | cut -d' ' -f2" + register: openssl_version + +- name: register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: cryptography_version + +- debug: msg="ACME test container IP is {{ acme_host }}; OpenSSL version is {{ openssl_version.stdout }}; cryptography version is {{ cryptography_version.stdout }}" diff --git a/tests/integration/targets/setup_acme/tasks/obtain-cert.yml b/tests/integration/targets/setup_acme/tasks/obtain-cert.yml new file mode 100644 index 00000000..98f5f804 --- /dev/null +++ b/tests/integration/targets/setup_acme/tasks/obtain-cert.yml @@ -0,0 +1,140 @@ +--- +## PRIVATE KEY ################################################################################ +- name: ({{ certgen_title }}) Create cert private key (RSA) + command: "openssl genrsa -out {{ output_dir }}/{{ certificate_name }}.key {{ rsa_bits if key_type == 'rsa' else 2048 }}" + when: "key_type == 'rsa'" +- name: ({{ certgen_title }}) Create cert private key (ECC 256) + command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/{{ certificate_name }}.key + when: "key_type == 'ec256'" +- name: ({{ certgen_title }}) Create cert private key (ECC 384) + command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/{{ certificate_name }}.key + when: "key_type == 'ec384'" +- name: ({{ certgen_title }}) Create cert private key (ECC 512) + command: openssl ecparam -name secp521r1 -genkey -out {{ output_dir }}/{{ certificate_name }}.key + when: "key_type == 'ec521'" +## CSR ######################################################################################## +- name: ({{ certgen_title }}) Create cert CSR + openssl_csr: + path: "{{ output_dir }}/{{ certificate_name }}.csr" + privatekey_path: "{{ output_dir }}/{{ certificate_name }}.key" + subject_alt_name: "{{ subject_alt_name }}" + subject_alt_name_critical: "{{ subject_alt_name_critical }}" +## ACME STEP 1 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 1 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_key: "{{ (output_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + modify_account: "{{ modify_account }}" + csr: "{{ output_dir }}/{{ certificate_name }}.csr" + dest: "{{ output_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ output_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ output_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + register: challenge_data +- name: ({{ certgen_title }}) Print challenge data + debug: + var: challenge_data +- name: ({{ certgen_title }}) Create HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: PUT + body_format: raw + body: "{{ item.value['http-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Create DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: PUT + body_format: json + body: "{{ item.value }}" + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (acm_challenge_cert_helper) + acme_challenge_cert_helper: + challenge: tls-alpn-01 + challenge_data: "{{ item.value['tls-alpn-01'] }}" + private_key_src: "{{ output_dir }}/{{ certificate_name }}.key" + with_dict: "{{ challenge_data.challenge_data }}" + register: tls_alpn_challenges + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is defined and challenge_alpn_tls == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Set TLS ALPN challenges (acm_challenge_cert_helper) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key" + method: PUT + body_format: raw + body: "{{ item.challenge_certificate }}\n{{ lookup('file', output_dir ~ '/' ~ certificate_name ~ '.key') }}" + headers: + content-type: "application/pem-certificate-chain" + with_items: "{{ tls_alpn_challenges.results }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is defined and challenge_alpn_tls == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64" + method: PUT + body_format: raw + body: "{{ item.value['tls-alpn-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is not defined or challenge_alpn_tls == 'der-value-b64')" +## ACME STEP 2 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 2 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + account_key: "{{ (output_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_uri: "{{ challenge_data.account_uri }}" + modify_account: "{{ modify_account }}" + csr: "{{ output_dir }}/{{ certificate_name }}.csr" + dest: "{{ output_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ output_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ output_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + data: "{{ challenge_data }}" + retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}" + select_chain: "{{ select_chain | default(omit) if select_crypto_backend == 'cryptography' else omit }}" + register: certificate_obtain_result + when: challenge_data is changed +- name: ({{ certgen_title }}) Deleting HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Deleting DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Deleting TLS ALPN challenges + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01'" +- name: ({{ certgen_title }}) Get root certificate + get_url: + url: "http://{{ acme_host }}:5000/root-certificate-for-ca/{{ acme_expected_root_number | default(0) if select_crypto_backend == 'cryptography' else 0 }}" + dest: "{{ output_dir }}/{{ certificate_name }}-root.pem" +############################################################################################### diff --git a/tests/integration/targets/setup_openssl/tasks/main.yml b/tests/integration/targets/setup_openssl/tasks/main.yml new file mode 100644 index 00000000..5a634458 --- /dev/null +++ b/tests/integration/targets/setup_openssl/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Include OS-specific variables + include_vars: '{{ ansible_os_family }}.yml' + when: not ansible_os_family == "Darwin" + +- name: Install OpenSSL + become: True + package: + name: '{{ openssl_package_name }}' + when: not ansible_os_family == 'Darwin' + +- name: Install pyOpenSSL (Python 3) + become: True + package: + name: '{{ pyopenssl_package_name_python3 }}' + when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '>=') + +- name: Install pyOpenSSL (Python 2) + become: True + package: + name: '{{ pyopenssl_package_name }}' + when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '<') + +- name: Install pyOpenSSL (Darwin) + become: True + pip: + name: pyOpenSSL + when: ansible_os_family == 'Darwin' + +- name: register pyOpenSSL version + command: "{{ ansible_python.executable }} -c 'import OpenSSL; print(OpenSSL.__version__)'" + register: pyopenssl_version + +- name: register openssl version + shell: "openssl version | cut -d' ' -f2" + register: openssl_version + +- name: register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: cryptography_version diff --git a/tests/integration/targets/setup_openssl/vars/Debian.yml b/tests/integration/targets/setup_openssl/vars/Debian.yml new file mode 100644 index 00000000..755c7a08 --- /dev/null +++ b/tests/integration/targets/setup_openssl/vars/Debian.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: python-openssl +pyopenssl_package_name_python3: python3-openssl +openssl_package_name: openssl diff --git a/tests/integration/targets/setup_openssl/vars/FreeBSD.yml b/tests/integration/targets/setup_openssl/vars/FreeBSD.yml new file mode 100644 index 00000000..60868915 --- /dev/null +++ b/tests/integration/targets/setup_openssl/vars/FreeBSD.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: py27-openssl +pyopenssl_package_name_python3: py36-openssl +openssl_package_name: openssl diff --git a/tests/integration/targets/setup_openssl/vars/RedHat.yml b/tests/integration/targets/setup_openssl/vars/RedHat.yml new file mode 100644 index 00000000..2959932c --- /dev/null +++ b/tests/integration/targets/setup_openssl/vars/RedHat.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: pyOpenSSL +pyopenssl_package_name_python3: python3-pyOpenSSL +openssl_package_name: openssl diff --git a/tests/integration/targets/setup_openssl/vars/Suse.yml b/tests/integration/targets/setup_openssl/vars/Suse.yml new file mode 100644 index 00000000..2d5200f3 --- /dev/null +++ b/tests/integration/targets/setup_openssl/vars/Suse.yml @@ -0,0 +1,3 @@ +pyopenssl_package_name: python-pyOpenSSL +pyopenssl_package_name_python3: python3-pyOpenSSL +openssl_package_name: openssl diff --git a/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml b/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml new file mode 100644 index 00000000..229037c8 --- /dev/null +++ b/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml @@ -0,0 +1,5 @@ +- name: delete temporary directory + include_tasks: default-cleanup.yml + +- name: delete temporary directory (windows) + include_tasks: windows-cleanup.yml diff --git a/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml b/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml new file mode 100644 index 00000000..39872d74 --- /dev/null +++ b/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml @@ -0,0 +1,5 @@ +- name: delete temporary directory + file: + path: "{{ remote_tmp_dir }}" + state: absent + no_log: yes diff --git a/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml b/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml new file mode 100644 index 00000000..1e0f51b8 --- /dev/null +++ b/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml @@ -0,0 +1,11 @@ +- name: create temporary directory + tempfile: + state: directory + suffix: .test + register: remote_tmp_dir + notify: + - delete temporary directory + +- name: record temporary directory + set_fact: + remote_tmp_dir: "{{ remote_tmp_dir.path }}" diff --git a/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml b/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml new file mode 100644 index 00000000..f8df391b --- /dev/null +++ b/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml @@ -0,0 +1,10 @@ +- name: make sure we have the ansible_os_family and ansible_distribution_version facts + setup: + gather_subset: distribution + when: ansible_facts == {} + +- include_tasks: "{{ lookup('first_found', files)}}" + vars: + files: + - "{{ ansible_os_family | lower }}.yml" + - "default.yml" diff --git a/tests/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml b/tests/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml new file mode 100644 index 00000000..32f372d0 --- /dev/null +++ b/tests/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml @@ -0,0 +1,4 @@ +- name: delete temporary directory (windows) + ansible.windows.win_file: + path: '{{ remote_tmp_dir }}' + state: absent diff --git a/tests/integration/targets/setup_remote_tmp_dir/tasks/windows.yml b/tests/integration/targets/setup_remote_tmp_dir/tasks/windows.yml new file mode 100644 index 00000000..317c146d --- /dev/null +++ b/tests/integration/targets/setup_remote_tmp_dir/tasks/windows.yml @@ -0,0 +1,10 @@ +- name: create temporary directory + register: remote_tmp_dir + notify: + - delete temporary directory (windows) + ansible.windows.win_tempfile: + state: directory + suffix: .test +- name: record temporary directory + set_fact: + remote_tmp_dir: '{{ remote_tmp_dir.path }}' diff --git a/tests/integration/targets/setup_ssh_keygen/tasks/main.yml b/tests/integration/targets/setup_ssh_keygen/tasks/main.yml new file mode 100644 index 00000000..885356d7 --- /dev/null +++ b/tests/integration/targets/setup_ssh_keygen/tasks/main.yml @@ -0,0 +1,8 @@ +- name: Include OS-specific variables + include_vars: '{{ ansible_os_family }}.yml' + when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD" + +- name: Install ssh-keygen + package: + name: '{{ openssh_client_package_name }}' + when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD" diff --git a/tests/integration/targets/setup_ssh_keygen/vars/Debian.yml b/tests/integration/targets/setup_ssh_keygen/vars/Debian.yml new file mode 100644 index 00000000..d7ff0c73 --- /dev/null +++ b/tests/integration/targets/setup_ssh_keygen/vars/Debian.yml @@ -0,0 +1 @@ +openssh_client_package_name: openssh-client diff --git a/tests/integration/targets/setup_ssh_keygen/vars/RedHat.yml b/tests/integration/targets/setup_ssh_keygen/vars/RedHat.yml new file mode 100644 index 00000000..bc656edf --- /dev/null +++ b/tests/integration/targets/setup_ssh_keygen/vars/RedHat.yml @@ -0,0 +1 @@ +openssh_client_package_name: openssh-clients diff --git a/tests/integration/targets/setup_ssh_keygen/vars/Suse.yml b/tests/integration/targets/setup_ssh_keygen/vars/Suse.yml new file mode 100644 index 00000000..4091fa7b --- /dev/null +++ b/tests/integration/targets/setup_ssh_keygen/vars/Suse.yml @@ -0,0 +1 @@ +openssh_client_package_name: openssh diff --git a/tests/integration/targets/x509_crl/aliases b/tests/integration/targets/x509_crl/aliases new file mode 100644 index 00000000..d225a8b1 --- /dev/null +++ b/tests/integration/targets/x509_crl/aliases @@ -0,0 +1,4 @@ +x509_crl_info +shippable/posix/group1 +destructive +skip/aix diff --git a/tests/integration/targets/x509_crl/meta/main.yml b/tests/integration/targets/x509_crl/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/x509_crl/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/x509_crl/tasks/impl.yml b/tests/integration/targets/x509_crl/tasks/impl.yml new file mode 100644 index 00000000..eafb2dad --- /dev/null +++ b/tests/integration/targets/x509_crl/tasks/impl.yml @@ -0,0 +1,289 @@ +--- +- name: Create CRL 1 (check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_check +- name: Create CRL 1 + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1 +- name: Retrieve CRL 1 infos + x509_crl_info: + path: '{{ output_dir }}/ca-crl1.crl' + register: crl_1_info_1 +- name: Retrieve CRL 1 infos via file content + x509_crl_info: + content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") }}' + register: crl_1_info_2 +- name: Create CRL 1 (idempotent, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_idem_check +- name: Create CRL 1 (idempotent) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_idem +- name: Create CRL 1 (idempotent with content, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_content: "{{ lookup('file', output_dir ~ '/ca.key') }}" + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - content: "{{ lookup('file', output_dir ~ '/cert-1.pem') }}" + revocation_date: 20191013000000Z + - content: "{{ lookup('file', output_dir ~ '/cert-2.pem') }}" + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_idem_content_check +- name: Create CRL 1 (idempotent with content) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_content: "{{ lookup('file', output_dir ~ '/ca.key') }}" + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - content: "{{ lookup('file', output_dir ~ '/cert-1.pem') }}" + revocation_date: 20191013000000Z + - content: "{{ lookup('file', output_dir ~ '/cert-2.pem') }}" + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_idem_content + +- name: Create CRL 2 (check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + check_mode: yes + register: crl_2_check +- name: Create CRL 2 + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + register: crl_2 +- name: Create CRL 2 (idempotent, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + ignore_timestamps: yes + check_mode: yes + register: crl_2_idem_check +- name: Create CRL 2 (idempotent) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + ignore_timestamps: yes + register: crl_2_idem +- name: Create CRL 2 (idempotent update, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1235 + ignore_timestamps: yes + mode: update + check_mode: yes + register: crl_2_idem_update_change_check +- name: Create CRL 2 (idempotent update) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1235 + ignore_timestamps: yes + mode: update + register: crl_2_idem_update_change +- name: Create CRL 2 (idempotent update, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: yes + mode: update + check_mode: yes + register: crl_2_idem_update_check +- name: Create CRL 2 (idempotent update) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: yes + mode: update + register: crl_2_idem_update +- name: Create CRL 2 (changed timestamps, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: no + mode: update + check_mode: yes + register: crl_2_change_check +- name: Create CRL 2 (changed timestamps) + x509_crl: + path: '{{ output_dir }}/ca-crl2.crl' + privatekey_path: '{{ output_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ output_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + ignore_timestamps: no + mode: update + return_content: yes + register: crl_2_change diff --git a/tests/integration/targets/x509_crl/tasks/main.yml b/tests/integration/targets/x509_crl/tasks/main.yml new file mode 100644 index 00000000..1f82ff9e --- /dev/null +++ b/tests/integration/targets/x509_crl/tasks/main.yml @@ -0,0 +1,83 @@ +--- +- set_fact: + certificates: + - name: ca + subject: + commonName: Ansible + is_ca: yes + - name: ca-2 + subject: + commonName: Ansible Other CA + is_ca: yes + - name: cert-1 + subject_alt_name: + - DNS:ansible.com + - name: cert-2 + subject_alt_name: + - DNS:example.com + - name: cert-3 + subject_alt_name: + - DNS:example.org + - IP:1.2.3.4 + - name: cert-4 + subject_alt_name: + - DNS:test.ansible.com + - DNS:b64.ansible.com + +- name: Generate private keys + openssl_privatekey: + path: '{{ output_dir }}/{{ item.name }}.key' + type: ECC + curve: secp256r1 + loop: "{{ certificates }}" + +- name: Generate CSRs + openssl_csr: + path: '{{ output_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ output_dir }}/{{ item.name }}.key' + subject: "{{ item.subject | default(omit) }}" + subject_alt_name: "{{ item.subject_alt_name | default(omit) }}" + basic_constraints: "{{ 'CA:TRUE' if item.is_ca | default(false) else omit }}" + use_common_name_for_san: no + loop: "{{ certificates }}" + +- name: Generate CA certificates + openssl_certificate: + path: '{{ output_dir }}/{{ item.name }}.pem' + csr_path: '{{ output_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ output_dir }}/{{ item.name }}.key' + provider: selfsigned + loop: "{{ certificates }}" + when: item.is_ca | default(false) + +- name: Generate other certificates + openssl_certificate: + path: '{{ output_dir }}/{{ item.name }}.pem' + csr_path: '{{ output_dir }}/{{ item.name }}.csr' + provider: ownca + ownca_path: '{{ output_dir }}/ca.pem' + ownca_privatekey_path: '{{ output_dir }}/ca.key' + loop: "{{ certificates }}" + when: not (item.is_ca | default(false)) + +- name: Get certificate infos + openssl_certificate_info: + path: '{{ output_dir }}/{{ item }}.pem' + loop: + - cert-1 + - cert-2 + - cert-3 + - cert-4 + register: certificate_infos + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.2', '>=') diff --git a/tests/integration/targets/x509_crl/tests/validate.yml b/tests/integration/targets/x509_crl/tests/validate.yml new file mode 100644 index 00000000..17b31f34 --- /dev/null +++ b/tests/integration/targets/x509_crl/tests/validate.yml @@ -0,0 +1,61 @@ +--- +- name: Validate CRL 1 + assert: + that: + - crl_1_check is changed + - crl_1 is changed + - crl_1_idem_check is not changed + - crl_1_idem is not changed + - crl_1_idem_content_check is not changed + - crl_1_idem_content is not changed + +- name: Validate CRL 1 info + assert: + that: + - crl_1_info_1 == crl_1_info_2 + - crl_1_info_1.digest == 'ecdsa-with-SHA256' + - crl_1_info_1.issuer | length == 1 + - crl_1_info_1.issuer.commonName == 'Ansible' + - crl_1_info_1.issuer_ordered | length == 1 + - crl_1_info_1.last_update == '20191013000000Z' + - crl_1_info_1.next_update == '20191113000000Z' + - crl_1_info_1.revoked_certificates | length == 3 + - crl_1_info_1.revoked_certificates[0].invalidity_date is none + - crl_1_info_1.revoked_certificates[0].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[0].issuer is none + - crl_1_info_1.revoked_certificates[0].issuer_critical == false + - crl_1_info_1.revoked_certificates[0].reason is none + - crl_1_info_1.revoked_certificates[0].reason_critical == false + - crl_1_info_1.revoked_certificates[0].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[0].serial_number == certificate_infos.results[0].serial_number + - crl_1_info_1.revoked_certificates[1].invalidity_date == '20191012000000Z' + - crl_1_info_1.revoked_certificates[1].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[1].issuer is none + - crl_1_info_1.revoked_certificates[1].issuer_critical == false + - crl_1_info_1.revoked_certificates[1].reason == 'key_compromise' + - crl_1_info_1.revoked_certificates[1].reason_critical == true + - crl_1_info_1.revoked_certificates[1].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[1].serial_number == certificate_infos.results[1].serial_number + - crl_1_info_1.revoked_certificates[2].invalidity_date is none + - crl_1_info_1.revoked_certificates[2].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[2].issuer is none + - crl_1_info_1.revoked_certificates[2].issuer_critical == false + - crl_1_info_1.revoked_certificates[2].reason is none + - crl_1_info_1.revoked_certificates[2].reason_critical == false + - crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z' + - crl_1_info_1.revoked_certificates[2].serial_number == 1234 + +- name: Validate CRL 2 + assert: + that: + - crl_2_check is changed + - crl_2 is changed + - crl_2_idem_check is not changed + - crl_2_idem is not changed + - crl_2_idem_update_change_check is changed + - crl_2_idem_update_change is changed + - crl_2_idem_update_check is not changed + - crl_2_idem_update is not changed + - crl_2_change_check is changed + - crl_2_change is changed + - crl_2_change.crl == lookup('file', output_dir ~ '/ca-crl2.crl', rstrip=False) diff --git a/tests/requirements.yml b/tests/requirements.yml new file mode 100644 index 00000000..654b4839 --- /dev/null +++ b/tests/requirements.yml @@ -0,0 +1,4 @@ +integration_tests_dependencies: +- community.general +- ansible.windows +unit_tests_dependencies: [] diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt new file mode 100644 index 00000000..1a252bcb --- /dev/null +++ b/tests/sanity/ignore-2.10.txt @@ -0,0 +1,12 @@ +plugins/modules/acme_account_info.py validate-modules:return-syntax-error +plugins/modules/acme_certificate.py validate-modules:doc-elements-mismatch +tests/unit/mock/path.py future-import-boilerplate +tests/unit/mock/path.py metaclass-boilerplate +tests/unit/mock/yaml_helper.py future-import-boilerplate +tests/unit/mock/yaml_helper.py metaclass-boilerplate +tests/unit/module_utils/conftest.py future-import-boilerplate +tests/unit/module_utils/conftest.py metaclass-boilerplate +tests/unit/modules/conftest.py future-import-boilerplate +tests/unit/modules/conftest.py metaclass-boilerplate +tests/unit/modules/utils.py future-import-boilerplate +tests/unit/modules/utils.py metaclass-boilerplate \ No newline at end of file diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt new file mode 100644 index 00000000..1a252bcb --- /dev/null +++ b/tests/sanity/ignore-2.9.txt @@ -0,0 +1,12 @@ +plugins/modules/acme_account_info.py validate-modules:return-syntax-error +plugins/modules/acme_certificate.py validate-modules:doc-elements-mismatch +tests/unit/mock/path.py future-import-boilerplate +tests/unit/mock/path.py metaclass-boilerplate +tests/unit/mock/yaml_helper.py future-import-boilerplate +tests/unit/mock/yaml_helper.py metaclass-boilerplate +tests/unit/module_utils/conftest.py future-import-boilerplate +tests/unit/module_utils/conftest.py metaclass-boilerplate +tests/unit/modules/conftest.py future-import-boilerplate +tests/unit/modules/conftest.py metaclass-boilerplate +tests/unit/modules/utils.py future-import-boilerplate +tests/unit/modules/utils.py metaclass-boilerplate \ No newline at end of file diff --git a/tests/sanity/requirements.txt b/tests/sanity/requirements.txt new file mode 100644 index 00000000..3e3a9669 --- /dev/null +++ b/tests/sanity/requirements.txt @@ -0,0 +1,4 @@ +packaging # needed for update-bundled and changelog +sphinx ; python_version >= '3.5' # docs build requires python 3+ +sphinx-notfound-page ; python_version >= '3.5' # docs build requires python 3+ +straight.plugin ; python_version >= '3.5' # needed for hacking/build-ansible.py which will host changelog generation and requires python 3+ diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/compat/__init__.py b/tests/unit/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/compat/builtins.py b/tests/unit/compat/builtins.py new file mode 100644 index 00000000..f60ee678 --- /dev/null +++ b/tests/unit/compat/builtins.py @@ -0,0 +1,33 @@ +# (c) 2014, Toshio Kuratomi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# +# Compat for python2.7 +# + +# One unittest needs to import builtins via __import__() so we need to have +# the string that represents it +try: + import __builtin__ +except ImportError: + BUILTINS = 'builtins' +else: + BUILTINS = '__builtin__' diff --git a/tests/unit/compat/mock.py b/tests/unit/compat/mock.py new file mode 100644 index 00000000..0972cd2e --- /dev/null +++ b/tests/unit/compat/mock.py @@ -0,0 +1,122 @@ +# (c) 2014, Toshio Kuratomi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python3.x's unittest.mock module +''' +import sys + +# Python 2.7 + +# Note: Could use the pypi mock library on python3.x as well as python2.x. It +# is the same as the python3 stdlib mock library + +try: + # Allow wildcard import because we really do want to import all of mock's + # symbols into this compat shim + # pylint: disable=wildcard-import,unused-wildcard-import + from unittest.mock import * +except ImportError: + # Python 2 + # pylint: disable=wildcard-import,unused-wildcard-import + try: + from mock import * + except ImportError: + print('You need the mock library installed on python2.x to run tests') + + +# Prior to 3.4.4, mock_open cannot handle binary read_data +if sys.version_info >= (3,) and sys.version_info < (3, 4, 4): + file_spec = None + + def _iterate_read_data(read_data): + # Helper for mock_open: + # Retrieve lines from read_data via a generator so that separate calls to + # readline, read, and readlines are properly interleaved + sep = b'\n' if isinstance(read_data, bytes) else '\n' + data_as_list = [l + sep for l in read_data.split(sep)] + + if data_as_list[-1] == sep: + # If the last line ended in a newline, the list comprehension will have an + # extra entry that's just a newline. Remove this. + data_as_list = data_as_list[:-1] + else: + # If there wasn't an extra newline by itself, then the file being + # emulated doesn't have a newline to end the last line remove the + # newline that our naive format() added + data_as_list[-1] = data_as_list[-1][:-1] + + for line in data_as_list: + yield line + + def mock_open(mock=None, read_data=''): + """ + A helper function to create a mock to replace the use of `open`. It works + for `open` called directly or used as a context manager. + + The `mock` argument is the mock object to configure. If `None` (the + default) then a `MagicMock` will be created for you, with the API limited + to methods or attributes available on standard file handles. + + `read_data` is a string for the `read` methoddline`, and `readlines` of the + file handle to return. This is an empty string by default. + """ + def _readlines_side_effect(*args, **kwargs): + if handle.readlines.return_value is not None: + return handle.readlines.return_value + return list(_data) + + def _read_side_effect(*args, **kwargs): + if handle.read.return_value is not None: + return handle.read.return_value + return type(read_data)().join(_data) + + def _readline_side_effect(): + if handle.readline.return_value is not None: + while True: + yield handle.readline.return_value + for line in _data: + yield line + + global file_spec + if file_spec is None: + import _io + file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) + + if mock is None: + mock = MagicMock(name='open', spec=open) + + handle = MagicMock(spec=file_spec) + handle.__enter__.return_value = handle + + _data = _iterate_read_data(read_data) + + handle.write.return_value = None + handle.read.return_value = None + handle.readline.return_value = None + handle.readlines.return_value = None + + handle.read.side_effect = _read_side_effect + handle.readline.side_effect = _readline_side_effect() + handle.readlines.side_effect = _readlines_side_effect + + mock.return_value = handle + return mock diff --git a/tests/unit/compat/unittest.py b/tests/unit/compat/unittest.py new file mode 100644 index 00000000..98f08ad6 --- /dev/null +++ b/tests/unit/compat/unittest.py @@ -0,0 +1,38 @@ +# (c) 2014, Toshio Kuratomi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python2.7's unittest module +''' + +import sys + +# Allow wildcard import because we really do want to import all of +# unittests's symbols into this compat shim +# pylint: disable=wildcard-import,unused-wildcard-import +if sys.version_info < (2, 7): + try: + # Need unittest2 on python2.6 + from unittest2 import * + except ImportError: + print('You need unittest2 installed on python2.6.x to run tests') +else: + from unittest import * diff --git a/tests/unit/mock/__init__.py b/tests/unit/mock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/mock/loader.py b/tests/unit/mock/loader.py new file mode 100644 index 00000000..0ee47fbb --- /dev/null +++ b/tests/unit/mock/loader.py @@ -0,0 +1,116 @@ +# (c) 2012-2014, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +from ansible.errors import AnsibleParserError +from ansible.parsing.dataloader import DataLoader +from ansible.module_utils._text import to_bytes, to_text + + +class DictDataLoader(DataLoader): + + def __init__(self, file_mapping=None): + file_mapping = {} if file_mapping is None else file_mapping + assert type(file_mapping) == dict + + super(DictDataLoader, self).__init__() + + self._file_mapping = file_mapping + self._build_known_directories() + self._vault_secrets = None + + def load_from_file(self, path, cache=True, unsafe=False): + path = to_text(path) + if path in self._file_mapping: + return self.load(self._file_mapping[path], path) + return None + + # TODO: the real _get_file_contents returns a bytestring, so we actually convert the + # unicode/text it's created with to utf-8 + def _get_file_contents(self, path): + path = to_text(path) + if path in self._file_mapping: + return (to_bytes(self._file_mapping[path]), False) + else: + raise AnsibleParserError("file not found: %s" % path) + + def path_exists(self, path): + path = to_text(path) + return path in self._file_mapping or path in self._known_directories + + def is_file(self, path): + path = to_text(path) + return path in self._file_mapping + + def is_directory(self, path): + path = to_text(path) + return path in self._known_directories + + def list_directory(self, path): + ret = [] + path = to_text(path) + for x in (list(self._file_mapping.keys()) + self._known_directories): + if x.startswith(path): + if os.path.dirname(x) == path: + ret.append(os.path.basename(x)) + return ret + + def is_executable(self, path): + # FIXME: figure out a way to make paths return true for this + return False + + def _add_known_directory(self, directory): + if directory not in self._known_directories: + self._known_directories.append(directory) + + def _build_known_directories(self): + self._known_directories = [] + for path in self._file_mapping: + dirname = os.path.dirname(path) + while dirname not in ('/', ''): + self._add_known_directory(dirname) + dirname = os.path.dirname(dirname) + + def push(self, path, content): + rebuild_dirs = False + if path not in self._file_mapping: + rebuild_dirs = True + + self._file_mapping[path] = content + + if rebuild_dirs: + self._build_known_directories() + + def pop(self, path): + if path in self._file_mapping: + del self._file_mapping[path] + self._build_known_directories() + + def clear(self): + self._file_mapping = dict() + self._known_directories = [] + + def get_basedir(self): + return os.getcwd() + + def set_vault_secrets(self, vault_secrets): + self._vault_secrets = vault_secrets diff --git a/tests/unit/mock/path.py b/tests/unit/mock/path.py new file mode 100644 index 00000000..b2101a13 --- /dev/null +++ b/tests/unit/mock/path.py @@ -0,0 +1,5 @@ +from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock +from ansible.utils.path import unfrackpath + + +mock_unfrackpath_noop = MagicMock(spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x) diff --git a/tests/unit/mock/procenv.py b/tests/unit/mock/procenv.py new file mode 100644 index 00000000..866f7bf8 --- /dev/null +++ b/tests/unit/mock/procenv.py @@ -0,0 +1,90 @@ +# (c) 2016, Matt Davis +# (c) 2016, Toshio Kuratomi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import json + +from contextlib import contextmanager +from io import BytesIO, StringIO +from ansible_collections.community.crypto.tests.unit.compat import unittest +from ansible.module_utils.six import PY3 +from ansible.module_utils._text import to_bytes + + +@contextmanager +def swap_stdin_and_argv(stdin_data='', argv_data=tuple()): + """ + context manager that temporarily masks the test runner's values for stdin and argv + """ + real_stdin = sys.stdin + real_argv = sys.argv + + if PY3: + fake_stream = StringIO(stdin_data) + fake_stream.buffer = BytesIO(to_bytes(stdin_data)) + else: + fake_stream = BytesIO(to_bytes(stdin_data)) + + try: + sys.stdin = fake_stream + sys.argv = argv_data + + yield + finally: + sys.stdin = real_stdin + sys.argv = real_argv + + +@contextmanager +def swap_stdout(): + """ + context manager that temporarily replaces stdout for tests that need to verify output + """ + old_stdout = sys.stdout + + if PY3: + fake_stream = StringIO() + else: + fake_stream = BytesIO() + + try: + sys.stdout = fake_stream + + yield fake_stream + finally: + sys.stdout = old_stdout + + +class ModuleTestCase(unittest.TestCase): + def setUp(self, module_args=None): + if module_args is None: + module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False} + + args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args)) + + # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually + self.stdin_swap = swap_stdin_and_argv(stdin_data=args) + self.stdin_swap.__enter__() + + def tearDown(self): + # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually + self.stdin_swap.__exit__(None, None, None) diff --git a/tests/unit/mock/vault_helper.py b/tests/unit/mock/vault_helper.py new file mode 100644 index 00000000..dcce9c78 --- /dev/null +++ b/tests/unit/mock/vault_helper.py @@ -0,0 +1,39 @@ +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils._text import to_bytes + +from ansible.parsing.vault import VaultSecret + + +class TextVaultSecret(VaultSecret): + '''A secret piece of text. ie, a password. Tracks text encoding. + + The text encoding of the text may not be the default text encoding so + we keep track of the encoding so we encode it to the same bytes.''' + + def __init__(self, text, encoding=None, errors=None, _bytes=None): + super(TextVaultSecret, self).__init__() + self.text = text + self.encoding = encoding or 'utf-8' + self._bytes = _bytes + self.errors = errors or 'strict' + + @property + def bytes(self): + '''The text encoded with encoding, unless we specifically set _bytes.''' + return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors) diff --git a/tests/unit/mock/yaml_helper.py b/tests/unit/mock/yaml_helper.py new file mode 100644 index 00000000..cc095fea --- /dev/null +++ b/tests/unit/mock/yaml_helper.py @@ -0,0 +1,121 @@ +import io +import yaml + +from ansible.module_utils.six import PY3 +from ansible.parsing.yaml.loader import AnsibleLoader +from ansible.parsing.yaml.dumper import AnsibleDumper + + +class YamlTestUtils(object): + """Mixin class to combine with a unittest.TestCase subclass.""" + def _loader(self, stream): + """Vault related tests will want to override this. + + Vault cases should setup a AnsibleLoader that has the vault password.""" + return AnsibleLoader(stream) + + def _dump_stream(self, obj, stream, dumper=None): + """Dump to a py2-unicode or py3-string stream.""" + if PY3: + return yaml.dump(obj, stream, Dumper=dumper) + else: + return yaml.dump(obj, stream, Dumper=dumper, encoding=None) + + def _dump_string(self, obj, dumper=None): + """Dump to a py2-unicode or py3-string""" + if PY3: + return yaml.dump(obj, Dumper=dumper) + else: + return yaml.dump(obj, Dumper=dumper, encoding=None) + + def _dump_load_cycle(self, obj): + # Each pass though a dump or load revs the 'generation' + # obj to yaml string + string_from_object_dump = self._dump_string(obj, dumper=AnsibleDumper) + + # wrap a stream/file like StringIO around that yaml + stream_from_object_dump = io.StringIO(string_from_object_dump) + loader = self._loader(stream_from_object_dump) + # load the yaml stream to create a new instance of the object (gen 2) + obj_2 = loader.get_data() + + # dump the gen 2 objects directory to strings + string_from_object_dump_2 = self._dump_string(obj_2, + dumper=AnsibleDumper) + + # The gen 1 and gen 2 yaml strings + self.assertEqual(string_from_object_dump, string_from_object_dump_2) + # the gen 1 (orig) and gen 2 py object + self.assertEqual(obj, obj_2) + + # again! gen 3... load strings into py objects + stream_3 = io.StringIO(string_from_object_dump_2) + loader_3 = self._loader(stream_3) + obj_3 = loader_3.get_data() + + string_from_object_dump_3 = self._dump_string(obj_3, dumper=AnsibleDumper) + + self.assertEqual(obj, obj_3) + # should be transitive, but... + self.assertEqual(obj_2, obj_3) + self.assertEqual(string_from_object_dump, string_from_object_dump_3) + + def _old_dump_load_cycle(self, obj): + '''Dump the passed in object to yaml, load it back up, dump again, compare.''' + stream = io.StringIO() + + yaml_string = self._dump_string(obj, dumper=AnsibleDumper) + self._dump_stream(obj, stream, dumper=AnsibleDumper) + + yaml_string_from_stream = stream.getvalue() + + # reset stream + stream.seek(0) + + loader = self._loader(stream) + # loader = AnsibleLoader(stream, vault_password=self.vault_password) + obj_from_stream = loader.get_data() + + stream_from_string = io.StringIO(yaml_string) + loader2 = self._loader(stream_from_string) + # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password) + obj_from_string = loader2.get_data() + + stream_obj_from_stream = io.StringIO() + stream_obj_from_string = io.StringIO() + + if PY3: + yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) + yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) + else: + yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None) + yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None) + + yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() + yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() + + stream_obj_from_stream.seek(0) + stream_obj_from_string.seek(0) + + if PY3: + yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) + yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) + else: + yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None) + yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None) + + assert yaml_string == yaml_string_obj_from_stream + assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string + assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream == + yaml_string_stream_obj_from_string) + assert obj == obj_from_stream + assert obj == obj_from_string + assert obj == yaml_string_obj_from_stream + assert obj == yaml_string_obj_from_string + assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string + return {'obj': obj, + 'yaml_string': yaml_string, + 'yaml_string_from_stream': yaml_string_from_stream, + 'obj_from_stream': obj_from_stream, + 'obj_from_string': obj_from_string, + 'yaml_string_obj_from_string': yaml_string_obj_from_string} diff --git a/tests/unit/module_utils/__init__.py b/tests/unit/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/module_utils/acme/__init__.py b/tests/unit/module_utils/acme/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/module_utils/acme/fixtures/cert_1.pem b/tests/unit/module_utils/acme/fixtures/cert_1.pem new file mode 100644 index 00000000..bb4aca51 --- /dev/null +++ b/tests/unit/module_utils/acme/fixtures/cert_1.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBljCCATugAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwthbnNpYmxl +LmNvbTAeFw0xODExMjUxNTI4MjNaFw0xODExMjYxNTI4MjRaMBYxFDASBgNVBAMT +C2Fuc2libGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAJz0yAAXAwEm +OhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8S +LzE44/Epm6N6MHgwIwYDVR0RBBwwGoILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jn +MAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/BAUDAweAADATBgNVHSUEDDAKBggrBgEF +BQcDATAdBgNVHQ4EFgQUmNL9PMzNaUX74owwLFRiGDS3B3MwCgYIKoZIzj0EAwID +SQAwRgIhALz7Ur96ky0OfM5D9MwFmCg2jccqm/UglGI9+4KeOEIyAiEAwFX4tdll +QSrd1HY/jMsHwdK5wH3JkK/9+fGwyRP11VI= +-----END CERTIFICATE----- diff --git a/tests/unit/module_utils/acme/fixtures/csr_1.pem b/tests/unit/module_utils/acme/fixtures/csr_1.pem new file mode 100644 index 00000000..8fc37c40 --- /dev/null +++ b/tests/unit/module_utils/acme/fixtures/csr_1.pem @@ -0,0 +1,9 @@ +-----BEGIN NEW CERTIFICATE REQUEST----- +MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs +4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE +MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E +AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl +FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr +URnCJfTLr2T3 +-----END NEW CERTIFICATE REQUEST----- diff --git a/tests/unit/module_utils/acme/fixtures/csr_1.txt b/tests/unit/module_utils/acme/fixtures/csr_1.txt new file mode 100644 index 00000000..37c5cbda --- /dev/null +++ b/tests/unit/module_utils/acme/fixtures/csr_1.txt @@ -0,0 +1,28 @@ +Certificate Request: + Data: + Version: 1 (0x0) + Subject: CN = ansible.com + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35: + f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a: + d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90: + e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31: + 38:e3:f1:29:9b + ASN1 OID: prime256v1 + NIST CURVE: P-256 + Attributes: + Requested Extensions: + X509v3 Subject Alternative Name: + DNS:example.com, DNS:example.org + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: critical + Digital Signature + Signature Algorithm: ecdsa-with-SHA256 + 30:44:02:20:70:3c:a8:46:6c:05:54:10:e5:16:f6:c5:66:d8: + 92:77:9c:26:25:4d:65:b4:ce:89:b5:c7:e7:2d:69:e3:63:9e: + 02:20:2a:ee:38:1c:ab:ae:8a:45:52:43:8a:29:be:31:5c:e6: + 2c:81:a0:d3:f7:75:ab:51:19:c2:25:f4:cb:af:64:f7 diff --git a/tests/unit/module_utils/acme/fixtures/csr_2.pem b/tests/unit/module_utils/acme/fixtures/csr_2.pem new file mode 100644 index 00000000..295a26e1 --- /dev/null +++ b/tests/unit/module_utils/acme/fixtures/csr_2.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEqjCCApICAQAwADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANv1 +V7gDsh76O//d9wclBcW6kNpWeR6eAggzThwbMZjcO7GFHQsBZCZGGVdyS37uhejc +RrIBdtDDWXhoh3Dz+GQxD+6GuwAEFyL1F3MfT0v1HHoO8fE74G5mD6+ZA2HRDeU9 +jf8BPyVWHBtNbCmJGSlSNOFejWCmwvsLARQxqFBuTyRjgos4BkLyWMqZRukrzO1P +z7IBhuFrB608t+AG4vGnPXZNM7xefhzO8bPOiepT0YS2ERPkFmOy97SnwTGdKykw +ZYM9oKukYhE4Z+yOaTFpJMBNXwDCI5TMnhtc6eJrf5sOFH92n2E9+YWMoahUOiTw +G6XV5HfSpySpwORUaTITQRsPAM+bmK9f1jB6ctfFVwpa8uW/h8pSgbHgZvkeD6s6 +rFLh9TQ24t0vrRmhnY7/AMFgbgJoBTBq0l0lEXS4FCGKDGqQOqSws+eHR/pHA4uY +v8d498SQl9fYsT/c7Uj3/JnMSRVN942yQUFCzwLf0/WzWCi2HTqPM8CPh5ryiJ30 +GAN2eb026/noyTOXm479Tg9o86Tw9qczE0j0CdcRnr6J337RGHQg58PZ7j+hnUmK +wgyclyvjE10ZFBgToMGSnzYp5UeRcOFZ3bnK6LOsGC75mIvz2OQgSQeO5VQASEnO +9uhygNyo91sK4BtVroloit8ZCa82LlsHSCj/mMzPAgMBAAGgZTBjBgkqhkiG9w0B +CQ4xVjBUMFIGA1UdEQRLMEmCC2Fuc2libGUuY29thwR/AAABhxAAAAAAAAAAAAAA +AAAAAAABhxAgAQ2IrBD+AQAAAAAAAAAAhxAgARI0VnirzZh2VDIQ/ty6MA0GCSqG +SIb3DQEBCwUAA4ICAQBFRuANzVRcze+iur0YevjtYIXDa03GoWWkgnLuE8u8epTM +2248duG3TmvVvxWPN4iFrvFcZIvNsevBo+Z7kXJ24m3YldtXvwfAYmCZ062apSoh +yzgo3Q0KfDehwLcoJPe5bh+jbbgJVGGvJug/QFyHSVl+iGyFUXE7pwafl9LuNDi3 +yfOYZLIQ34mBH4Rsvymj9xSTYliWDEEU/o7RrrZeEqkOxNeLh64LbnifdrYUputz +yBURg2xs9hpAsytZJX90iJW8aYPM1aQ7eetqTViIRoqUAmIQobnKlNnpOliBHl+p +RY+AtTnsfAetKUP7OsAZkHRTGAXx0JHJQ1ITY8w5Dcw/v1bDCbAfkDubBP3X+us9 +RQk2h6m74hWFFNu9xOfkNejPf7h4gywfDjo/wGZFSWKyi6avB9V53znZgRUwc009 +p5MM9e37MH8pyBqfnbSwOj4hUoyecRCIAFdywjMb9akP2u15XP3MOtJOEvecyCxN +TZBxupTg65zB47GeSAufnc8FaTZkE8xPuCtbvqOVOkWYqzlqNdCfK8f3AZdlpwLh +38wdUm5G7LIu6aQNiY66aQs9qVpoGvqdmxHRkuSwqwZxGgzcY1yJaWGXQ6R4jgC3 +VKlMTUVs1WYV6jrYLHcVt6Rn/2FVTOns3Jn6cTPOdKViYoqF+yW8yCEAqAskZw== +-----END CERTIFICATE REQUEST----- diff --git a/tests/unit/module_utils/acme/fixtures/csr_2.txt b/tests/unit/module_utils/acme/fixtures/csr_2.txt new file mode 100644 index 00000000..7a54ee3f --- /dev/null +++ b/tests/unit/module_utils/acme/fixtures/csr_2.txt @@ -0,0 +1,78 @@ +Certificate Request: + Data: + Version: 1 (0x0) + Subject: + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:db:f5:57:b8:03:b2:1e:fa:3b:ff:dd:f7:07:25: + 05:c5:ba:90:da:56:79:1e:9e:02:08:33:4e:1c:1b: + 31:98:dc:3b:b1:85:1d:0b:01:64:26:46:19:57:72: + 4b:7e:ee:85:e8:dc:46:b2:01:76:d0:c3:59:78:68: + 87:70:f3:f8:64:31:0f:ee:86:bb:00:04:17:22:f5: + 17:73:1f:4f:4b:f5:1c:7a:0e:f1:f1:3b:e0:6e:66: + 0f:af:99:03:61:d1:0d:e5:3d:8d:ff:01:3f:25:56: + 1c:1b:4d:6c:29:89:19:29:52:34:e1:5e:8d:60:a6: + c2:fb:0b:01:14:31:a8:50:6e:4f:24:63:82:8b:38: + 06:42:f2:58:ca:99:46:e9:2b:cc:ed:4f:cf:b2:01: + 86:e1:6b:07:ad:3c:b7:e0:06:e2:f1:a7:3d:76:4d: + 33:bc:5e:7e:1c:ce:f1:b3:ce:89:ea:53:d1:84:b6: + 11:13:e4:16:63:b2:f7:b4:a7:c1:31:9d:2b:29:30: + 65:83:3d:a0:ab:a4:62:11:38:67:ec:8e:69:31:69: + 24:c0:4d:5f:00:c2:23:94:cc:9e:1b:5c:e9:e2:6b: + 7f:9b:0e:14:7f:76:9f:61:3d:f9:85:8c:a1:a8:54: + 3a:24:f0:1b:a5:d5:e4:77:d2:a7:24:a9:c0:e4:54: + 69:32:13:41:1b:0f:00:cf:9b:98:af:5f:d6:30:7a: + 72:d7:c5:57:0a:5a:f2:e5:bf:87:ca:52:81:b1:e0: + 66:f9:1e:0f:ab:3a:ac:52:e1:f5:34:36:e2:dd:2f: + ad:19:a1:9d:8e:ff:00:c1:60:6e:02:68:05:30:6a: + d2:5d:25:11:74:b8:14:21:8a:0c:6a:90:3a:a4:b0: + b3:e7:87:47:fa:47:03:8b:98:bf:c7:78:f7:c4:90: + 97:d7:d8:b1:3f:dc:ed:48:f7:fc:99:cc:49:15:4d: + f7:8d:b2:41:41:42:cf:02:df:d3:f5:b3:58:28:b6: + 1d:3a:8f:33:c0:8f:87:9a:f2:88:9d:f4:18:03:76: + 79:bd:36:eb:f9:e8:c9:33:97:9b:8e:fd:4e:0f:68: + f3:a4:f0:f6:a7:33:13:48:f4:09:d7:11:9e:be:89: + df:7e:d1:18:74:20:e7:c3:d9:ee:3f:a1:9d:49:8a: + c2:0c:9c:97:2b:e3:13:5d:19:14:18:13:a0:c1:92: + 9f:36:29:e5:47:91:70:e1:59:dd:b9:ca:e8:b3:ac: + 18:2e:f9:98:8b:f3:d8:e4:20:49:07:8e:e5:54:00: + 48:49:ce:f6:e8:72:80:dc:a8:f7:5b:0a:e0:1b:55: + ae:89:68:8a:df:19:09:af:36:2e:5b:07:48:28:ff: + 98:cc:cf + Exponent: 65537 (0x10001) + Attributes: + Requested Extensions: + X509v3 Subject Alternative Name: + DNS:ansible.com, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:2001:D88:AC10:FE01:0:0:0:0, IP Address:2001:1234:5678:ABCD:9876:5432:10FE:DCBA + Signature Algorithm: sha256WithRSAEncryption + 45:46:e0:0d:cd:54:5c:cd:ef:a2:ba:bd:18:7a:f8:ed:60:85: + c3:6b:4d:c6:a1:65:a4:82:72:ee:13:cb:bc:7a:94:cc:db:6e: + 3c:76:e1:b7:4e:6b:d5:bf:15:8f:37:88:85:ae:f1:5c:64:8b: + cd:b1:eb:c1:a3:e6:7b:91:72:76:e2:6d:d8:95:db:57:bf:07: + c0:62:60:99:d3:ad:9a:a5:2a:21:cb:38:28:dd:0d:0a:7c:37: + a1:c0:b7:28:24:f7:b9:6e:1f:a3:6d:b8:09:54:61:af:26:e8: + 3f:40:5c:87:49:59:7e:88:6c:85:51:71:3b:a7:06:9f:97:d2: + ee:34:38:b7:c9:f3:98:64:b2:10:df:89:81:1f:84:6c:bf:29: + a3:f7:14:93:62:58:96:0c:41:14:fe:8e:d1:ae:b6:5e:12:a9: + 0e:c4:d7:8b:87:ae:0b:6e:78:9f:76:b6:14:a6:eb:73:c8:15: + 11:83:6c:6c:f6:1a:40:b3:2b:59:25:7f:74:88:95:bc:69:83: + cc:d5:a4:3b:79:eb:6a:4d:58:88:46:8a:94:02:62:10:a1:b9: + ca:94:d9:e9:3a:58:81:1e:5f:a9:45:8f:80:b5:39:ec:7c:07: + ad:29:43:fb:3a:c0:19:90:74:53:18:05:f1:d0:91:c9:43:52: + 13:63:cc:39:0d:cc:3f:bf:56:c3:09:b0:1f:90:3b:9b:04:fd: + d7:fa:eb:3d:45:09:36:87:a9:bb:e2:15:85:14:db:bd:c4:e7: + e4:35:e8:cf:7f:b8:78:83:2c:1f:0e:3a:3f:c0:66:45:49:62: + b2:8b:a6:af:07:d5:79:df:39:d9:81:15:30:73:4d:3d:a7:93: + 0c:f5:ed:fb:30:7f:29:c8:1a:9f:9d:b4:b0:3a:3e:21:52:8c: + 9e:71:10:88:00:57:72:c2:33:1b:f5:a9:0f:da:ed:79:5c:fd: + cc:3a:d2:4e:12:f7:9c:c8:2c:4d:4d:90:71:ba:94:e0:eb:9c: + c1:e3:b1:9e:48:0b:9f:9d:cf:05:69:36:64:13:cc:4f:b8:2b: + 5b:be:a3:95:3a:45:98:ab:39:6a:35:d0:9f:2b:c7:f7:01:97: + 65:a7:02:e1:df:cc:1d:52:6e:46:ec:b2:2e:e9:a4:0d:89:8e: + ba:69:0b:3d:a9:5a:68:1a:fa:9d:9b:11:d1:92:e4:b0:ab:06: + 71:1a:0c:dc:63:5c:89:69:61:97:43:a4:78:8e:00:b7:54:a9: + 4c:4d:45:6c:d5:66:15:ea:3a:d8:2c:77:15:b7:a4:67:ff:61: + 55:4c:e9:ec:dc:99:fa:71:33:ce:74:a5:62:62:8a:85:fb:25: + bc:c8:21:00:a8:0b:24:67 diff --git a/tests/unit/module_utils/acme/fixtures/privatekey_1.pem b/tests/unit/module_utils/acme/fixtures/privatekey_1.pem new file mode 100644 index 00000000..97209eda --- /dev/null +++ b/tests/unit/module_utils/acme/fixtures/privatekey_1.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49 +AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A +mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw== +-----END EC PRIVATE KEY----- diff --git a/tests/unit/module_utils/acme/fixtures/privatekey_1.txt b/tests/unit/module_utils/acme/fixtures/privatekey_1.txt new file mode 100644 index 00000000..e25cfd45 --- /dev/null +++ b/tests/unit/module_utils/acme/fixtures/privatekey_1.txt @@ -0,0 +1,14 @@ +read EC key +Private-Key: (256 bit) +priv: + 35:9a:8d:4d:0f:ca:16:0a:7a:e9:5f:cb:f9:6e:36: + d9:00:bd:ee:c3:93:04:34:d5:b5:c9:f7:bc:db:c4: + 1e:ba +pub: + 04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35: + f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a: + d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90: + e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31: + 38:e3:f1:29:9b +ASN1 OID: prime256v1 +NIST CURVE: P-256 diff --git a/tests/unit/module_utils/acme/test_acme.py b/tests/unit/module_utils/acme/test_acme.py new file mode 100644 index 00000000..6fe90e6d --- /dev/null +++ b/tests/unit/module_utils/acme/test_acme.py @@ -0,0 +1,216 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import base64 +import datetime +import os.path +import pytest + +from mock import MagicMock + + +from ansible_collections.community.crypto.plugins.module_utils.acme import ( + HAS_CURRENT_CRYPTOGRAPHY, + nopad_b64, + write_file, + read_file, + pem_to_der, + _parse_key_openssl, + # _sign_request_openssl, + _parse_key_cryptography, + # _sign_request_cryptography, + _normalize_ip, + openssl_get_csr_identifiers, + cryptography_get_csr_identifiers, + cryptography_get_cert_days, +) + + +def load_fixture(name): + with open(os.path.join(os.path.dirname(__file__), 'fixtures', name)) as f: + return f.read() + + +################################################ + +NOPAD_B64 = [ + ("", ""), + ("\n", "Cg"), + ("123", "MTIz"), + ("Lorem?ipsum", "TG9yZW0_aXBzdW0"), +] + + +@pytest.mark.parametrize("value, result", NOPAD_B64) +def test_nopad_b64(value, result): + assert nopad_b64(value.encode('utf-8')) == result + + +################################################ + +TEST_TEXT = r"""1234 +5678""" + + +def test_read_file(tmpdir): + fn = tmpdir / 'test.txt' + fn.write(TEST_TEXT) + assert read_file(str(fn), 't') == TEST_TEXT + assert read_file(str(fn), 'b') == TEST_TEXT.encode('utf-8') + + +def test_write_file(tmpdir): + fn = tmpdir / 'test.txt' + module = MagicMock() + write_file(module, str(fn), TEST_TEXT.encode('utf-8')) + assert fn.read() == TEST_TEXT + + +################################################ + +TEST_PEM_DERS = [ + ( + load_fixture('privatekey_1.pem'), + base64.b64decode('MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAo' + 'GCCqGSM49AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3' + 'lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==') + ) +] + + +@pytest.mark.parametrize("pem, der", TEST_PEM_DERS) +def test_pem_to_der(pem, der, tmpdir): + fn = tmpdir / 'test.pem' + fn.write(pem) + assert pem_to_der(str(fn)) == der + + +################################################ + +TEST_KEYS = [ + ( + load_fixture('privatekey_1.pem'), + { + 'alg': 'ES256', + 'hash': 'sha256', + 'jwk': { + 'crv': 'P-256', + 'kty': 'EC', + 'x': 'AJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68E', + 'y': 'vEEs4V0egJkNyM2Q4pp001zu14VcpQ0_Ei8xOOPxKZs', + }, + 'point_size': 32, + 'type': 'ec', + }, + load_fixture('privatekey_1.txt'), + ) +] + + +@pytest.mark.parametrize("pem, result, openssl_output", TEST_KEYS) +def test_eckeyparse_openssl(pem, result, openssl_output, tmpdir): + fn = tmpdir / 'test.key' + fn.write(pem) + module = MagicMock() + module.run_command = MagicMock(return_value=(0, openssl_output, 0)) + error, key = _parse_key_openssl('openssl', module, key_file=str(fn)) + assert error is None + key.pop('key_file') + assert key == result + + +if HAS_CURRENT_CRYPTOGRAPHY: + @pytest.mark.parametrize("pem, result, dummy", TEST_KEYS) + def test_eckeyparse_cryptography(pem, result, dummy): + module = MagicMock() + error, key = _parse_key_cryptography(module, key_content=pem) + assert error is None + key.pop('key_obj') + assert key == result + + +################################################ + +TEST_IPS = [ + ("0:0:0:0:0:0:0:1", "::1"), + ("1::0:2", "1::2"), + ("0000:0001:0000:0000:0000:0000:0000:0001", "0:1::1"), + ("0000:0001:0000:0000:0001:0000:0000:0001", "0:1::1:0:0:1"), + ("0000:0001:0000:0001:0000:0001:0000:0001", "0:1:0:1:0:1:0:1"), + ("0.0.0.0", "0.0.0.0"), + ("000.001.000.000", "0.1.0.0"), + ("2001:d88:ac10:fe01:0:0:0:0", "2001:d88:ac10:fe01::"), + ("0000:0000:0000:0000:0000:0000:0000:0000", "::"), +] + + +@pytest.mark.parametrize("ip, result", TEST_IPS) +def test_normalize_ip(ip, result): + assert _normalize_ip(ip) == result + + +################################################ + +TEST_CSRS = [ + ( + load_fixture('csr_1.pem'), + set([ + ('dns', 'ansible.com'), + ('dns', 'example.com'), + ('dns', 'example.org') + ]), + load_fixture('csr_1.txt'), + ), + ( + load_fixture('csr_2.pem'), + set([ + ('dns', 'ansible.com'), + ('ip', '127.0.0.1'), + ('ip', '::1'), + ('ip', '2001:d88:ac10:fe01::'), + ('ip', '2001:1234:5678:abcd:9876:5432:10fe:dcba') + ]), + load_fixture('csr_2.txt'), + ), +] + + +@pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS) +def test_csridentifiers_openssl(csr, result, openssl_output, tmpdir): + fn = tmpdir / 'test.csr' + fn.write(csr) + module = MagicMock() + module.run_command = MagicMock(return_value=(0, openssl_output, 0)) + identifiers = openssl_get_csr_identifiers('openssl', module, str(fn)) + assert identifiers == result + + +if HAS_CURRENT_CRYPTOGRAPHY: + @pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS) + def test_csridentifiers_cryptography(csr, result, openssl_output, tmpdir): + fn = tmpdir / 'test.csr' + fn.write(csr) + module = MagicMock() + identifiers = cryptography_get_csr_identifiers(module, str(fn)) + assert identifiers == result + + +################################################ + +TEST_CERT = load_fixture("cert_1.pem") + +TEST_CERT_DAYS = [ + (datetime.datetime(2018, 11, 15, 1, 2, 3), 11), + (datetime.datetime(2018, 11, 25, 15, 20, 0), 1), + (datetime.datetime(2018, 11, 25, 15, 30, 0), 0), +] + + +if HAS_CURRENT_CRYPTOGRAPHY: + @pytest.mark.parametrize("now, expected_days", TEST_CERT_DAYS) + def test_certdays_cryptography(now, expected_days, tmpdir): + fn = tmpdir / 'test-cert.pem' + fn.write(TEST_CERT) + module = MagicMock() + days = cryptography_get_cert_days(module, str(fn), now=now) + assert days == expected_days diff --git a/tests/unit/module_utils/conftest.py b/tests/unit/module_utils/conftest.py new file mode 100644 index 00000000..9e4e5257 --- /dev/null +++ b/tests/unit/module_utils/conftest.py @@ -0,0 +1,69 @@ +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import json +import sys +from io import BytesIO + +import pytest + +import ansible.module_utils.basic +from ansible.module_utils.six import PY3, string_types +from ansible.module_utils._text import to_bytes +from ansible.module_utils.common._collections_compat import MutableMapping + + +@pytest.fixture +def stdin(mocker, request): + old_args = ansible.module_utils.basic._ANSIBLE_ARGS + ansible.module_utils.basic._ANSIBLE_ARGS = None + old_argv = sys.argv + sys.argv = ['ansible_unittest'] + + if isinstance(request.param, string_types): + args = request.param + elif isinstance(request.param, MutableMapping): + if 'ANSIBLE_MODULE_ARGS' not in request.param: + request.param = {'ANSIBLE_MODULE_ARGS': request.param} + if '_ansible_remote_tmp' not in request.param['ANSIBLE_MODULE_ARGS']: + request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in request.param['ANSIBLE_MODULE_ARGS']: + request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False + args = json.dumps(request.param) + else: + raise Exception('Malformed data to the stdin pytest fixture') + + fake_stdin = BytesIO(to_bytes(args, errors='surrogate_or_strict')) + if PY3: + mocker.patch('ansible.module_utils.basic.sys.stdin', mocker.MagicMock()) + mocker.patch('ansible.module_utils.basic.sys.stdin.buffer', fake_stdin) + else: + mocker.patch('ansible.module_utils.basic.sys.stdin', fake_stdin) + + yield fake_stdin + + ansible.module_utils.basic._ANSIBLE_ARGS = old_args + sys.argv = old_argv + + +@pytest.fixture +def am(stdin, request): + old_args = ansible.module_utils.basic._ANSIBLE_ARGS + ansible.module_utils.basic._ANSIBLE_ARGS = None + old_argv = sys.argv + sys.argv = ['ansible_unittest'] + + argspec = {} + if hasattr(request, 'param'): + if isinstance(request.param, dict): + argspec = request.param + + am = ansible.module_utils.basic.AnsibleModule( + argument_spec=argspec, + ) + am._name = 'ansible_unittest' + + yield am + + ansible.module_utils.basic._ANSIBLE_ARGS = old_args + sys.argv = old_argv diff --git a/tests/unit/modules/__init__.py b/tests/unit/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/modules/conftest.py b/tests/unit/modules/conftest.py new file mode 100644 index 00000000..3bbfe0b7 --- /dev/null +++ b/tests/unit/modules/conftest.py @@ -0,0 +1,28 @@ +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import json + +import pytest + +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_bytes +from ansible.module_utils.common._collections_compat import MutableMapping + + +@pytest.fixture +def patch_ansible_module(request, mocker): + if isinstance(request.param, string_types): + args = request.param + elif isinstance(request.param, MutableMapping): + if 'ANSIBLE_MODULE_ARGS' not in request.param: + request.param = {'ANSIBLE_MODULE_ARGS': request.param} + if '_ansible_remote_tmp' not in request.param['ANSIBLE_MODULE_ARGS']: + request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in request.param['ANSIBLE_MODULE_ARGS']: + request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False + args = json.dumps(request.param) + else: + raise Exception('Malformed data to the patch_ansible_module pytest fixture') + + mocker.patch('ansible.module_utils.basic._ANSIBLE_ARGS', to_bytes(args)) diff --git a/tests/unit/modules/crypto/__init__.py b/tests/unit/modules/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/modules/crypto/test_luks_device.py b/tests/unit/modules/crypto/test_luks_device.py new file mode 100644 index 00000000..835bba61 --- /dev/null +++ b/tests/unit/modules/crypto/test_luks_device.py @@ -0,0 +1,305 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest +from ansible_collections.community.crypto.plugins.modules import luks_device +from ansible_collections.community.crypto.tests.unit.compat.mock import patch +from ansible.module_utils import basic + + +class DummyModule(object): + # module to mock AnsibleModule class + def __init__(self): + self.params = dict() + + def fail_json(self, msg=""): + raise ValueError(msg) + + def get_bin_path(self, command, dummy): + return command + + +# ===== Handler & CryptHandler methods tests ===== + +def test_generate_luks_name(monkeypatch): + module = DummyModule() + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, "UUID", ""]) + crypt = luks_device.CryptHandler(module) + assert crypt.generate_luks_name("/dev/dummy") == "luks-UUID" + + +def test_get_container_name_by_device(monkeypatch): + module = DummyModule() + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, "crypt container_name", ""]) + crypt = luks_device.CryptHandler(module) + assert crypt.get_container_name_by_device("/dev/dummy") == "container_name" + + +def test_get_container_device_by_name(monkeypatch): + module = DummyModule() + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, "device: /dev/luksdevice", ""]) + crypt = luks_device.CryptHandler(module) + assert crypt.get_container_device_by_name("dummy") == "/dev/luksdevice" + + +def test_run_luks_remove(monkeypatch): + def run_command_check(self, command): + # check that wipefs command is actually called + assert command[0] == "wipefs" + return [0, "", ""] + + module = DummyModule() + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_name_by_device", + lambda x, y: None) + monkeypatch.setattr(luks_device.Handler, + "_run_command", + run_command_check) + crypt = luks_device.CryptHandler(module) + crypt.run_luks_remove("dummy") + + +# ===== ConditionsHandler methods data and tests ===== + +# device, key, passphrase, state, is_luks, label, expected +LUKS_CREATE_DATA = ( + ("dummy", "key", None, "present", False, None, True), + (None, "key", None, "present", False, None, False), + (None, "key", None, "present", False, "labelName", True), + ("dummy", None, None, "present", False, None, False), + ("dummy", "key", None, "absent", False, None, False), + ("dummy", "key", None, "opened", True, None, False), + ("dummy", "key", None, "closed", True, None, False), + ("dummy", "key", None, "present", True, None, False), + ("dummy", None, "foo", "present", False, None, True), + (None, None, "bar", "present", False, None, False), + (None, None, "baz", "present", False, "labelName", True), + ("dummy", None, None, "present", False, None, False), + ("dummy", None, "quz", "absent", False, None, False), + ("dummy", None, "qux", "opened", True, None, False), + ("dummy", None, "quux", "closed", True, None, False), + ("dummy", None, "corge", "present", True, None, False)) + +# device, state, is_luks, expected +LUKS_REMOVE_DATA = ( + ("dummy", "absent", True, True), + (None, "absent", True, False), + ("dummy", "present", True, False), + ("dummy", "absent", False, False)) + +# device, key, passphrase, state, name, name_by_dev, expected +LUKS_OPEN_DATA = ( + ("dummy", "key", None, "present", "name", None, False), + ("dummy", "key", None, "absent", "name", None, False), + ("dummy", "key", None, "closed", "name", None, False), + ("dummy", "key", None, "opened", "name", None, True), + (None, "key", None, "opened", "name", None, False), + ("dummy", None, None, "opened", "name", None, False), + ("dummy", "key", None, "opened", "name", "name", False), + ("dummy", "key", None, "opened", "beer", "name", "exception"), + ("dummy", None, "foo", "present", "name", None, False), + ("dummy", None, "bar", "absent", "name", None, False), + ("dummy", None, "baz", "closed", "name", None, False), + ("dummy", None, "qux", "opened", "name", None, True), + (None, None, "quux", "opened", "name", None, False), + ("dummy", None, None, "opened", "name", None, False), + ("dummy", None, "quuz", "opened", "name", "name", False), + ("dummy", None, "corge", "opened", "beer", "name", "exception")) + +# device, dev_by_name, name, name_by_dev, state, label, expected +LUKS_CLOSE_DATA = ( + ("dummy", "dummy", "name", "name", "present", None, False), + ("dummy", "dummy", "name", "name", "absent", None, False), + ("dummy", "dummy", "name", "name", "opened", None, False), + ("dummy", "dummy", "name", "name", "closed", None, True), + (None, "dummy", "name", "name", "closed", None, True), + ("dummy", "dummy", None, "name", "closed", None, True), + (None, "dummy", None, "name", "closed", None, False)) + +# device, key, passphrase, new_key, new_passphrase, state, label, expected +LUKS_ADD_KEY_DATA = ( + ("dummy", "key", None, "new_key", None, "present", None, True), + (None, "key", None, "new_key", None, "present", "labelName", True), + (None, "key", None, "new_key", None, "present", None, False), + ("dummy", None, None, "new_key", None, "present", None, False), + ("dummy", "key", None, None, None, "present", None, False), + ("dummy", "key", None, "new_key", None, "absent", None, "exception"), + ("dummy", None, "pass", "new_key", None, "present", None, True), + (None, None, "pass", "new_key", None, "present", "labelName", True), + ("dummy", "key", None, None, "new_pass", "present", None, True), + (None, "key", None, None, "new_pass", "present", "labelName", True), + (None, "key", None, None, "new_pass", "present", None, False), + ("dummy", None, None, None, "new_pass", "present", None, False), + ("dummy", "key", None, None, None, "present", None, False), + ("dummy", "key", None, None, "new_pass", "absent", None, "exception"), + ("dummy", None, "pass", None, "new_pass", "present", None, True), + (None, None, "pass", None, "new_pass", "present", "labelName", True)) + +# device, remove_key, remove_passphrase, state, label, expected +LUKS_REMOVE_KEY_DATA = ( + ("dummy", "key", None, "present", None, True), + (None, "key", None, "present", None, False), + (None, "key", None, "present", "labelName", True), + ("dummy", None, None, "present", None, False), + ("dummy", "key", None, "absent", None, "exception"), + ("dummy", None, "foo", "present", None, True), + (None, None, "foo", "present", None, False), + (None, None, "foo", "present", "labelName", True), + ("dummy", None, None, "present", None, False), + ("dummy", None, "foo", "absent", None, "exception")) + + +@pytest.mark.parametrize("device, keyfile, passphrase, state, is_luks, " + + "label, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6]) + for d in LUKS_CREATE_DATA)) +def test_luks_create(device, keyfile, passphrase, state, is_luks, label, + expected, monkeypatch): + module = DummyModule() + + module.params["device"] = device + module.params["keyfile"] = keyfile + module.params["passphrase"] = passphrase + module.params["state"] = state + module.params["label"] = label + + monkeypatch.setattr(luks_device.CryptHandler, "is_luks", + lambda x, y: is_luks) + crypt = luks_device.CryptHandler(module) + if device is None: + monkeypatch.setattr(luks_device.Handler, "get_device_by_label", + lambda x, y: [0, "/dev/dummy", ""]) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_create() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, state, is_luks, expected", + ((d[0], d[1], d[2], d[3]) + for d in LUKS_REMOVE_DATA)) +def test_luks_remove(device, state, is_luks, expected, monkeypatch): + module = DummyModule() + + module.params["device"] = device + module.params["state"] = state + + monkeypatch.setattr(luks_device.CryptHandler, "is_luks", + lambda x, y: is_luks) + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_remove() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, keyfile, passphrase, state, name, " + "name_by_dev, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6]) + for d in LUKS_OPEN_DATA)) +def test_luks_open(device, keyfile, passphrase, state, name, name_by_dev, + expected, monkeypatch): + module = DummyModule() + module.params["device"] = device + module.params["keyfile"] = keyfile + module.params["passphrase"] = passphrase + module.params["state"] = state + module.params["name"] = name + + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_name_by_device", + lambda x, y: name_by_dev) + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_device_by_name", + lambda x, y: device) + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, device, ""]) + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_open() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, dev_by_name, name, name_by_dev, " + "state, label, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6]) + for d in LUKS_CLOSE_DATA)) +def test_luks_close(device, dev_by_name, name, name_by_dev, state, + label, expected, monkeypatch): + module = DummyModule() + module.params["device"] = device + module.params["name"] = name + module.params["state"] = state + module.params["label"] = label + + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_name_by_device", + lambda x, y: name_by_dev) + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_device_by_name", + lambda x, y: dev_by_name) + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_close() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, keyfile, passphrase, new_keyfile, " + + "new_passphrase, state, label, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]) + for d in LUKS_ADD_KEY_DATA)) +def test_luks_add_key(device, keyfile, passphrase, new_keyfile, new_passphrase, + state, label, expected, monkeypatch): + module = DummyModule() + module.params["device"] = device + module.params["keyfile"] = keyfile + module.params["passphrase"] = passphrase + module.params["new_keyfile"] = new_keyfile + module.params["new_passphrase"] = new_passphrase + module.params["state"] = state + module.params["label"] = label + + monkeypatch.setattr(luks_device.Handler, "get_device_by_label", + lambda x, y: [0, "/dev/dummy", ""]) + + try: + conditions = luks_device.ConditionsHandler(module, module) + assert conditions.luks_add_key() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, remove_keyfile, remove_passphrase, state, " + + "label, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5]) + for d in LUKS_REMOVE_KEY_DATA)) +def test_luks_remove_key(device, remove_keyfile, remove_passphrase, state, + label, expected, monkeypatch): + + module = DummyModule() + module.params["device"] = device + module.params["remove_keyfile"] = remove_keyfile + module.params["remove_passphrase"] = remove_passphrase + module.params["state"] = state + module.params["label"] = label + + monkeypatch.setattr(luks_device.Handler, "get_device_by_label", + lambda x, y: [0, "/dev/dummy", ""]) + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, device, ""]) + + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_remove_key() == expected + except ValueError: + assert expected == "exception" diff --git a/tests/unit/modules/utils.py b/tests/unit/modules/utils.py new file mode 100644 index 00000000..dfa205df --- /dev/null +++ b/tests/unit/modules/utils.py @@ -0,0 +1,47 @@ +import json + +from ansible_collections.community.crypto.tests.unit.compat import unittest +from ansible_collections.community.crypto.tests.unit.compat.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + + +def set_module_args(args): + if '_ansible_remote_tmp' not in args: + args['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in args: + args['_ansible_keep_remote_files'] = False + + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +class ModuleTestCase(unittest.TestCase): + + def setUp(self): + self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) + self.mock_module.start() + self.mock_sleep = patch('time.sleep') + self.mock_sleep.start() + set_module_args({}) + self.addCleanup(self.mock_module.stop) + self.addCleanup(self.mock_sleep.stop) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt new file mode 100644 index 00000000..a9772bea --- /dev/null +++ b/tests/unit/requirements.txt @@ -0,0 +1,42 @@ +boto3 +placebo +pycrypto +passlib +pypsrp +python-memcached +pytz +pyvmomi +redis +requests +setuptools > 0.6 # pytest-xdist installed via requirements does not work with very old setuptools (sanity_ok) +unittest2 ; python_version < '2.7' +importlib ; python_version < '2.7' +netaddr +ipaddress +netapp-lib +solidfire-sdk-python + +# requirements for F5 specific modules +f5-sdk ; python_version >= '2.7' +f5-icontrol-rest ; python_version >= '2.7' +deepdiff + +# requirement for Fortinet specific modules +pyFMG + +# requirement for aci_rest module +xmljson + +# requirement for winrm connection plugin tests +pexpect + +# requirement for the linode module +linode-python # APIv3 +linode_api4 ; python_version > '2.6' # APIv4 + +# requirement for the gitlab module +python-gitlab +httmock + +# requirment for kubevirt modules +openshift ; python_version >= '2.7'