Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions antsibull-nox.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ mypy_ansible_core_package = "ansible-core>=2.19.0"
mypy_config = ".mypy.ini"
mypy_extra_deps = [
"cryptography",
"pyasn1",
"types-mock",
"types-PyYAML",
"types-pyasn1",
]

[sessions.docs_check]
Expand Down Expand Up @@ -98,7 +100,7 @@ test_playbooks = ["tests/ee/all.yml"]
config.images.base_image.name = "docker.io/redhat/ubi9:latest"
config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/devel.tar.gz"
config.dependencies.ansible_runner.package_pip = "ansible-runner"
config.dependencies.python_interpreter.package_system = "python3.12 python3.12-pip python3.12-wheel python3.12-cryptography"
config.dependencies.python_interpreter.package_system = "python3.12 python3.12-pip python3.12-wheel python3.12-cryptography python3-pyasn1"
config.dependencies.python_interpreter.python_path = "/usr/bin/python3.12"
runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"}

Expand All @@ -109,7 +111,7 @@ test_playbooks = ["tests/ee/all.yml"]
config.images.base_image.name = "quay.io/rockylinux/rockylinux:9"
config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/stable-2.17.tar.gz"
config.dependencies.ansible_runner.package_pip = "ansible-runner"
config.dependencies.python_interpreter.package_system = "python3.11 python3.11-pip python3.11-wheel python3.11-cryptography"
config.dependencies.python_interpreter.package_system = "python3.11 python3.11-pip python3.11-wheel python3.11-cryptography python3-pyasn1"
config.dependencies.python_interpreter.python_path = "/usr/bin/python3.11"
runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"}

