Initial commit

This commit is contained in:
Ansible Core Team
2020-03-09 13:11:34 +00:00
commit a9f45b4d5b
249 changed files with 37903 additions and 0 deletions

View File

@@ -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

387
.gitignore vendored Normal file
View File

@@ -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

675
COPYING Normal file
View File

@@ -0,0 +1,675 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <http://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

4
README.md Normal file
View File

@@ -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
=================================================

15
galaxy.yml Normal file
View File

@@ -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

7
meta/action_groups.yml Normal file
View File

@@ -0,0 +1,7 @@
acme:
- acme_inspect
- acme_certificate_revoke
- acme_certificate
- acme_account
- acme_account_facts
- acme_account_info

6
meta/routing.yml Normal file
View File

@@ -0,0 +1,6 @@
plugin_routing:
modules:
acme_account_facts:
deprecation:
removal_date: TBD
warning_text: see plugin documentation for details

View File

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# 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 ]
'''

View File

@@ -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"
'''

View File

1028
plugins/module_utils/acme.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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(<function>) 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()

View File

View File

@@ -0,0 +1,278 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# 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()

View File

@@ -0,0 +1 @@
acme_account_info.py

View File

@@ -0,0 +1,300 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018 Felix Fontein <felix@fontein.de>
# 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()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# 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()

View File

@@ -0,0 +1,298 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018 Felix Fontein <felix@fontein.de>
# 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()

View File

@@ -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": "<https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;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()

View File

@@ -0,0 +1,349 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2018, Felix Fontein <felix@fontein.de>
# 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()

View File

@@ -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 <domain>.<tld> only the www.<domain>.<tld> value
is accepted. If the CN of the certificate is www.<domain>.<tld> only the <domain>.<tld> 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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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 <luks-name> out of lsblk output in format 'crypt <luks-name>'
# regex takes care of any possible blank characters
LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
# used to get </luks/device> out of lsblk output
# in format 'device: </luks/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-<UUID>').
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()

View File

@@ -0,0 +1,589 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
# 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()

View File

@@ -0,0 +1,490 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
# 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()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,861 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# 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()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,665 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# 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()

View File

@@ -0,0 +1,414 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Thom Wiggers <ansible@thomwiggers.nl>
# 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 <path> <bits>
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()

View File

@@ -0,0 +1,467 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Guillaume Delpierre <gde@llew.me>
# 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()

View File

@@ -0,0 +1,933 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
# 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()

View File

@@ -0,0 +1,649 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# 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()

View File

@@ -0,0 +1,471 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
# 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()

782
plugins/modules/x509_crl.py Normal file
View File

@@ -0,0 +1,782 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Felix Fontein <felix@fontein.de>
# 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()

View File

@@ -0,0 +1,280 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
# 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()

1
tests/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
output/

View File

@@ -0,0 +1,2 @@
shippable/cloud/group1
cloud/acme

View File

@@ -0,0 +1,2 @@
dependencies:
- setup_acme

View File

@@ -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

View File

@@ -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', '>=')

View File

@@ -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.'

View File

@@ -0,0 +1,2 @@
shippable/cloud/group1
cloud/acme

View File

@@ -0,0 +1,2 @@
dependencies:
- setup_acme

View File

@@ -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

View File

@@ -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', '>=')

View File

@@ -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

View File

@@ -0,0 +1,2 @@
shippable/cloud/group1
cloud/acme

View File

@@ -0,0 +1,2 @@
dependencies:
- setup_acme

View File

@@ -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

View File

@@ -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', '>=')

View File

@@ -0,0 +1 @@
../../setup_acme/tasks/obtain-cert.yml

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
shippable/cloud/group1
cloud/acme

View File

@@ -0,0 +1,2 @@
dependencies:
- setup_acme

View File

@@ -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

View File

@@ -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', '>=')

View File

@@ -0,0 +1 @@
../../setup_acme/tasks/obtain-cert.yml

View File

@@ -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

View File

@@ -0,0 +1,2 @@
shippable/cloud/group1
cloud/acme

View File

@@ -0,0 +1,2 @@
dependencies:
- setup_acme

View File

@@ -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', '>=')

View File

@@ -0,0 +1 @@
../../setup_acme/tasks/obtain-cert.yml

View File

@@ -0,0 +1,2 @@
shippable/cloud/group1
cloud/acme

View File

@@ -0,0 +1,2 @@
dependencies:
- setup_acme

View File

@@ -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

View File

@@ -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', '>=')

View File

@@ -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 }}"

View File

@@ -0,0 +1,2 @@
shippable/posix/group1
skip/aix

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

File diff suppressed because it is too large Load Diff

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -0,0 +1,2 @@
dependencies:
- setup_remote_tmp_dir

View File

@@ -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', '>=')

View File

@@ -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

View File

@@ -0,0 +1,2 @@
---
# defaults file for test_ecs_certificate

View File

@@ -0,0 +1,3 @@
dependencies:
- prepare_tests
- setup_openssl

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -0,0 +1,2 @@
---
# defaults file for test_ecs_domain

View File

@@ -0,0 +1,2 @@
dependencies:
- prepare_tests

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More