Add simple integration test for openssl_certificate (#29038)

* openssl_certificate: Fix parameter assertion in Python3

Parameter assertion in Python3 is broken. pyOpenSSL get_X() functions
returns b'' type string and tries to compare it with '' string, leading
to failure.

The error mentionned above has been fixed by sanitizing the inputs from
a user to the assert only backend.

Also, this error was hidden by the fact that the improper check method
was called in the generate() functions.

* Add simple integration test for openssl_certificate

* remove subject == issuer assertion

* run integration tests only on supported hosts

* change min supported version to 0.15.x

* Add test for more CSR fields

* also convert dict members to bytes

* fix version_compare

* openssl_{csr, certificate}: Fail if pyOpenSSL <= 0.15

Previous 0.13 pyOpenSSL was a C-binding, and required the parameter
passed to add_extention to be in ASN.1. This has changed with the move
to 0.14 and it is now all pythong and string based.

Previous the 0.15 release, the `get_extensions()` method didn't exist,
since the modules rely heavily on it we ensure pyOpenSSL version is at
last 0.15.0.

* check pyopenssl version in openssl_csr integration test
This commit is contained in:
MarkusTeufelberger 2017-09-13 23:39:32 +02:00 committed by Toshio Kuratomi
parent 9ad90de4bc
commit 2186b04934
7 changed files with 150 additions and 20 deletions

View file

@ -32,8 +32,8 @@ description:
want to receive a certificate with these properties is a CSR (Certificate Signing Request). want to receive a certificate with these properties is a CSR (Certificate Signing Request).
It uses the pyOpenSSL python library to interact with OpenSSL." It uses the pyOpenSSL python library to interact with OpenSSL."
requirements: requirements:
- python-pyOpenSSL >= 0.15 (if using C(selfsigned) provider) - python-pyOpenSSL >= 0.15 (if using C(selfsigned) or C(assertonly) provider)
- acme-tiny (if using the acme provider) - acme-tiny (if using the C(acme) provider)
options: options:
state: state:
default: "present" default: "present"
@ -301,7 +301,7 @@ import os
from ansible.module_utils import crypto as crypto_utils from ansible.module_utils import crypto as crypto_utils
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native, to_bytes
try: try:
import OpenSSL import OpenSSL
@ -408,12 +408,7 @@ class SelfSignedCertificate(Certificate):
cert.set_subject(self.csr.get_subject()) cert.set_subject(self.csr.get_subject())
cert.set_version(self.csr.get_version() - 1) cert.set_version(self.csr.get_version() - 1)
cert.set_pubkey(self.csr.get_pubkey()) cert.set_pubkey(self.csr.get_pubkey())
cert.add_extensions(self.csr.get_extensions())
try:
# NOTE: This is only available starting from pyOpenSSL >= 0.15
cert.add_extensions(self.csr.get_extensions())
except NameError as exc:
raise CertificateError('You need to have PyOpenSSL>= 0.15 to generate public keys')
cert.sign(self.privatekey, self.digest) cert.sign(self.privatekey, self.digest)
self.certificate = cert self.certificate = cert
@ -467,6 +462,24 @@ class AssertOnlyCertificate(Certificate):
self.invalid_at = module.params['invalid_at'] self.invalid_at = module.params['invalid_at']
self.valid_in = module.params['valid_in'] self.valid_in = module.params['valid_in']
self.message = [] self.message = []
self._sanitize_inputs()
def _sanitize_inputs(self):
"""Ensure inputs are properly sanitized before comparison."""
for param in ['signature_algorithms', 'keyUsage', 'extendedKeyUsage',
'subjectAltName', 'subject', 'issuer', 'notBefore',
'notAfter', 'valid_at', 'invalid_at']:
attr = getattr(self, param)
if isinstance(attr, list):
setattr(self, param, [to_bytes(item) for item in attr])
elif isinstance(attr, tuple):
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
elif isinstance(attr, dict):
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
elif isinstance(attr, str):
setattr(self, param, to_bytes(attr))
def assertonly(self): def assertonly(self):
@ -606,7 +619,8 @@ class AssertOnlyCertificate(Certificate):
self.assertonly() self.assertonly()
if self.privatekey_path and not self.check(self.module, perms_required=False): if self.privatekey_path and \
not super(AssertOnlyCertificate, self).check(module, perms_required=False):
self.message.append( self.message.append(
'Certificate %s and private key %s does not match' % (self.path, self.privatekey_path) 'Certificate %s and private key %s does not match' % (self.path, self.privatekey_path)
) )
@ -740,6 +754,11 @@ def main():
if not pyopenssl_found: if not pyopenssl_found:
module.fail_json(msg='The python pyOpenSSL library is required') module.fail_json(msg='The python pyOpenSSL library is required')
if module.params['provider'] in ['selfsigned', 'assertonly']:
try:
getattr(crypto.X509Req, 'get_extensions')
except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
base_dir = os.path.dirname(module.params['path']) base_dir = os.path.dirname(module.params['path'])
if not os.path.isdir(base_dir): if not os.path.isdir(base_dir):

View file

@ -26,7 +26,7 @@ description:
Note: At least one of common_name or subject_alt_name must be specified. Note: At least one of common_name or subject_alt_name must be specified.
This module uses file common arguments to specify generated file permissions." This module uses file common arguments to specify generated file permissions."
requirements: requirements:
- "python-pyOpenSSL" - "python-pyOpenSSL >= 0.15"
options: options:
state: state:
required: false required: false
@ -430,6 +430,11 @@ def main():
if not pyopenssl_found: if not pyopenssl_found:
module.fail_json(msg='the python pyOpenSSL module is required') module.fail_json(msg='the python pyOpenSSL module is required')
try:
getattr(crypto.X509Req, 'get_extensions')
except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
base_dir = os.path.dirname(module.params['path']) base_dir = os.path.dirname(module.params['path'])
if not os.path.isdir(base_dir): if not os.path.isdir(base_dir):
module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir) module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir)

