Add support for CRLs in DER format. (#29)
parent
9e5969a644
commit
de3c99eeac
|
@ -25,6 +25,17 @@ PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY')
|
|||
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
|
||||
|
|
|
@ -60,6 +60,15 @@ options:
|
|||
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.
|
||||
|
@ -263,6 +272,12 @@ privatekey:
|
|||
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.
|
||||
|
@ -337,12 +352,16 @@ revoked_certificates:
|
|||
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
|
||||
|
||||
|
@ -384,6 +403,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
|||
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
|
||||
|
@ -420,6 +443,8 @@ class CRL(OpenSSLObject):
|
|||
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']
|
||||
|
@ -511,11 +536,18 @@ class CRL(OpenSSLObject):
|
|||
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:
|
||||
|
@ -546,7 +578,7 @@ class CRL(OpenSSLObject):
|
|||
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)
|
||||
|
@ -581,6 +613,9 @@ class CRL(OpenSSLObject):
|
|||
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):
|
||||
|
@ -628,13 +663,27 @@ class CRL(OpenSSLObject):
|
|||
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)
|
||||
|
@ -649,6 +698,7 @@ class CRL(OpenSSLObject):
|
|||
'changed': self.changed,
|
||||
'filename': self.path,
|
||||
'privatekey': self.privatekey_path,
|
||||
'format': self.format,
|
||||
'last_update': None,
|
||||
'next_update': None,
|
||||
'digest': None,
|
||||
|
@ -701,6 +751,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),
|
||||
|
@ -757,7 +808,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()
|
||||
|
|
|
@ -26,7 +26,7 @@ options:
|
|||
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
|
||||
|
||||
|
@ -48,6 +48,12 @@ EXAMPLES = r'''
|
|||
'''
|
||||
|
||||
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.
|
||||
|
@ -124,6 +130,7 @@ revoked_certificates:
|
|||
'''
|
||||
|
||||
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
@ -150,6 +157,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
|||
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'
|
||||
|
@ -195,15 +206,22 @@ class CRLInfo(OpenSSLObject):
|
|||
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,
|
||||
|
|
|
@ -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'
|
||||
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue