Add support for CRLs in DER format. (#29)

pull/50/head
Felix Fontein 2020-05-15 09:57:07 +02:00 committed by GitHub
parent 9e5969a644
commit de3c99eeac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 212 additions and 12 deletions

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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:

View File

@ -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: