Skip to content

Commit

Permalink
Add support for CRLs in DER format.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Apr 13, 2020
1 parent 02b536b commit 669c97a
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 12 deletions.
11 changes: 11 additions & 0 deletions plugins/module_utils/crypto/identify.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@
PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY'


def identify_pem_format(content):
'''Given the contents of a binary file, tests whether this could be a PEM file.'''
try:
lines = content.decode('utf-8').splitlines(False)
if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END):
return True
except UnicodeDecodeError:
pass
return False


def identify_private_key_format(content):
'''Given the contents of a private key file, identifies its format.'''
# See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85
Expand Down
69 changes: 60 additions & 9 deletions plugins/modules/x509_crl.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@
type: path
required: yes
format:
description:
- Whether the CRL file should be in PEM or DER format.
- If an existing CRL file does match everything but I(format), it will be converted to the correct format
instead of regenerated.
type: str
choices: [pem, der]
default: pem
privatekey_path:
description:
- Path to the CA's private key to use when signing the CRL.
Expand Down Expand Up @@ -266,6 +275,12 @@
returned: changed or success
type: str
sample: /path/to/my-ca.pem
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.
Expand Down Expand Up @@ -340,12 +355,16 @@
type: bool
sample: no
crl:
description: The (current or generated) CRL's content.
description:
- The (current or generated) CRL's content.
- Will be the CRL itself if I(format) is C(pem), and Base64 of the
CRL if I(format) is C(der).
returned: if I(state) is C(present) and I(return_content) is C(yes)
type: str
'''


import base64
import os
import traceback

Expand Down Expand Up @@ -387,6 +406,10 @@
cryptography_get_signature_algorithm_oid_from_crl,
)

from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
identify_pem_format,
)

MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'

CRYPTOGRAPHY_IMP_ERR = None
Expand Down Expand Up @@ -423,6 +446,8 @@ def __init__(self, module):
module.check_mode
)

self.format = module.params['format']

self.update = module.params['mode'] == 'update'
self.ignore_timestamps = module.params['ignore_timestamps']
self.return_content = module.params['return_content']
Expand Down Expand Up @@ -514,11 +539,18 @@ def __init__(self, module):
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
self.actual_format = 'pem' if identify_pem_format(data) else 'der'
if self.actual_format == 'pem':
self.crl = x509.load_pem_x509_crl(data, default_backend())
if self.return_content:
self.crl_content = data
else:
self.crl = x509.load_der_x509_crl(data, default_backend())
if self.return_content:
self.crl_content = base64.b64encode(data)
except Exception as dummy:
self.crl_content = None
self.actual_format = self.format

def remove(self):
if self.backup:
Expand Down Expand Up @@ -549,7 +581,7 @@ def _compress_entry(self, entry):
entry['invalidity_date_critical'],
)

def check(self, perms_required=True):
def check(self, perms_required=True, ignore_conversion=True):
"""Ensure the resource is in its desired state."""

state_and_perms = super(CRL, self).check(self.module, perms_required)
Expand Down Expand Up @@ -584,6 +616,9 @@ def check(self, perms_required=True):
if old_entries != new_entries:
return False

if self.format != self.actual_format and not ignore_conversion:
return False

return True

def _generate_crl(self):
Expand Down Expand Up @@ -631,13 +666,27 @@ def _generate_crl(self):
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)
if self.format == 'pem':
return self.crl.public_bytes(Encoding.PEM)
else:
return self.crl.public_bytes(Encoding.DER)

def generate(self):
if not self.check(perms_required=False) or self.force:
result = None
if not self.check(perms_required=False, ignore_conversion=True) or self.force:
result = self._generate_crl()
elif not self.check(perms_required=False, ignore_conversion=False) and self.crl:
if self.format == 'pem':
result = self.crl.public_bytes(Encoding.PEM)
else:
result = self.crl.public_bytes(Encoding.DER)

if result is not None:
if self.return_content:
self.crl_content = result
if self.format == 'pem':
self.crl_content = result
else:
self.crl_content = base64.b64encode(result)
if self.backup:
self.backup_file = self.module.backup_local(self.path)
write_file(self.module, result)
Expand All @@ -652,6 +701,7 @@ def dump(self, check_mode=False):
'changed': self.changed,
'filename': self.path,
'privatekey': self.privatekey_path,
'format': self.format,
'last_update': None,
'next_update': None,
'digest': None,
Expand Down Expand Up @@ -704,6 +754,7 @@ def main():
force=dict(type='bool', default=False),
backup=dict(type='bool', default=False),
path=dict(type='path', required=True),
format=dict(type='str', default='pem', choices=['pem', 'der']),
privatekey_path=dict(type='path'),
privatekey_content=dict(type='str'),
privatekey_passphrase=dict(type='str', no_log=True),
Expand Down Expand Up @@ -760,7 +811,7 @@ def main():
if module.params['state'] == 'present':
if module.check_mode:
result = crl.dump(check_mode=True)
result['changed'] = module.params['force'] or not crl.check()
result['changed'] = module.params['force'] or not crl.check() or not crl.check(ignore_conversion=False)
module.exit_json(**result)

crl.generate()
Expand Down
22 changes: 20 additions & 2 deletions plugins/modules/x509_crl_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
type: path
content:
description:
- Content of the X.509 certificate in PEM format.
- Content of the X.509 CRL in PEM format, or Base64-encoded X.509 CRL.
- Either I(path) or I(content) must be specified, but not both.
type: str
Expand All @@ -51,6 +51,12 @@
'''

RETURN = r'''
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.
Expand Down Expand Up @@ -127,6 +133,7 @@
'''


