Add x509_crl_info filter (#558)

* Add x509_crl_info filter.

* Work around bugs in Ansible 2.9 and ansible-base 2.10.
pull/562/head
Felix Fontein 2022-12-31 07:56:34 +01:00 committed by GitHub
parent c08bae8308
commit c173449c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 652 additions and 0 deletions

View File

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
name: x509_crl_info
short_description: Retrieve information from X.509 CRLs in PEM format
version_added: 2.10.0
author:
- Felix Fontein (@felixfontein)
description:
- Provided a X.509 crl in PEM format, retrieve information.
- This is a filter version of the M(community.crypto.x509_crl_info) module.
options:
_input:
description:
- The content of the X.509 CRL in PEM format.
type: string
required: true
list_revoked_certificates:
description:
- If set to C(false), the list of revoked certificates is not included in the result.
- This is useful when retrieving information on large CRL files. Enumerating all revoked
certificates can take some time, including serializing the result as JSON, sending it to
the Ansible controller, and decoding it again.
type: bool
default: true
version_added: 1.7.0
extends_documentation_fragment:
- community.crypto.name_encoding
seealso:
- module: community.crypto.x509_crl_info
'''
EXAMPLES = '''
- name: Show the Organization Name of the CRL's subject
ansible.builtin.debug:
msg: >-
{{
(
lookup('ansible.builtin.file', '/path/to/cert.pem')
| community.crypto.x509_crl_info
).issuer.organizationName
}}
'''
RETURN = '''
_value:
description:
- Information on the CRL.
type: dict
contains:
format:
description:
- Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)).
returned: success
type: str
sample: pem
issuer:
description:
- The CRL's issuer.
- Note that for repeated values, only the last one will be returned.
- See I(name_encoding) for how IDNs are handled.
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 if I(list_revoked_certificates=true)
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.
- See I(name_encoding) for how IDNs are handled.
type: list
elements: str
sample: ["DNS:ca.example.org"]
issuer_critical:
description: Whether the certificate issuer extension is critical.
type: bool
sample: false
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: false
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: false
'''
import base64
import binascii
from ansible.errors import AnsibleFilterError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
identify_pem_format,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
get_crl_info,
)
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
def x509_crl_info_filter(data, name_encoding='ignore', list_revoked_certificates=True):
'''Extract information from X.509 PEM certificate.'''
if not isinstance(data, string_types):
raise AnsibleFilterError('The community.crypto.x509_crl_info input must be a text type, not %s' % type(data))
if not isinstance(name_encoding, string_types):
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding))
if not isinstance(list_revoked_certificates, bool):
raise AnsibleFilterError('The list_revoked_certificates option must be a boolean, not %s' % type(list_revoked_certificates))
name_encoding = to_native(name_encoding)
if name_encoding not in ('ignore', 'idna', 'unicode'):
raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding)
data = to_bytes(data)
if not identify_pem_format(data):
try:
data = base64.b64decode(to_native(data))
except (binascii.Error, TypeError, ValueError, UnicodeEncodeError) as e:
pass
module = FilterModuleMock({'name_encoding': name_encoding})
try:
return get_crl_info(module, content=data, list_revoked_certificates=list_revoked_certificates)
except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc))
class FilterModule(object):
'''Ansible jinja2 filters'''
def filters(self):
return {
'x509_crl_info': x509_crl_info_filter,
}

View File

@ -50,6 +50,10 @@ notes:
They are all in UTC.
seealso:
- module: community.crypto.x509_crl
- ref: community.crypto.x509_crl_info filter <ansible_collections.community.crypto.x509_crl_info_filter>
# - plugin: community.crypto.x509_crl_info
# plugin_type: filter
description: A filter variant of this module.
'''
EXAMPLES = r'''

View File

@ -0,0 +1,7 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
azp/generic/2
azp/posix/2
destructive

View File

@ -0,0 +1,8 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
dependencies:
- setup_openssl
- setup_remote_tmp_dir

View File

@ -0,0 +1,346 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Create CRL 1
x509_crl:
path: '{{ remote_tmp_dir }}/ca-crl1.crl'
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
issuer:
CN: Ansible
last_update: 20191013000000Z
next_update: 20191113000000Z
revoked_certificates:
- path: '{{ remote_tmp_dir }}/cert-1.pem'
revocation_date: 20191013000000Z
- path: '{{ remote_tmp_dir }}/cert-2.pem'
revocation_date: 20191013000000Z
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
- serial_number: 1234
revocation_date: 20191001000000Z
- name: Retrieve CRL 1 infos
set_fact:
crl_1_info_1: >-
{{ lookup('file', remote_tmp_dir ~ '/ca-crl1.crl') | community.crypto.x509_crl_info }}
- name: Retrieve CRL 1 infos
set_fact:
crl_1_info_2: >-
{{ lookup('file', remote_tmp_dir ~ '/ca-crl1.crl') | b64encode | community.crypto.x509_crl_info }}
- name: Validate CRL 1 info
assert:
that:
- crl_1_info_1.format == 'pem'
- crl_1_info_1.digest == 'ecdsa-with-SHA256'
- crl_1_info_1.issuer | length == 1
- crl_1_info_1.issuer.commonName == 'Ansible'
- crl_1_info_1.issuer_ordered | length == 1
- crl_1_info_1.last_update == '20191013000000Z'
- crl_1_info_1.next_update == '20191113000000Z'
- crl_1_info_1.revoked_certificates | length == 3
- crl_1_info_1.revoked_certificates[0].invalidity_date is none
- crl_1_info_1.revoked_certificates[0].invalidity_date_critical == false
- crl_1_info_1.revoked_certificates[0].issuer is none
- crl_1_info_1.revoked_certificates[0].issuer_critical == false
- crl_1_info_1.revoked_certificates[0].reason is none
- crl_1_info_1.revoked_certificates[0].reason_critical == false
- crl_1_info_1.revoked_certificates[0].revocation_date == '20191013000000Z'
- crl_1_info_1.revoked_certificates[0].serial_number == certificate_infos.results[0].serial_number
- crl_1_info_1.revoked_certificates[1].invalidity_date == '20191012000000Z'
- crl_1_info_1.revoked_certificates[1].invalidity_date_critical == false
- crl_1_info_1.revoked_certificates[1].issuer is none
- crl_1_info_1.revoked_certificates[1].issuer_critical == false
- crl_1_info_1.revoked_certificates[1].reason == 'key_compromise'
- crl_1_info_1.revoked_certificates[1].reason_critical == true
- crl_1_info_1.revoked_certificates[1].revocation_date == '20191013000000Z'
- crl_1_info_1.revoked_certificates[1].serial_number == certificate_infos.results[1].serial_number
- crl_1_info_1.revoked_certificates[2].invalidity_date is none
- crl_1_info_1.revoked_certificates[2].invalidity_date_critical == false
- crl_1_info_1.revoked_certificates[2].issuer is none
- crl_1_info_1.revoked_certificates[2].issuer_critical == false
- crl_1_info_1.revoked_certificates[2].reason is none
- crl_1_info_1.revoked_certificates[2].reason_critical == false
- crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z'
- crl_1_info_1.revoked_certificates[2].serial_number == 1234
- crl_1_info_1 == crl_1_info_2
- name: Recreate CRL 1 as DER file
x509_crl:
path: '{{ remote_tmp_dir }}/ca-crl1.crl'
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
format: der
issuer:
CN: Ansible
last_update: 20191013000000Z
next_update: 20191113000000Z
revoked_certificates:
- path: '{{ remote_tmp_dir }}/cert-1.pem'
revocation_date: 20191013000000Z
- path: '{{ remote_tmp_dir }}/cert-2.pem'
revocation_date: 20191013000000Z
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
- serial_number: 1234
revocation_date: 20191001000000Z
- name: Read ca-crl1.crl
slurp:
src: "{{ remote_tmp_dir }}/ca-crl1.crl"
register: content
- name: Retrieve CRL 1 infos from DER (raw bytes)
set_fact:
crl_1_info_4: >-
{{ content.content | b64decode | community.crypto.x509_crl_info }}
# Ansible 2.9 and ansible-base 2.10 on Python 2 mangle bytes, so do not run this on these versions
when: ansible_version.string is version('2.11', '>=') or ansible_python.version.major > 2
- name: Retrieve CRL 1 infos from DER (Base64 encoded)
set_fact:
crl_1_info_5: >-
{{ content.content | community.crypto.x509_crl_info }}
- name: Validate CRL 1
assert:
that:
- crl_1_info_4 is not defined or crl_1_info_4.format == 'der'
- crl_1_info_5.format == 'der'
- crl_1_info_4 is not defined or crl_1_info_4 == crl_1_info_5
- name: Create CRL 2
x509_crl:
path: '{{ remote_tmp_dir }}/ca-crl2.crl'
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
issuer_ordered:
- CN: Ansible
- CN: CRL
- countryName: US
- CN: Test
last_update: +0d
next_update: +0d
revoked_certificates:
- path: '{{ remote_tmp_dir }}/cert-2.pem'
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
ignore_timestamps: no
mode: update
return_content: yes
register: crl_2_change
- name: Retrieve CRL 2 infos
set_fact:
crl_2_info_1: >-
{{ lookup('file', remote_tmp_dir ~ '/ca-crl2.crl') | community.crypto.x509_crl_info(list_revoked_certificates=false) }}
- name: Create CRL 2 (changed order)
x509_crl:
path: '{{ remote_tmp_dir }}/ca-crl2.crl'
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
issuer_ordered:
- CN: Ansible
- countryName: US
- CN: CRL
- CN: Test
last_update: +0d
next_update: +0d
revoked_certificates:
- path: '{{ remote_tmp_dir }}/cert-2.pem'
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
ignore_timestamps: true
mode: update
return_content: yes
register: crl_2_change_order
- name: Retrieve CRL 2 infos again
set_fact:
crl_2_info_2: >-
{{ lookup('file', remote_tmp_dir ~ '/ca-crl2.crl') | community.crypto.x509_crl_info(list_revoked_certificates=false) }}
- name: Validate CRL 2 info
assert:
that:
- "'revoked_certificates' not in crl_2_info_1"
- >
crl_2_info_1.issuer_ordered == [
['commonName', 'Ansible'],
['commonName', 'CRL'],
['countryName', 'US'],
['commonName', 'Test'],
]
- >
crl_2_info_2.issuer_ordered == [
['commonName', 'Ansible'],
['countryName', 'US'],
['commonName', 'CRL'],
['commonName', 'Test'],
]
- name: Create CRL 3
x509_crl:
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
issuer:
CN: Ansible
last_update: +0d
next_update: +0d
revoked_certificates:
- serial_number: 1234
revocation_date: 20191001000000Z
# * cryptography < 2.1 strips username and password from URIs. To avoid problems, we do
# not pass usernames and passwords for URIs when the cryptography version is < 2.1.
# * Python 3.5 before 3.5.8 rc 1 has a bug in urllib.parse.urlparse() that results in an
# error if a Unicode netloc has a username or password included.
# (https://github.com/ansible-collections/community.crypto/pull/436#issuecomment-1101737134)
# This affects the Python 3.5 included in Ansible 2.9's default test container; to avoid
# this, we also do not pass usernames and passwords for Python 3.5.
issuer:
- "DNS:ca.example.org"
- "DNS:ffóò.ḃâŗ.çøṁ"
- "email:foo@ḃâŗ.çøṁ"
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar"
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de"
- "URI:https://straße.de:8080"
- "URI:http://gefäß.org"
- "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1"
issuer_critical: true
register: crl_3
- name: Create CRL 3 (IDNA encoding)
x509_crl:
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
issuer:
CN: Ansible
last_update: +0d
next_update: +0d
revoked_certificates:
- serial_number: 1234
revocation_date: 20191001000000Z
issuer:
- "DNS:ca.example.org"
- "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n"
- "email:foo@xn--2ca8uh37e.xn--7ca8a981n"
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar"
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.xn--strae-oqa.de"
- "URI:https://xn--strae-oqa.de:8080"
- "URI:http://xn--gef-7kay.org"
- "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}xn--4ca:1"
issuer_critical: true
ignore_timestamps: true
name_encoding: idna
register: crl_3_idna
- name: Create CRL 3 (Unicode encoding)
x509_crl:
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
issuer:
CN: Ansible
last_update: +0d
next_update: +0d
revoked_certificates:
- serial_number: 1234
revocation_date: 20191001000000Z
issuer:
- "DNS:ca.example.org"
- "DNS:ffóò.ḃâŗ.çøṁ"
- "email:foo@ḃâŗ.çøṁ"
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar"
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de"
- "URI:https://straße.de:8080"
- "URI:http://gefäß.org"
- "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1"
issuer_critical: true
ignore_timestamps: true
name_encoding: unicode
register: crl_3_unicode
- name: Retrieve CRL 3 infos
set_fact:
crl_3_info: >-
{{ lookup('file', remote_tmp_dir ~ '/ca-crl3.crl') | community.crypto.x509_crl_info(list_revoked_certificates=true) }}
crl_3_info_idna: >-
{{ lookup('file', remote_tmp_dir ~ '/ca-crl3.crl') | community.crypto.x509_crl_info(list_revoked_certificates=true, name_encoding='idna') }}
crl_3_info_unicode: >-
{{ lookup('file', remote_tmp_dir ~ '/ca-crl3.crl') | community.crypto.x509_crl_info(list_revoked_certificates=true, name_encoding='unicode') }}
- name: Validate CRL 3 info
assert:
that:
- crl_3.revoked_certificates == crl_3_info.revoked_certificates
- crl_3_idna.revoked_certificates == crl_3_info_idna.revoked_certificates
- crl_3_unicode.revoked_certificates == crl_3_info_unicode.revoked_certificates
- name: Get invalid CRL info
set_fact:
result: >-
{{ [] | community.crypto.x509_crl_info }}
ignore_errors: true
register: output
- name: Check that task failed and error message is OK
assert:
that:
- output is failed
- output.msg is search("^The community.crypto.x509_crl_info input must be a text type, not <(?:class|type) 'list'>$")
- name: Get invalid CRL info
set_fact:
result: >-
{{ 'foo' | community.crypto.x509_crl_info }}
ignore_errors: true
register: output
- name: Check that task failed and error message is OK
assert:
that:
- output is failed
- output.msg is search("^Error while decoding CRL")
- name: Get invalid CRL info
set_fact:
result: >-
{{ 'foo' | community.crypto.x509_crl_info(name_encoding=[]) }}
ignore_errors: true
register: output
- name: Check that task failed and error message is OK
assert:
that:
- output is failed
- output.msg is search("^The name_encoding option must be of a text type, not <(?:class|type) 'list'>$")
- name: Get invalid name_encoding parameter
set_fact:
result: >-
{{ 'bar' | community.crypto.x509_crl_info(name_encoding='foo') }}
ignore_errors: true
register: output
- name: Check that task failed and error message is OK
assert:
that:
- output is failed
- output.msg is search("^The name_encoding option must be one of the values \"ignore\", \"idna\", or \"unicode\", not \"foo\"$")
- name: Get invalid list_revoked_certificates parameter
set_fact:
result: >-
{{ 'bar' | community.crypto.x509_crl_info(list_revoked_certificates=[]) }}
ignore_errors: true
register: output
- name: Check that task failed and error message is OK
assert:
that:
- output is failed
- output.msg is search("^The list_revoked_certificates option must be a boolean, not <(?:class|type) 'list'>$")

View File

@ -0,0 +1,91 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
- name: Make sure the Python idna library is installed
pip:
name: idna
state: present
- set_fact:
certificates:
- name: ca
subject:
commonName: Ansible
is_ca: yes
- name: ca-2
subject:
commonName: Ansible Other CA
is_ca: yes
- name: cert-1
subject_alt_name:
- DNS:ansible.com
- name: cert-2
subject_alt_name:
- DNS:example.com
- name: cert-3
subject_alt_name:
- DNS:example.org
- IP:1.2.3.4
- name: cert-4
subject_alt_name:
- DNS:test.ansible.com
- DNS:b64.ansible.com
- name: Generate private keys
openssl_privatekey:
path: '{{ remote_tmp_dir }}/{{ item.name }}.key'
type: ECC
curve: secp256r1
loop: "{{ certificates }}"
- name: Generate CSRs
openssl_csr:
path: '{{ remote_tmp_dir }}/{{ item.name }}.csr'
privatekey_path: '{{ remote_tmp_dir }}/{{ item.name }}.key'
subject: "{{ item.subject | default(omit) }}"
subject_alt_name: "{{ item.subject_alt_name | default(omit) }}"
basic_constraints: "{{ 'CA:TRUE' if item.is_ca | default(false) else omit }}"
use_common_name_for_san: no
loop: "{{ certificates }}"
- name: Generate CA certificates
x509_certificate:
path: '{{ remote_tmp_dir }}/{{ item.name }}.pem'
csr_path: '{{ remote_tmp_dir }}/{{ item.name }}.csr'
privatekey_path: '{{ remote_tmp_dir }}/{{ item.name }}.key'
provider: selfsigned
loop: "{{ certificates }}"
when: item.is_ca | default(false)
- name: Generate other certificates
x509_certificate:
path: '{{ remote_tmp_dir }}/{{ item.name }}.pem'
csr_path: '{{ remote_tmp_dir }}/{{ item.name }}.csr'
provider: ownca
ownca_path: '{{ remote_tmp_dir }}/ca.pem'
ownca_privatekey_path: '{{ remote_tmp_dir }}/ca.key'
loop: "{{ certificates }}"
when: not (item.is_ca | default(false))
- name: Get certificate infos
x509_certificate_info:
path: '{{ remote_tmp_dir }}/{{ item }}.pem'
loop:
- cert-1
- cert-2
- cert-3
- cert-4
register: certificate_infos
- block:
- name: Running tests
include_tasks: impl.yml
when: cryptography_version.stdout is version('1.2', '>=')