Repository: ansible/ansible

Issue:
Add OCSP Must Staple support for openssl_csr
##### ISSUE TYPE
Feature Idea

##### COMPONENT NAME
openssl_csr

##### ANSIBLE VERSION
```
2.4, devel
```

##### SUMMARY
It would be nice if the `openssl_csr` module could generate CSRs which include the OCSP Must Staple (id-pe-tlsfeature, 1.3.6.1.5.5.7.1.24) extension. The RFC is [here](https://tools.ietf.org/html/rfc7633), an explanation can be found [here](https://scotthelme.co.uk/ocsp-must-staple/).

The implementation should be pretty simple, see for example here: https://github.com/certbot/certbot/blob/d2df6880e5601af2813a7455abb265686a4c8b83/certbot/crypto_util.py#L123



Discussion:
Files identified in the description:
* [lib/ansible/modules/crypto/openssl_csr.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/crypto/openssl_csr.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Spredzy
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/crypto/openssl_csr.py b/lib/ansible/modules/crypto/openssl_csr.py
--- a/lib/ansible/modules/crypto/openssl_csr.py
+++ b/lib/ansible/modules/crypto/openssl_csr.py
@@ -22,7 +22,8 @@
 description:
     - "This module allows one to (re)generate OpenSSL certificate signing requests.
        It uses the pyOpenSSL python library to interact with openssl. This module supports
-       the subjectAltName as well as the keyUsage and extendedKeyUsage extensions."
+       the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple
+       extensions."
 requirements:
     - "python-pyOpenSSL >= 0.15"
 options:
@@ -148,12 +149,29 @@
         description:
             - Should the basicConstraints extension be considered as critical
         version_added: 2.5
+    ocsp_must_staple:
+        required: false
+        aliases: ['ocspMustStaple']
+        description:
+            - Indicates that the certificate should contain the OCSP Must Staple
+              extension (U(https://tools.ietf.org/html/rfc7633)).
+        version_added: 2.5
+    ocsp_must_staple_critical:
+        required: false
+        aliases: [ 'ocspMustStaple_critical' ]
+        description:
+            - Should the OCSP Must Staple extension be considered as critical
+            - "Warning: according to the RFC, this extension should not be marked
+               as critical, as old clients not knowing about OCSP Must Staple
+               are required to reject such certificates
+               (see U(https://tools.ietf.org/html/rfc7633#section-4))."
+        version_added: 2.5
 extends_documentation_fragment: files
 
 notes:
     - "If the certificate signing request already exists it will be checked whether subjectAltName,
-       keyUsage and extendedKeyUsage only contain the requested values and if the request was signed
-       by the given private key"
+       keyUsage, extendedKeyUsage and basicConstraints only contain the requested values, whether
+       OCSP Must Staple is as requested, and if the request was signed by the given private key."
 '''
 
 
@@ -204,6 +222,13 @@
       - keyAgreement
     extended_key_usage:
       - clientAuth
+
+# Generate an OpenSSL Certificate Signing Request with OCSP Must Staple
+- openssl_csr:
+    path: /etc/ssl/csr/www.ansible.com.csr
+    privatekey_path: /etc/ssl/private/ansible.com.pem
+    common_name: www.ansible.com
+    ocsp_must_staple: true
 '''
 
 
@@ -243,6 +268,12 @@
     returned: changed or success
     type: list
     sample: ['CA:TRUE', 'pathLenConstraint:0']
+ocsp_must_staple:
+    description: Indicates whether the certificate has the OCSP
+                 Must Staple feature enabled
+    returned: changed or success
+    type: bool
+    sample: false
 '''
 
 import os
@@ -258,6 +289,14 @@
     pyopenssl_found = False
 else:
     pyopenssl_found = True
+    if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
+        # OpenSSL 1.1.0 or newer
+        MUST_STAPLE_NAME = b"tlsfeature"
+        MUST_STAPLE_VALUE = b"status_request"
+    else:
+        # OpenSSL 1.0.x or older
+        MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
+        MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
 
 
 class CertificateSigningRequestError(crypto_utils.OpenSSLObjectError):
@@ -285,6 +324,8 @@ def __init__(self, module):
         self.extendedKeyUsage_critical = module.params['extendedKeyUsage_critical']
         self.basicConstraints = module.params['basicConstraints']
         self.basicConstraints_critical = module.params['basicConstraints_critical']
+        self.ocspMustStaple = module.params['ocspMustStaple']
+        self.ocspMustStaple_critical = module.params['ocspMustStaple_critical']
         self.request = None
         self.privatekey = None
 
@@ -338,6 +379,9 @@ def generate(self, module):
                 usages = ', '.join(self.basicConstraints)
                 extensions.append(crypto.X509Extension(b"basicConstraints", self.basicConstraints_critical, usages.encode('ascii')))
 
+            if self.ocspMustStaple:
+                extensions.append(crypto.X509Extension(MUST_STAPLE_NAME, self.ocspMustStaple_critical, MUST_STAPLE_VALUE))
+
             if extensions:
                 req.add_extensions(extensions)
 
@@ -407,10 +451,21 @@ def _check_extenededKeyUsage(extensions):
         def _check_basicConstraints(extensions):
             return _check_keyUsage_(extensions, b'basicConstraints', self.basicConstraints, self.basicConstraints_critical)
 
+        def _check_ocspMustStaple(extensions):
+            oms_ext = [ext for ext in extensions if ext.get_short_name() == MUST_STAPLE_NAME and str(ext) == MUST_STAPLE_VALUE]
+            if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
+                # Older versions of libssl don't know about OCSP Must Staple
+                oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
+            if self.ocspMustStaple:
+                return len(oms_ext) > 0 and oms_ext[0].get_critical() == self.ocspMustStaple_critical
+            else:
+                return len(oms_ext) == 0
+
         def _check_extensions(csr):
             extensions = csr.get_extensions()
             return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and
-                    _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions))
+                    _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and
+                    _check_ocspMustStaple(extensions))
 
         def _check_signature(csr):
             try:
@@ -436,6 +491,7 @@ def dump(self):
             'keyUsage': self.keyUsage,
             'extendedKeyUsage': self.extendedKeyUsage,
             'basicConstraints': self.basicConstraints,
+            'ocspMustStaple': self.ocspMustStaple,
             'changed': self.changed
         }
 
@@ -468,6 +524,8 @@ def main():
             extendedKeyUsage_critical=dict(aliases=['extKeyUsage_critical', 'extended_key_usage_critical'], default=False, type='bool'),
             basicConstraints=dict(aliases=['basic_constraints'], type='list'),
             basicConstraints_critical=dict(aliases=['basic_constraints_critical'], default=False, type='bool'),
+            ocspMustStaple=dict(aliases=['ocsp_must_staple'], default=False, type='bool'),
+            ocspMustStaple_critical=dict(aliases=['ocsp_must_staple_critical'], default=False, type='bool'),
         ),
         add_file_common_args=True,
         supports_check_mode=True,


Test patch:
diff --git a/test/integration/targets/openssl_csr/tasks/main.yml b/test/integration/targets/openssl_csr/tasks/main.yml
--- a/test/integration/targets/openssl_csr/tasks/main.yml
+++ b/test/integration/targets/openssl_csr/tasks/main.yml
@@ -51,6 +51,21 @@
         privatekey_path: '{{ output_dir }}/privatekey.pem'
         commonName: www.ansible.com
 
+    - name: Generate CSR with OCSP Must Staple
+      openssl_csr:
+        path: '{{ output_dir }}/csr_ocsp.csr'
+        privatekey_path: '{{ output_dir }}/privatekey.pem'
+        subject_alt_name: "DNS:www.ansible.com"
+        ocsp_must_staple: true
+
+    - name: Generate CSR with OCSP Must Staple (test idempotency)
+      openssl_csr:
+        path: '{{ output_dir }}/csr_ocsp.csr'
+        privatekey_path: '{{ output_dir }}/privatekey.pem'
+        subject_alt_name: "DNS:www.ansible.com"
+        ocsp_must_staple: true
+      register: csr_ocsp_idempotency
+
     - import_tasks: ../tests/validate.yml
 
   when: pyopenssl_version.stdout is version('0.15', '>=')
diff --git a/test/integration/targets/openssl_csr/tests/validate.yml b/test/integration/targets/openssl_csr/tests/validate.yml
--- a/test/integration/targets/openssl_csr/tests/validate.yml
+++ b/test/integration/targets/openssl_csr/tests/validate.yml
@@ -19,7 +19,7 @@
 - name: Validate CSR_KU_XKU (assert idempotency)
   assert:
     that:
-      - csr_ku_xku.changed == False
+      - csr_ku_xku is not changed
 
 - name: Validate old_API CSR (test - Common Name)
   shell: "openssl req -noout -subject -in {{ output_dir }}/csr_oldapi.csr -nameopt oneline,-space_eq"
@@ -34,3 +34,18 @@
     that:
       - csr_oldapi_cn.stdout.split('=')[-1] == 'www.ansible.com'
       - csr_oldapi_modulus.stdout == privatekey_modulus.stdout
+
+- name: Validate OCSP Must Staple CSR (test - everything)
+  shell: "openssl req -noout -in {{ output_dir }}/csr_ocsp.csr -text"
+  register: csr_ocsp
+
+- name: Validate OCSP Must Staple CSR (assert)
+  assert:
+    that:
+      - "(csr_ocsp.stdout is search('\\s+TLS Feature:\\s*\\n\\s+status_request\\s+')) or
+         (csr_ocsp.stdout is search('\\s+1.3.6.1.5.5.7.1.24:\\s*\\n\\s+0\\.\\.\\.\\.\\s+'))"
+
+- name: Validate OCSP Must Staple CSR (assert idempotency)
+  assert:
+    that:
+      - csr_ocsp_idempotency is not changed