import base64
import traceback

from distutils.version import LooseVersion
Expand All @@ -153,6 +160,10 @@
cryptography_get_signature_algorithm_oid_from_crl,
)

from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
identify_pem_format,
)

# crypto_utils

MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
Expand Down Expand Up @@ -198,15 +209,22 @@ def __init__(self, module):
self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
else:
data = self.content.encode('utf-8')
if not identify_pem_format(data):
data = base64.b64decode(self.content)

self.crl_pem = identify_pem_format(data)
try:
self.crl = x509.load_pem_x509_crl(data, default_backend())
if self.crl_pem:
self.crl = x509.load_pem_x509_crl(data, default_backend())
else:
self.crl = x509.load_der_x509_crl(data, default_backend())
except Exception as e:
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))

def get_info(self):
result = {
'changed': False,
'format': 'pem' if self.crl_pem else 'der',
'last_update': None,
'next_update': None,
'digest': None,
Expand Down
99 changes: 99 additions & 0 deletions tests/integration/targets/x509_crl/tasks/impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
x509_crl_info:
content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") }}'
register: crl_1_info_2
- name: Retrieve CRL 1 infos via file content (Base64)
x509_crl_info:
content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") | b64encode }}'
register: crl_1_info_3
- name: Create CRL 1 (idempotent, check mode)
x509_crl:
path: '{{ output_dir }}/ca-crl1.crl'
Expand Down Expand Up @@ -124,6 +128,101 @@
- serial_number: 1234
revocation_date: 20191001000000Z
register: crl_1_idem_content
- name: Create CRL 1 (format, check mode)
x509_crl:
path: '{{ output_dir }}/ca-crl1.crl'
privatekey_path: '{{ output_dir }}/ca.key'
format: der
issuer:
CN: Ansible
last_update: 20191013000000Z
next_update: 20191113000000Z
revoked_certificates:
- path: '{{ output_dir }}/cert-1.pem'
revocation_date: 20191013000000Z
- path: '{{ output_dir }}/cert-2.pem'
revocation_date: 20191013000000Z
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
- serial_number: 1234
revocation_date: 20191001000000Z
check_mode: yes
register: crl_1_format_check
- name: Create CRL 1 (format)
x509_crl:
path: '{{ output_dir }}/ca-crl1.crl'
privatekey_path: '{{ output_dir }}/ca.key'
format: der
issuer:
CN: Ansible
last_update: 20191013000000Z
next_update: 20191113000000Z
revoked_certificates:
- path: '{{ output_dir }}/cert-1.pem'
revocation_date: 20191013000000Z
- path: '{{ output_dir }}/cert-2.pem'
revocation_date: 20191013000000Z
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
- serial_number: 1234
revocation_date: 20191001000000Z
register: crl_1_format
- name: Create CRL 1 (format, idempotent, check mode)
x509_crl:
path: '{{ output_dir }}/ca-crl1.crl'
privatekey_path: '{{ output_dir }}/ca.key'
format: der
issuer:
CN: Ansible
last_update: 20191013000000Z
next_update: 20191113000000Z
revoked_certificates:
- path: '{{ output_dir }}/cert-1.pem'
revocation_date: 20191013000000Z
- path: '{{ output_dir }}/cert-2.pem'
revocation_date: 20191013000000Z
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
- serial_number: 1234
revocation_date: 20191001000000Z
check_mode: yes
register: crl_1_format_idem_check
- name: Create CRL 1 (format, idempotent)
x509_crl:
path: '{{ output_dir }}/ca-crl1.crl'
privatekey_path: '{{ output_dir }}/ca.key'
format: der
issuer:
CN: Ansible
last_update: 20191013000000Z
next_update: 20191113000000Z
revoked_certificates:
- path: '{{ output_dir }}/cert-1.pem'
revocation_date: 20191013000000Z
- path: '{{ output_dir }}/cert-2.pem'
revocation_date: 20191013000000Z
reason: key_compromise
reason_critical: yes
invalidity_date: 20191012000000Z
- serial_number: 1234
revocation_date: 20191001000000Z
return_content: yes
register: crl_1_format_idem
- name: Retrieve CRL 1 infos via file
x509_crl_info:
path: '{{ output_dir }}/ca-crl1.crl'
register: crl_1_info_4
- name: Read ca-crl1.crl
slurp:
src: "{{ output_dir }}/ca-crl1.crl"
register: content
- name: Retrieve CRL 1 infos via file content (Base64)
x509_crl_info:
content: '{{ content.content }}'
register: crl_1_info_5

- name: Create CRL 2 (check mode)
x509_crl:
Expand Down
23 changes: 22 additions & 1 deletion tests/integration/targets/x509_crl/tests/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- name: Validate CRL 1 info
assert:
that:
- crl_1_info_1 == crl_1_info_2
- 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'
Expand Down Expand Up @@ -44,6 +44,27 @@
- 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
- crl_1_info_1 == crl_1_info_3

- name: Validate CRL 1
assert:
that:
- crl_1_format_check is changed
- crl_1_format is changed
- crl_1_format_idem_check is not changed
- crl_1_format_idem is not changed
- crl_1_info_4.format == 'der'
- crl_1_info_5.format == 'der'

- name: Read ca-crl1.crl
slurp:
src: "{{ output_dir }}/ca-crl1.crl"
register: content
- name: Validate CRL 1 Base64 content
assert:
that:
- crl_1_format_idem.crl | b64decode == content.content | b64decode

- name: Validate CRL 2
assert:
Expand Down

0 comments on commit 669c97a

Please sign in to comment.