1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
5# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11
12DOCUMENTATION = r'''
13---
14module: x509_certificate_info
15short_description: Provide information of OpenSSL X.509 certificates
16description:
17    - This module allows one to query information on OpenSSL certificates.
18    - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
19      cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
20      cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
21      C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
22      and will be removed in community.crypto 2.0.0.
23    - Note that this module was called C(openssl_certificate_info) when included directly in Ansible
24      up to version 2.9.  When moved to the collection C(community.crypto), it was renamed to
25      M(community.crypto.x509_certificate_info). From Ansible 2.10 on, it can still be used by the
26      old short name (or by C(ansible.builtin.openssl_certificate_info)), which redirects to
27      C(community.crypto.x509_certificate_info). When using FQCNs or when using the
28      L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook)
29      keyword, the new name M(community.crypto.x509_certificate_info) should be used to avoid
30      a deprecation warning.
31requirements:
32    - PyOpenSSL >= 0.15 or cryptography >= 1.6
33author:
34  - Felix Fontein (@felixfontein)
35  - Yanis Guenane (@Spredzy)
36  - Markus Teufelberger (@MarkusTeufelberger)
37options:
38    path:
39        description:
40            - Remote absolute path where the certificate file is loaded from.
41            - Either I(path) or I(content) must be specified, but not both.
42        type: path
43    content:
44        description:
45            - Content of the X.509 certificate in PEM format.
46            - Either I(path) or I(content) must be specified, but not both.
47        type: str
48        version_added: '1.0.0'
49    valid_at:
50        description:
51            - A dict of names mapping to time specifications. Every time specified here
52              will be checked whether the certificate is valid at this point. See the
53              C(valid_at) return value for informations on the result.
54            - Time can be specified either as relative time or as absolute timestamp.
55            - Time will always be interpreted as UTC.
56            - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
57              + C([w | d | h | m | s]) (e.g. C(+32w1d2h), and ASN.1 TIME (in other words, pattern C(YYYYMMDDHHMMSSZ)).
58              Note that all timestamps will be treated as being in UTC.
59        type: dict
60    select_crypto_backend:
61        description:
62            - Determines which crypto backend to use.
63            - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
64            - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
65            - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
66            - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
67              From that point on, only the C(cryptography) backend will be available.
68        type: str
69        default: auto
70        choices: [ auto, cryptography, pyopenssl ]
71
72notes:
73    - All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
74      They are all in UTC.
75    - Supports C(check_mode).
76seealso:
77- module: community.crypto.x509_certificate
78- module: community.crypto.x509_certificate_pipe
79'''
80
81EXAMPLES = r'''
82- name: Generate a Self Signed OpenSSL certificate
83  community.crypto.x509_certificate:
84    path: /etc/ssl/crt/ansible.com.crt
85    privatekey_path: /etc/ssl/private/ansible.com.pem
86    csr_path: /etc/ssl/csr/ansible.com.csr
87    provider: selfsigned
88
89
90# Get information on the certificate
91
92- name: Get information on generated certificate
93  community.crypto.x509_certificate_info:
94    path: /etc/ssl/crt/ansible.com.crt
95  register: result
96
97- name: Dump information
98  ansible.builtin.debug:
99    var: result
100
101
102# Check whether the certificate is valid or not valid at certain times, fail
103# if this is not the case. The first task (x509_certificate_info) collects
104# the information, and the second task (assert) validates the result and
105# makes the playbook fail in case something is not as expected.
106
107- name: Test whether that certificate is valid tomorrow and/or in three weeks
108  community.crypto.x509_certificate_info:
109    path: /etc/ssl/crt/ansible.com.crt
110    valid_at:
111      point_1: "+1d"
112      point_2: "+3w"
113  register: result
114
115- name: Validate that certificate is valid tomorrow, but not in three weeks
116  assert:
117    that:
118      - result.valid_at.point_1      # valid in one day
119      - not result.valid_at.point_2  # not valid in three weeks
120'''
121
122RETURN = r'''
123expired:
124    description: Whether the certificate is expired (in other words, C(notAfter) is in the past).
125    returned: success
126    type: bool
127basic_constraints:
128    description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present.
129    returned: success
130    type: list
131    elements: str
132    sample: "[CA:TRUE, pathlen:1]"
133basic_constraints_critical:
134    description: Whether the C(basic_constraints) extension is critical.
135    returned: success
136    type: bool
137extended_key_usage:
138    description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present.
139    returned: success
140    type: list
141    elements: str
142    sample: "[Biometric Info, DVCS, Time Stamping]"
143extended_key_usage_critical:
144    description: Whether the C(extended_key_usage) extension is critical.
145    returned: success
146    type: bool
147extensions_by_oid:
148    description: Returns a dictionary for every extension OID.
149    returned: success
150    type: dict
151    contains:
152        critical:
153            description: Whether the extension is critical.
154            returned: success
155            type: bool
156        value:
157            description: The Base64 encoded value (in DER format) of the extension.
158            returned: success
159            type: str
160            sample: "MAMCAQU="
161    sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}'
162key_usage:
163    description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
164    returned: success
165    type: str
166    sample: "[Key Agreement, Data Encipherment]"
167key_usage_critical:
168    description: Whether the C(key_usage) extension is critical.
169    returned: success
170    type: bool
171subject_alt_name:
172    description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
173    returned: success
174    type: list
175    elements: str
176    sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
177subject_alt_name_critical:
178    description: Whether the C(subject_alt_name) extension is critical.
179    returned: success
180    type: bool
181ocsp_must_staple:
182    description: C(yes) if the OCSP Must Staple extension is present, C(none) otherwise.
183    returned: success
184    type: bool
185ocsp_must_staple_critical:
186    description: Whether the C(ocsp_must_staple) extension is critical.
187    returned: success
188    type: bool
189issuer:
190    description:
191        - The certificate's issuer.
192        - Note that for repeated values, only the last one will be returned.
193    returned: success
194    type: dict
195    sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
196issuer_ordered:
197    description: The certificate's issuer as an ordered list of tuples.
198    returned: success
199    type: list
200    elements: list
201    sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
202subject:
203    description:
204        - The certificate's subject as a dictionary.
205        - Note that for repeated values, only the last one will be returned.
206    returned: success
207    type: dict
208    sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}'
209subject_ordered:
210    description: The certificate's subject as an ordered list of tuples.
211    returned: success
212    type: list
213    elements: list
214    sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]'
215not_after:
216    description: C(notAfter) date as ASN.1 TIME.
217    returned: success
218    type: str
219    sample: 20190413202428Z
220not_before:
221    description: C(notBefore) date as ASN.1 TIME.
222    returned: success
223    type: str
224    sample: 20190331202428Z
225public_key:
226    description: Certificate's public key in PEM format.
227    returned: success
228    type: str
229    sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
230public_key_type:
231    description:
232        - The certificate's public key's type.
233        - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
234        - Will start with C(unknown) if the key type cannot be determined.
235    returned: success
236    type: str
237    version_added: 1.7.0
238    sample: RSA
239public_key_data:
240    description:
241        - Public key data. Depends on the public key's type.
242    returned: success
243    type: dict
244    version_added: 1.7.0
245    contains:
246        size:
247            description:
248                - Bit size of modulus (RSA) or prime number (DSA).
249            type: int
250            returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
251        modulus:
252            description:
253                - The RSA key's modulus.
254            type: int
255            returned: When C(public_key_type=RSA)
256        exponent:
257            description:
258                - The RSA key's public exponent.
259            type: int
260            returned: When C(public_key_type=RSA)
261        p:
262            description:
263                - The C(p) value for DSA.
264                - This is the prime modulus upon which arithmetic takes place.
265            type: int
266            returned: When C(public_key_type=DSA)
267        q:
268            description:
269                - The C(q) value for DSA.
270                - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
271                  multiplicative group of the prime field used.
272            type: int
273            returned: When C(public_key_type=DSA)
274        g:
275            description:
276                - The C(g) value for DSA.
277                - This is the element spanning the subgroup of the multiplicative group of the prime field used.
278            type: int
279            returned: When C(public_key_type=DSA)
280        curve:
281            description:
282                - The curve's name for ECC.
283            type: str
284            returned: When C(public_key_type=ECC)
285        exponent_size:
286            description:
287                - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
288            type: int
289            returned: When C(public_key_type=ECC)
290        x:
291            description:
292                - The C(x) coordinate for the public point on the elliptic curve.
293            type: int
294            returned: When C(public_key_type=ECC)
295        y:
296            description:
297                - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
298                - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
299            type: int
300            returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
301public_key_fingerprints:
302    description:
303        - Fingerprints of certificate's public key.
304        - For every hash algorithm available, the fingerprint is computed.
305    returned: success
306    type: dict
307    sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
308              'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
309fingerprints:
310    description:
311        - Fingerprints of the DER-encoded form of the whole certificate.
312        - For every hash algorithm available, the fingerprint is computed.
313    returned: success
314    type: dict
315    sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
316              'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
317    version_added: 1.2.0
318signature_algorithm:
319    description: The signature algorithm used to sign the certificate.
320    returned: success
321    type: str
322    sample: sha256WithRSAEncryption
323serial_number:
324    description: The certificate's serial number.
325    returned: success
326    type: int
327    sample: 1234
328version:
329    description: The certificate version.
330    returned: success
331    type: int
332    sample: 3
333valid_at:
334    description: For every time stamp provided in the I(valid_at) option, a
335                 boolean whether the certificate is valid at that point in time
336                 or not.
337    returned: success
338    type: dict
339subject_key_identifier:
340    description:
341        - The certificate's subject key identifier.
342        - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
343        - Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
344    returned: success and if the pyOpenSSL backend is I(not) used
345    type: str
346    sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
347authority_key_identifier:
348    description:
349        - The certificate's authority key identifier.
350        - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
351        - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
352    returned: success and if the pyOpenSSL backend is I(not) used
353    type: str
354    sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
355authority_cert_issuer:
356    description:
357        - The certificate's authority cert issuer as a list of general names.
358        - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
359    returned: success and if the pyOpenSSL backend is I(not) used
360    type: list
361    elements: str
362    sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
363authority_cert_serial_number:
364    description:
365        - The certificate's authority cert serial number.
366        - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
367    returned: success and if the pyOpenSSL backend is I(not) used
368    type: int
369    sample: '12345'
370ocsp_uri:
371    description: The OCSP responder URI, if included in the certificate. Will be
372                 C(none) if no OCSP responder URI is included.
373    returned: success
374    type: str
375'''
376
377
378from ansible.module_utils.basic import AnsibleModule
379from ansible.module_utils.six import string_types
380from ansible.module_utils.common.text.converters import to_native
381
382from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
383    OpenSSLObjectError,
384)
385
386from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
387    get_relative_time_option,
388)
389
390from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
391    select_backend,
392)
393
394
395def main():
396    module = AnsibleModule(
397        argument_spec=dict(
398            path=dict(type='path'),
399            content=dict(type='str'),
400            valid_at=dict(type='dict'),
401            select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
402        ),
403        required_one_of=(
404            ['path', 'content'],
405        ),
406        mutually_exclusive=(
407            ['path', 'content'],
408        ),
409        supports_check_mode=True,
410    )
411    if module._name == 'community.crypto.openssl_certificate_info':
412        module.deprecate("The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'",
413                         version='2.0.0', collection_name='community.crypto')
414
415    if module.params['content'] is not None:
416        data = module.params['content'].encode('utf-8')
417    else:
418        try:
419            with open(module.params['path'], 'rb') as f:
420                data = f.read()
421        except (IOError, OSError) as e:
422            module.fail_json(msg='Error while reading certificate file from disk: {0}'.format(e))
423
424    backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data)
425
426    valid_at = module.params['valid_at']
427    if valid_at:
428        for k, v in valid_at.items():
429            if not isinstance(v, string_types):
430                module.fail_json(
431                    msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
432                )
433            valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k))
434
435    try:
436        result = module_backend.get_info()
437
438        not_before = module_backend.get_not_before()
439        not_after = module_backend.get_not_after()
440
441        result['valid_at'] = dict()
442        if valid_at:
443            for k, v in valid_at.items():
444                result['valid_at'][k] = not_before <= v <= not_after
445
446        module.exit_json(**result)
447    except OpenSSLObjectError as exc:
448        module.fail_json(msg=to_native(exc))
449
450
451if __name__ == "__main__":
452    main()
453