View file

@ -0,0 +1,2 @@
posix/ci/group1
destructive

View file

@ -0,0 +1,2 @@
dependencies:
- setup_openssl

View file

@ -0,0 +1,74 @@
- block:
- name: Generate privatekey
openssl_privatekey:
path: '{{ output_dir }}/privatekey.pem'
- name: Generate CSR
openssl_csr:
path: '{{ output_dir }}/csr.csr'
privatekey_path: '{{ output_dir }}/privatekey.pem'
commonName: 'www.ansible.com'
- name: Generate selfsigned certificate
openssl_certificate:
path: '{{ output_dir }}/cert.pem'
csr_path: '{{ output_dir }}/csr.csr'
privatekey_path: '{{ output_dir }}/privatekey.pem'
provider: selfsigned
selfsigned_digest: sha256
- name: Check selfsigned certificate
openssl_certificate:
path: '{{ output_dir }}/cert.pem'
privatekey_path: '{{ output_dir }}/privatekey.pem'
provider: assertonly
has_expired: False
version: 3
signature_algorithms:
- sha256WithRSAEncryption
- sha256WithECDSAEncryption
- name: Generate privatekey2
openssl_privatekey:
path: '{{ output_dir }}/privatekey2.pem'
- name: Generate CSR2
openssl_csr:
C: US
ST: California
L: Los Angeles
O: ACME Inc.
OU: Roadrunner pest control
path: '{{ output_dir }}/csr2.csr'
privatekey_path: '{{ output_dir }}/privatekey2.pem'
CN: 'www.example.com'
- name: Generate selfsigned certificate2
openssl_certificate:
path: '{{ output_dir }}/cert2.pem'
csr_path: '{{ output_dir }}/csr2.csr'
privatekey_path: '{{ output_dir }}/privatekey2.pem'
provider: selfsigned
selfsigned_digest: sha256
- name: Check selfsigned certificate2
openssl_certificate:
path: '{{ output_dir }}/cert2.pem'
privatekey_path: '{{ output_dir }}/privatekey2.pem'
provider: assertonly
has_expired: False
version: 3
signature_algorithms:
- sha256WithRSAEncryption
- sha256WithECDSAEncryption
subject:
CN: www.example.com
C: US
ST: California
L: Los Angeles
O: ACME Inc.
OU: Roadrunner pest control
- import_tasks: ../tests/validate.yml
when: pyopenssl_version.stdout|version_compare('0.15', '>=')

View file

@ -0,0 +1,25 @@
- name: Validate certificate (test - privatekey modulus)
shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey.pem | openssl md5'
register: privatekey_modulus
- name: Validate certificate (test - certificate modulus)
shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/cert.pem | openssl md5'
register: cert_modulus
- name: Validate certificate (assert)
assert:
that:
- cert_modulus.stdout == privatekey_modulus.stdout
- name: Validate certificate2 (test - privatekey modulus)
shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey2.pem | openssl md5'
register: privatekey2_modulus
- name: Validate certificate2 (test - certificate modulus)
shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/cert2.pem | openssl md5'
register: cert2_modulus
- name: Validate certificate2 (assert)
assert:
that:
- cert2_modulus.stdout == privatekey2_modulus.stdout

View file

@ -1,11 +1,14 @@
- name: Generate privatekey - block:
openssl_privatekey: - name: Generate privatekey
path: '{{ output_dir }}/privatekey.pem' openssl_privatekey:
path: '{{ output_dir }}/privatekey.pem'
- name: Generate CSR - name: Generate CSR
openssl_csr: openssl_csr:
path: '{{ output_dir }}/csr.csr' path: '{{ output_dir }}/csr.csr'
privatekey_path: '{{ output_dir }}/privatekey.pem' privatekey_path: '{{ output_dir }}/privatekey.pem'
commonName: 'www.ansible.com' commonName: 'www.ansible.com'
- import_tasks: ../tests/validate.yml - import_tasks: ../tests/validate.yml
when: pyopenssl_version.stdout|version_compare('0.15', '>=')