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'
|
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):
|
def identify_private_key_format(content):
|
||||||
'''Given the contents of a private key file, identifies its format.'''
|
'''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
|
# See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85
|
||||||
|
|
|
@ -60,6 +60,15 @@ options:
|
||||||
type: path
|
type: path
|
||||||
required: yes
|
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:
|
privatekey_path:
|
||||||
description:
|
description:
|
||||||
- Path to the CA's private key to use when signing the CRL.
|
- Path to the CA's private key to use when signing the CRL.
|
||||||
|
@ -263,6 +272,12 @@ privatekey:
|
||||||
returned: changed or success
|
returned: changed or success
|
||||||
type: str
|
type: str
|
||||||
sample: /path/to/my-ca.pem
|
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:
|
issuer:
|
||||||
description:
|
description:
|
||||||
- The CRL's issuer.
|
- The CRL's issuer.
|
||||||
|
@ -337,12 +352,16 @@ revoked_certificates:
|
||||||
type: bool
|
type: bool
|
||||||
sample: no
|
sample: no
|
||||||
crl:
|
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)
|
returned: if I(state) is C(present) and I(return_content) is C(yes)
|
||||||
type: str
|
type: str
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import base64
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
@ -384,6 +403,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||||
cryptography_get_signature_algorithm_oid_from_crl,
|
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'
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
CRYPTOGRAPHY_IMP_ERR = None
|
||||||
|
@ -420,6 +443,8 @@ class CRL(OpenSSLObject):
|
||||||
module.check_mode
|
module.check_mode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.format = module.params['format']
|
||||||
|
|
||||||
self.update = module.params['mode'] == 'update'
|
self.update = module.params['mode'] == 'update'
|
||||||
self.ignore_timestamps = module.params['ignore_timestamps']
|
self.ignore_timestamps = module.params['ignore_timestamps']
|
||||||
self.return_content = module.params['return_content']
|
self.return_content = module.params['return_content']
|
||||||
|
@ -511,11 +536,18 @@ class CRL(OpenSSLObject):
|
||||||
try:
|
try:
|
||||||
with open(self.path, 'rb') as f:
|
with open(self.path, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
self.crl = x509.load_pem_x509_crl(data, default_backend())
|
self.actual_format = 'pem' if identify_pem_format(data) else 'der'
|
||||||
if self.return_content:
|
if self.actual_format == 'pem':
|
||||||
self.crl_content = data
|
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:
|
except Exception as dummy:
|
||||||
self.crl_content = None
|
self.crl_content = None
|
||||||
|
self.actual_format = self.format
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
if self.backup:
|
if self.backup:
|
||||||
|
@ -546,7 +578,7 @@ class CRL(OpenSSLObject):
|
||||||
entry['invalidity_date_critical'],
|
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."""
|
"""Ensure the resource is in its desired state."""
|
||||||
|
|
||||||
state_and_perms = super(CRL, self).check(self.module, perms_required)
|
state_and_perms = super(CRL, self).check(self.module, perms_required)
|
||||||
|
@ -581,6 +613,9 @@ class CRL(OpenSSLObject):
|
||||||
if old_entries != new_entries:
|
if old_entries != new_entries:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self.format != self.actual_format and not ignore_conversion:
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _generate_crl(self):
|
def _generate_crl(self):
|
||||||
|
@ -628,13 +663,27 @@ class CRL(OpenSSLObject):
|
||||||
crl = crl.add_revoked_certificate(revoked_cert.build(backend))
|
crl = crl.add_revoked_certificate(revoked_cert.build(backend))
|
||||||
|
|
||||||
self.crl = crl.sign(self.privatekey, self.digest, backend=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):
|
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()
|
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:
|
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:
|
if self.backup:
|
||||||
self.backup_file = self.module.backup_local(self.path)
|
self.backup_file = self.module.backup_local(self.path)
|
||||||
write_file(self.module, result)
|
write_file(self.module, result)
|
||||||
|
@ -649,6 +698,7 @@ class CRL(OpenSSLObject):
|
||||||
'changed': self.changed,
|
'changed': self.changed,
|
||||||
'filename': self.path,
|
'filename': self.path,
|
||||||
'privatekey': self.privatekey_path,
|
'privatekey': self.privatekey_path,
|
||||||
|
'format': self.format,
|
||||||
'last_update': None,
|
'last_update': None,
|
||||||
'next_update': None,
|
'next_update': None,
|
||||||
'digest': None,
|
'digest': None,
|
||||||
|
@ -701,6 +751,7 @@ def main():
|
||||||
force=dict(type='bool', default=False),
|
force=dict(type='bool', default=False),
|
||||||
backup=dict(type='bool', default=False),
|
backup=dict(type='bool', default=False),
|
||||||
path=dict(type='path', required=True),
|
path=dict(type='path', required=True),
|
||||||
|
format=dict(type='str', default='pem', choices=['pem', 'der']),
|
||||||
privatekey_path=dict(type='path'),
|
privatekey_path=dict(type='path'),
|
||||||
privatekey_content=dict(type='str'),
|
privatekey_content=dict(type='str'),
|
||||||
privatekey_passphrase=dict(type='str', no_log=True),
|
privatekey_passphrase=dict(type='str', no_log=True),
|
||||||
|
@ -757,7 +808,7 @@ def main():
|
||||||
if module.params['state'] == 'present':
|
if module.params['state'] == 'present':
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
result = crl.dump(check_mode=True)
|
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)
|
module.exit_json(**result)
|
||||||
|
|
||||||
crl.generate()
|
crl.generate()
|
||||||
|
|
|
@ -26,7 +26,7 @@ options:
|
||||||
type: path
|
type: path
|
||||||
content:
|
content:
|
||||||
description:
|
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.
|
- Either I(path) or I(content) must be specified, but not both.
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
|
@ -48,6 +48,12 @@ EXAMPLES = r'''
|
||||||
'''
|
'''
|
||||||
|
|
||||||
RETURN = 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:
|
issuer:
|
||||||
description:
|
description:
|
||||||
- The CRL's issuer.
|
- The CRL's issuer.
|
||||||
|
@ -124,6 +130,7 @@ revoked_certificates:
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import base64
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
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,
|
cryptography_get_signature_algorithm_oid_from_crl,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
|
||||||
|
identify_pem_format,
|
||||||
|
)
|
||||||
|
|
||||||
# crypto_utils
|
# crypto_utils
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
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))
|
self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
|
||||||
else:
|
else:
|
||||||
data = self.content.encode('utf-8')
|
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:
|
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:
|
except Exception as e:
|
||||||
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
|
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
result = {
|
result = {
|
||||||
'changed': False,
|
'changed': False,
|
||||||
|
'format': 'pem' if self.crl_pem else 'der',
|
||||||
'last_update': None,
|
'last_update': None,
|
||||||
'next_update': None,
|
'next_update': None,
|
||||||
'digest': None,
|
'digest': None,
|
||||||
|
|
|
@ -46,6 +46,10 @@
|
||||||
x509_crl_info:
|
x509_crl_info:
|
||||||
content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") }}'
|
content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") }}'
|
||||||
register: crl_1_info_2
|
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)
|
- name: Create CRL 1 (idempotent, check mode)
|
||||||
x509_crl:
|
x509_crl:
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
path: '{{ output_dir }}/ca-crl1.crl'
|
||||||
|
@ -124,6 +128,101 @@
|
||||||
- serial_number: 1234
|
- serial_number: 1234
|
||||||
revocation_date: 20191001000000Z
|
revocation_date: 20191001000000Z
|
||||||
register: crl_1_idem_content
|
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)
|
- name: Create CRL 2 (check mode)
|
||||||
x509_crl:
|
x509_crl:
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
- name: Validate CRL 1 info
|
- name: Validate CRL 1 info
|
||||||
assert:
|
assert:
|
||||||
that:
|
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.digest == 'ecdsa-with-SHA256'
|
||||||
- crl_1_info_1.issuer | length == 1
|
- crl_1_info_1.issuer | length == 1
|
||||||
- crl_1_info_1.issuer.commonName == 'Ansible'
|
- 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].reason_critical == false
|
||||||
- crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z'
|
- crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z'
|
||||||
- crl_1_info_1.revoked_certificates[2].serial_number == 1234
|
- 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
|
- name: Validate CRL 2
|
||||||
assert:
|
assert:
|
||||||
|
|
Loading…
Reference in New Issue