Expand All @@ -120,6 +122,6 @@ test_playbooks = ["tests/ee/all.yml"]
config.images.base_image.name = "quay.io/centos/centos:stream9"
config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/stable-2.18.tar.gz"
config.dependencies.ansible_runner.package_pip = "ansible-runner"
config.dependencies.python_interpreter.package_system = "python3.11 python3.11-pip python3.11-wheel python3.11-cryptography"
config.dependencies.python_interpreter.package_system = "python3.11 python3.11-pip python3.11-wheel python3.11-cryptography python3-pyasn1"
config.dependencies.python_interpreter.python_path = "/usr/bin/python3.11"
runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"}
2 changes: 2 additions & 0 deletions meta/ee-bindep.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ openssl [platform:dpkg]
openssl [platform:rpm]
python3-cryptography [platform:dpkg]
python3-cryptography [platform:rpm]
python3-pyasn1 [platform:dpkg]
python3-pyasn1 [platform:rpm]
python3-openssl [platform:dpkg]
2 changes: 2 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def create_certificates(session: nox.Session) -> None:
Regenerate some vendored certificates.
"""
session.install("cryptography<39.0.0") # we want support for SHA1 signatures
session.install("pyasn1>=0.4.8")
session.install("types-pyasn1>=0.4.8")
session.run("python", "tests/create-certificates.py")
session.warn(
"Note that you need to modify some values in tests/integration/targets/x509_certificate_info/tasks/impl.yml"
Expand Down
48 changes: 48 additions & 0 deletions plugins/doc_fragments/_module_csr.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ModuleDocFragment:
support: full
requirements:
- cryptography >= 3.3
- pyasn1 >= 0.4.8 (only if O(custom_extensions) are specified)
options:
digest:
description:
Expand Down Expand Up @@ -325,6 +326,53 @@ class ModuleDocFragment:
- privilege_withdrawn
- aa_compromise
version_added: 1.4.0
custom_extensions:
description:
- Allows to specify one or multiple custom extensions.
- The extension value must not be empty, unless O(custom_extensions[].skip_if_empty) is specified.
- In such case the extension will be skipped instead of throwing an error.
- Custom extensions require the V(pyasn1) Python package to be installed in the environment where the code is run.
type: list
elements: dict
suboptions:
critical:
description:
- Set the critical flag.
type: bool
default: false
oid:
description:
- The OID of the custom extension
- 'Example: V(1.3.6.1.4.1.34380.1.1.25).'
type: str
skip_if_empty:
description:
- Allow empty value to be specified. In such case the extension will be skipped when processing.
type: bool
default: false
value_type:
description:
- The type of the value. Only valid if O(custom_extensions[].value) is specified.
- 'Supported data types are: V(str), V(bool), V(int), V(real).'
type: str
default: str
value:
description:
- The value of the custom extension.
- Mutually exclusive with O(custom_extensions[].value_raw) and O(custom_extensions[].value_b64).
type: str
value_raw:
description:
- The raw value of the custom extension. Will be copied to the extension as-is.
- The value is always assumed to be an UTF8String
- Mutually exclusive with O(custom_extensions[].value) and O(custom_extensions[].value_b64).
type: str
value_b64:
description:
- The raw value of the custom extension, base64 encoded. Will be decoded, then copied to the extension as-is.
- Mutually exclusive with O(custom_extensions[].value) and O(custom_extensions[].value_raw).
type: str
version_added: 3.1.0
notes:
- If the certificate signing request already exists it will be checked whether subjectAltName, keyUsage, extendedKeyUsage
and basicConstraints only contain the requested values, whether OCSP Must Staple is as requested, and if the request was
Expand Down
110 changes: 110 additions & 0 deletions plugins/module_utils/_crypto/module_backends/csr.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

import base64
import binascii
import typing as t

Expand Down Expand Up @@ -70,6 +71,12 @@
except ImportError:
pass

try:
from pyasn1.codec.der import encoder
from pyasn1.type import char, univ
except ImportError:
pass


class CertificateSigningRequestError(OpenSSLObjectError):
pass
Expand Down Expand Up @@ -132,6 +139,78 @@ def parse_crl_distribution_points(
return result


def parse_custom_extensions(
*, module: AnsibleModule, custom_extensions: list[dict[str, t.Any]]
) -> list[tuple[cryptography.x509.UnrecognizedExtension, bool]]:
result = []
for index, custom_extension in enumerate(custom_extensions):
try:
if not custom_extension.get("oid"):
raise OpenSSLObjectError("oid must not be empty")
oid = custom_extension["oid"]

critical = custom_extension.get("critical", False)
if not isinstance(critical, bool):
raise OpenSSLObjectError(f"critical must be boolean (oid {oid})")

value = custom_extension.get("value", "")
value_type = custom_extension.get("value_type", "str")
value_raw = custom_extension.get("value_raw", "")
value_b64 = custom_extension.get("value_b64", "")

if not value and not value_raw and not value_b64:
if custom_extension.get("skip_if_empty"):
continue
raise OpenSSLObjectError(
f"neither of value, value_raw, or value_b64 was specified (oid {oid})"
)

if sum(bool(x) for x in [value, value_raw, value_b64]) != 1:
raise OpenSSLObjectError(
f"exactly one of value, value_raw, or value_b64 can be set (oid {oid})"
)

if value_raw or value_b64:
extension_value = (
value_raw.encode("UTF-8")
if value_raw
else base64.b64decode(value_b64)
)
else:
if value_type == "str":
extension_value = encoder.encode(char.UTF8String(value))
elif value_type == "bool":
value = value.strip().lower()
if value not in ["true", "false"]:
raise OpenSSLObjectError(
f"Unexpected bool value: {value} (oid {oid})"
)
extension_value = encoder.encode(univ.Boolean(value == "true"))
elif value_type == "int":
extension_value = encoder.encode(univ.Integer(int(value.strip())))
elif value_type == "real":
extension_value = encoder.encode(univ.Real(float(value.strip())))
else:
raise OpenSSLObjectError(
f"Data type of value unknown; supported types: str, bool, int, float (oid {oid}"
)

result.append(
(
cryptography.x509.UnrecognizedExtension(
oid=cryptography.x509.ObjectIdentifier(oid),
value=extension_value,
),
critical,
)
)
except (OpenSSLObjectError, ValueError) as e:
raise OpenSSLObjectError(
f"Error while parsing custom extension #{index}: {e}"
) from e
return result


class CertificateSigningRequestBackend:
def __init__(self, *, module: AnsibleModule) -> None:
self.module = module
Expand Down Expand Up @@ -185,6 +264,10 @@ def __init__(self, *, module: AnsibleModule) -> None:
self.crl_distribution_points: (
list[cryptography.x509.DistributionPoint] | None
) = None
self.custom_extensions: (
list[tuple[cryptography.x509.UnrecognizedExtension, bool]] | None
) = None

self.csr: cryptography.x509.CertificateSigningRequest | None = None
self.privatekey: CertificateIssuerPrivateKeyTypes | None = None

Expand Down Expand Up @@ -268,6 +351,14 @@ def __init__(self, *, module: AnsibleModule) -> None:
module=module, crl_distribution_points=crl_distribution_points
)

custom_extensions: list[dict[str, t.Any]] | None = module.params[
"custom_extensions"
]
if custom_extensions:
self.custom_extensions = parse_custom_extensions(
module=module, custom_extensions=custom_extensions
)

def _get_info(self, *, data: bytes | None) -> dict[str, t.Any]:
if data is None:
return {}
Expand Down Expand Up @@ -413,6 +504,10 @@ def generate_csr(self) -> None:
critical=False,
)

if self.custom_extensions:
for custom_extension, critical in self.custom_extensions:
csr = csr.add_extension(custom_extension, critical=critical)

# csr.sign() does not accept some digests we theoretically could have in digest.
# For that reason we use type t.Any here. csr.sign() will complain if
# the digest is not acceptable.
Expand Down Expand Up @@ -906,6 +1001,21 @@ def get_csr_argument_spec() -> ArgumentSpec:
"mutually_exclusive": [("full_name", "relative_name")],
"required_one_of": [("full_name", "relative_name", "crl_issuer")],
},
"custom_extensions": {
"type": "list",
"elements": "dict",
"options": {
"critical": {"type": "bool", "default": False},
"oid": {"type": "str"},
"skip_if_empty": {"type": "bool", "default": False},
"value_type": {"type": "str", "default": "str"},
"value": {"type": "str"},
"value_raw": {"type": "str"},
"value_b64": {"type": "str"},
},
"mutually_exclusive": [("value", "value_raw", "value_b64")],
"required_one_of": [("value", "value_raw", "value_b64")],
},
"select_crypto_backend": {
"type": "str",
"default": "auto",
Expand Down
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 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

pyasn1>=0.4.8
types-pyasn1>=0.4.8
1 change: 1 addition & 0 deletions tests/integration/requirements.txt
30 changes: 30 additions & 0 deletions tests/integration/targets/openssl_csr/tasks/impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,36 @@
authority_cert_issuer: '{{ value_for_authority_cert_issuer }}'
authority_cert_serial_number: 12345
select_crypto_backend: '{{ select_crypto_backend }}'
custom_extensions:
- oid: 1.3.6.1.4.1.34380.1.2.1
value: "DE" # stored as an UTF8String
critical: false
- oid: 1.3.6.1.4.1.34380.1.2.2
value: ""
critical: false
skip_if_empty: true # will not be stored to the CSR
- oid: 1.3.6.1.4.1.34380.1.2.3
value: 17
value_type: int
critical: false
- oid: 1.3.6.1.4.1.34380.1.2.4
value: 3.14
value_type: real
critical: false
- oid: 1.3.6.1.4.1.34380.1.2.5
value: true
value_type: bool
critical: false
- oid: 1.3.6.1.4.1.34380.1.2.6
value: false
value_type: bool
critical: false
- oid: 1.3.6.1.4.1.34380.1.2.7
value_raw: "DE" # stored as an OctetString (raw)
critical: false
- oid: 1.3.6.1.4.1.34380.1.2.8
value_b64: "REU=" # DE, base64 encoded, stored as an OctetString (raw)
critical: true
vars:
value_for_extended_key_usage:
- serverAuth # the same as "TLS Web Server Authentication"
Expand Down
23 changes: 23 additions & 0 deletions tests/integration/targets/openssl_csr/tests/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,29 @@
"IP:1.2.3.0/24",
"IP:::1:0:0/112",
]
- '"1.3.6.1.4.1.34380.1.2.1" in everything_info.extensions_by_oid'
- '"1.3.6.1.4.1.34380.1.2.2" not in everything_info.extensions_by_oid'
- '"1.3.6.1.4.1.34380.1.2.3" in everything_info.extensions_by_oid'
- '"1.3.6.1.4.1.34380.1.2.4" in everything_info.extensions_by_oid'
- '"1.3.6.1.4.1.34380.1.2.5" in everything_info.extensions_by_oid'
- '"1.3.6.1.4.1.34380.1.2.6" in everything_info.extensions_by_oid'
- '"1.3.6.1.4.1.34380.1.2.7" in everything_info.extensions_by_oid'
- '"1.3.6.1.4.1.34380.1.2.8" in everything_info.extensions_by_oid'
# values provided by csr_info are always base64 encoded
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.1"].critical == false
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.1"].value == "DAJERQ=="
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.3"].critical == false
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.3"].value == "AgER"
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.4"].critical == false
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.4"].value == "CQcDMzE0RS0y"
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.5"].critical == false
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.5"].value == "AQH/"
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.6"].critical == false
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.6"].value == "AQEA"
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.7"].critical == false
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.7"].value == "REU="
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.8"].critical == true
- everything_info.extensions_by_oid["1.3.6.1.4.1.34380.1.2.8"].value == "REU="

- name: "({{ select_crypto_backend }}) Verify Ed25519 and Ed448 tests"
ansible.builtin.assert:
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/targets/setup_openssl/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,24 @@
name: '{{ cryptography_package_name_python3 }}'
when: ansible_python_version is version('3.0', '>=')

- name: Install pyasn1 (Python 3 from system packages)
become: true
ansible.builtin.package:
name: '{{ pyasn1_package_name_python3 }}'
when: ansible_python_version is version('3.0', '>=')

- name: Install cryptography (Python 2 from system packages)
become: true
ansible.builtin.package:
name: '{{ cryptography_package_name }}'
when: ansible_python_version is version('3.0', '<')

- name: Install pyasn1 (Python 2 from system packages)
become: true
ansible.builtin.package:
name: '{{ pyasn1_package_name }}'
when: ansible_python_version is version('3.0', '<')

- name: Install from PyPi
when: ansible_os_family == "Darwin" or not target_system_python
block:
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/targets/setup_openssl/vars/Alpine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
openssl_package_name: openssl
cryptography_package_name: py-cryptography
cryptography_package_name_python3: py3-cryptography
pyasn1_package_name: py-asn1
pyasn1_package_name_python3: py3-asn1
2 changes: 2 additions & 0 deletions tests/integration/targets/setup_openssl/vars/Archlinux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
openssl_package_name: openssl
cryptography_package_name: python-cryptography
cryptography_package_name_python3: python-cryptography
pyasn1_package_name: python-pyasn1
pyasn1_package_name_python3: python-pyasn1
2 changes: 2 additions & 0 deletions tests/integration/targets/setup_openssl/vars/Debian.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
openssl_package_name: openssl
cryptography_package_name: python-cryptography
cryptography_package_name_python3: python3-cryptography
pyasn1_package_name: python-pyasn1
pyasn1_package_name_python3: python3-pyasn1
2 changes: 2 additions & 0 deletions tests/integration/targets/setup_openssl/vars/FreeBSD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
openssl_package_name: openssl
cryptography_package_name: py27-cryptography
cryptography_package_name_python3: "py{{ ansible_python.version.major }}{{ ansible_python.version.minor }}-cryptography"
pyasn1_package_name: py27-pyasn1
pyasn1_package_name_python3: "py{{ ansible_python.version.major }}{{ ansible_python.version.minor }}-pyasn1"
Loading
Loading