1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright (c), Entrust Datacard Corporation, 2019
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'community'}
14
15DOCUMENTATION = '''
16---
17module: ecs_certificate
18author:
19    - Chris Trufan (@ctrufan)
20version_added: '2.9'
21short_description: Request SSL/TLS certificates with the Entrust Certificate Services (ECS) API
22description:
23    - Create, reissue, and renew certificates with the Entrust Certificate Services (ECS) API.
24    - Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API.
25    - In order to request a certificate, the domain and organization used in the certificate signing request must be already
26      validated in the ECS system. It is I(not) the responsibility of this module to perform those steps.
27notes:
28    - C(path) must be specified as the output location of the certificate.
29requirements:
30    - cryptography >= 1.6
31options:
32    backup:
33        description:
34            - Whether a backup should be made for the certificate in I(path).
35        type: bool
36        default: false
37    force:
38        description:
39            - If force is used, a certificate is requested regardless of whether I(path) points to an existing valid certificate.
40            - If C(request_type=renew), a forced renew will fail if the certificate being renewed has been issued within the past 30 days, regardless of the
41              value of I(remaining_days) or the return value of I(cert_days) - the ECS API does not support the "renew" operation for certificates that are not
42              at least 30 days old.
43        type: bool
44        default: false
45    path:
46        description:
47            - The destination path for the generated certificate as a PEM encoded cert.
48            - If the certificate at this location is not an Entrust issued certificate, a new certificate will always be requested even if the current
49              certificate is technically valid.
50            - If there is already an Entrust certificate at this location, whether it is replaced is depends on the I(remaining_days) calculation.
51            - If an existing certificate is being replaced (see I(remaining_days), I(force), and I(tracking_id)), whether a new certificate is requested
52              or the existing certificate is renewed or reissued is based on I(request_type).
53        type: path
54        required: true
55    full_chain_path:
56        description:
57            - The destination path for the full certificate chain of the certificate, intermediates, and roots.
58        type: path
59    csr:
60        description:
61            - Base-64 encoded Certificate Signing Request (CSR). I(csr) is accepted with or without PEM formatting around the Base-64 string.
62            - If no I(csr) is provided when C(request_type=reissue) or C(request_type=renew), the certificate will be generated with the same public key as
63              the certificate being renewed or reissued.
64            - If I(subject_alt_name) is specified, it will override the subject alternate names in the CSR.
65            - If I(eku) is specified, it will override the extended key usage in the CSR.
66            - If I(ou) is specified, it will override the organizational units "ou=" present in the subject distinguished name of the CSR, if any.
67            - The organization "O=" field from the CSR will not be used. It will be replaced in the issued certificate by I(org) if present, and if not present,
68              the organization tied to I(client_id).
69        type: str
70    tracking_id:
71        description:
72            - The tracking ID of the certificate to reissue or renew.
73            - I(tracking_id) is invalid if C(request_type=new) or C(request_type=validate_only).
74            - If there is a certificate present in I(path) and it is an ECS certificate, I(tracking_id) will be ignored.
75            - If there is no certificate present in I(path) or there is but it is from another provider, the certificate represented by I(tracking_id) will
76              be renewed or reissued and saved to I(path).
77            - If there is no certificate present in I(path) and the I(force) and I(remaining_days) parameters do not indicate a new certificate is needed,
78              the certificate referenced by I(tracking_id) certificate will be saved to I(path).
79            - This can be used when a known certificate is not currently present on a server, but you want to renew or reissue it to be managed by an ansible
80              playbook. For example, if you specify C(request_type=renew), I(tracking_id) of an issued certificate, and I(path) to a file that does not exist,
81              the first run of a task will download the certificate specified by I(tracking_id) (assuming it is still valid). Future runs of the task will
82              (if applicable - see I(force) and I(remaining_days)) renew the certificate now present in I(path).
83        type: int
84    remaining_days:
85        description:
86            - The number of days the certificate must have left being valid. If C(cert_days < remaining_days) then a new certificate will be
87              obtained using I(request_type).
88            - If C(request_type=renew), a renewal will fail if the certificate being renewed has been issued within the past 30 days, so do not set a
89              I(remaining_days) value that is within 30 days of the full lifetime of the certificate being acted upon. (e.g. if you are requesting Certificates
90              with a 90 day lifetime, do not set remaining_days to a value C(60) or higher).
91            - The I(force) option may be used to ensure that a new certificate is always obtained.
92        type: int
93        default: 30
94    request_type:
95        description:
96            - The operation performed if I(tracking_id) references a valid certificate to reissue, or there is already a certificate present in I(path) but
97              either I(force) is specified or C(cert_days < remaining_days).
98            - Specifying C(request_type=validate_only) means the request will be validated against the ECS API, but no certificate will be issued.
99            - Specifying C(request_type=new) means a certificate request will always be submitted and a new certificate issued.
100            - Specifying C(request_type=renew) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be renewed.
101              If there is no certificate to renew, a new certificate is requested.
102            - Specifying C(request_type=reissue) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be
103              reissued.
104              If there is no certificate to reissue, a new certificate is requested.
105            - If a certificate was issued within the past 30 days, the 'renew' operation is not a valid operation and will fail.
106            - Note that C(reissue) is an operation that will result in the revocation of the certificate that is reissued, be cautious with it's use.
107            - I(check_mode) is only supported if C(request_type=new)
108            - For example, setting C(request_type=renew) and C(remaining_days=30) and pointing to the same certificate on multiple playbook runs means that on
109              the first run new certificate will be requested. It will then be left along on future runs until it is within 30 days of expiry, then the
110              ECS "renew" operation will be performed.
111        type: str
112        choices: [ 'new', 'renew', 'reissue', 'validate_only']
113        default: new
114    cert_type:
115        description:
116            - Specify the type of certificate requested.
117            - If a certificate is being reissued or renewed, this parameter is ignored, and the C(cert_type) of the initial certificate is used.
118        type: str
119        choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CODE_SIGNING', 'EV_CODE_SIGNING',
120                   'CDS_INDIVIDUAL', 'CDS_GROUP', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ]
121    subject_alt_name:
122        description:
123            - The subject alternative name identifiers, as an array of values (applies to I(cert_type) with a value of C(STANDARD_SSL), C(ADVANTAGE_SSL),
124              C(UC_SSL), C(EV_SSL), C(WILDCARD_SSL), C(PRIVATE_SSL), and C(PD_SSL)).
125            - If you are requesting a new SSL certificate, and you pass a I(subject_alt_name) parameter, any SAN names in the CSR are ignored.
126              If no subjectAltName parameter is passed, the SAN names in the CSR are used.
127            - See I(request_type) to understand more about SANs during reissues and renewals.
128            - In the case of certificates of type C(STANDARD_SSL) certificates, if the CN of the certificate is <domain>.<tld> only the www.<domain>.<tld> value
129              is accepted. If the CN of the certificate is www.<domain>.<tld> only the <domain>.<tld> value is accepted.
130        type: list
131        elements: str
132    eku:
133        description:
134            - If specified, overrides the key usage in the I(csr).
135        type: str
136        choices: [ SERVER_AUTH, CLIENT_AUTH, SERVER_AND_CLIENT_AUTH ]
137    ct_log:
138        description:
139            - In compliance with browser requirements, this certificate may be posted to the Certificate Transparency (CT) logs. This is a best practice
140              technique that helps domain owners monitor certificates issued to their domains. Note that not all certificates are eligible for CT logging.
141            - If I(ct_log) is not specified, the certificate uses the account default.
142            - If I(ct_log) is specified and the account settings allow it, I(ct_log) overrides the account default.
143            - If I(ct_log) is set to C(false), but the account settings are set to "always log", the certificate generation will fail.
144        type: bool
145    client_id:
146        description:
147            - The client ID to submit the Certificate Signing Request under.
148            - If no client ID is specified, the certificate will be submitted under the primary client with ID of 1.
149            - When using a client other than the primary client, the I(org) parameter cannot be specified.
150            - The issued certificate will have an organization value in the subject distinguished name represented by the client.
151        type: int
152        default: 1
153    org:
154        description:
155            - Organization "O=" to include in the certificate.
156            - If I(org) is not specified, the organization from the client represented by I(client_id) is used.
157            - Unless the I(cert_type) is C(PD_SSL), this field may not be specified if the value of I(client_id) is not "1" (the primary client).
158              non-primary clients, certificates may only be issued with the organization of that client.
159        type: str
160    ou:
161        description:
162            - Organizational unit "OU=" to include in the certificate.
163            - I(ou) behavior is dependent on whether organizational units are enabled for your account. If organizational unit support is disabled for your
164              account, organizational units from the I(csr) and the I(ou) parameter are ignored.
165            - If both I(csr) and I(ou) are specified, the value in I(ou) will override the OU fields present in the subject distinguished name in the I(csr)
166            - If neither I(csr) nor I(ou) are specified for a renew or reissue operation, the OU fields in the initial certificate are reused.
167            - An invalid OU from I(csr) is ignored, but any invalid organizational units in I(ou) will result in an error indicating "Unapproved OU". The I(ou)
168              parameter can be used to force failure if an unapproved organizational unit is provided.
169            - A maximum of one OU may be specified for current products. Multiple OUs are reserved for future products.
170        type: list
171        elements: str
172    end_user_key_storage_agreement:
173        description:
174            - The end user of the Code Signing certificate must generate and store the private key for this request on cryptographically secure
175              hardware to be compliant with the Entrust CSP and Subscription agreement. If requesting a certificate of type C(CODE_SIGNING) or
176              C(EV_CODE_SIGNING), you must set I(end_user_key_storage_agreement) to true if and only if you acknowledge that you will inform the user of this
177              requirement.
178            - Applicable only to I(cert_type) of values C(CODE_SIGNING) and C(EV_CODE_SIGNING).
179        type: bool
180    tracking_info:
181        description: Free form tracking information to attach to the record for the certificate.
182        type: str
183    requester_name:
184        description: The requester name to associate with certificate tracking information.
185        type: str
186        required: true
187    requester_email:
188        description: The requester email to associate with certificate tracking information and receive delivery and expiry notices for the certificate.
189        type: str
190        required: true
191    requester_phone:
192        description: The requester phone number to associate with certificate tracking information.
193        type: str
194        required: true
195    additional_emails:
196        description: A list of additional email addresses to receive the delivery notice and expiry notification for the certificate.
197        type: list
198        elements: str
199    custom_fields:
200        description:
201            - Mapping of custom fields to associate with the certificate request and certificate.
202            - Only supported if custom fields are enabled for your account.
203            - Each custom field specified must be a custom field you have defined for your account.
204        type: dict
205        suboptions:
206            text1:
207                description: Custom text field (maximum 500 characters)
208                type: str
209            text2:
210                description: Custom text field (maximum 500 characters)
211                type: str
212            text3:
213                description: Custom text field (maximum 500 characters)
214                type: str
215            text4:
216                description: Custom text field (maximum 500 characters)
217                type: str
218            text5:
219                description: Custom text field (maximum 500 characters)
220                type: str
221            text6:
222                description: Custom text field (maximum 500 characters)
223                type: str
224            text7:
225                description: Custom text field (maximum 500 characters)
226                type: str
227            text8:
228                description: Custom text field (maximum 500 characters)
229                type: str
230            text9:
231                description: Custom text field (maximum 500 characters)
232                type: str
233            text10:
234                description: Custom text field (maximum 500 characters)
235                type: str
236            text11:
237                description: Custom text field (maximum 500 characters)
238                type: str
239            text12:
240                description: Custom text field (maximum 500 characters)
241                type: str
242            text13:
243                description: Custom text field (maximum 500 characters)
244                type: str
245            text14:
246                description: Custom text field (maximum 500 characters)
247                type: str
248            text15:
249                description: Custom text field (maximum 500 characters)
250                type: str
251            number1:
252                description: Custom number field.
253                type: float
254            number2:
255                description: Custom number field.
256                type: float
257            number3:
258                description: Custom number field.
259                type: float
260            number4:
261                description: Custom number field.
262                type: float
263            number5:
264                description: Custom number field.
265                type: float
266            date1:
267                description: Custom date field.
268                type: str
269            date2:
270                description: Custom date field.
271                type: str
272            date3:
273                description: Custom date field.
274                type: str
275            date4:
276                description: Custom date field.
277                type: str
278            date5:
279                description: Custom date field.
280                type: str
281            email1:
282                description: Custom email field.
283                type: str
284            email2:
285                description: Custom email field.
286                type: str
287            email3:
288                description: Custom email field.
289                type: str
290            email4:
291                description: Custom email field.
292                type: str
293            email5:
294                description: Custom email field.
295                type: str
296            dropdown1:
297                description: Custom dropdown field.
298                type: str
299            dropdown2:
300                description: Custom dropdown field.
301                type: str
302            dropdown3:
303                description: Custom dropdown field.
304                type: str
305            dropdown4:
306                description: Custom dropdown field.
307                type: str
308            dropdown5:
309                description: Custom dropdown field.
310                type: str
311    cert_expiry:
312        description:
313            - The date the certificate should be set to expire, in RFC3339 compliant date or date-time format. For example,
314              C(2020-02-23), C(2020-02-23T15:00:00.05Z).
315            - I(cert_expiry) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue),
316              I(cert_expiry) will be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial
317              certificate.
318            - A reissued certificate will always have the same expiry as the original certificate.
319            - Note that only the date (day, month, year) is supported for specifying the expiry date. If you choose to specify an expiry time with the expiry
320              date, the time will be adjusted to Eastern Standard Time (EST). This could have the unintended effect of moving your expiry date to the previous
321              day.
322            - Applies only to accounts with a pooling inventory model.
323            - Only one of I(cert_expiry) or I(cert_lifetime) may be specified.
324        type: str
325    cert_lifetime:
326        description:
327            - The lifetime of the certificate.
328            - Applies to all certificates for accounts with a non-pooling inventory model.
329            - I(cert_lifetime) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue), I(cert_lifetime) will
330              be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial certificate.
331            - Applies to certificates of I(cert_type)=C(CDS_INDIVIDUAL, CDS_GROUP, CDS_ENT_LITE, CDS_ENT_PRO, SMIME_ENT) for accounts with a pooling inventory
332              model.
333            - C(P1Y) is a certificate with a 1 year lifetime.
334            - C(P2Y) is a certificate with a 2 year lifetime.
335            - C(P3Y) is a certificate with a 3 year lifetime.
336            - Only one of I(cert_expiry) or I(cert_lifetime) may be specified.
337        type: str
338        choices: [ P1Y, P2Y, P3Y ]
339seealso:
340    - module: openssl_privatekey
341      description: Can be used to create private keys (both for certificates and accounts).
342    - module: openssl_csr
343      description: Can be used to create a Certificate Signing Request (CSR).
344extends_documentation_fragment:
345    - ecs_credential
346'''
347
348EXAMPLES = r'''
349- name: Request a new certificate from Entrust with bare minimum parameters.
350        Will request a new certificate if current one is valid but within 30
351        days of expiry. If replacing an existing file in path, will back it up.
352  ecs_certificate:
353    backup: true
354    path: /etc/ssl/crt/ansible.com.crt
355    full_chain_path: /etc/ssl/crt/ansible.com.chain.crt
356    csr: /etc/ssl/csr/ansible.com.csr
357    cert_type: EV_SSL
358    requester_name: Jo Doe
359    requester_email: jdoe@ansible.com
360    requester_phone: 555-555-5555
361    entrust_api_user: apiusername
362    entrust_api_key: a^lv*32!cd9LnT
363    entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
364    entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
365
366- name: If there is no certificate present in path, request a new certificate
367        of type EV_SSL. Otherwise, if there is an Entrust managed certificate
368        in path and it is within 63 days of expiration, request a renew of that
369        certificate.
370  ecs_certificate:
371    path: /etc/ssl/crt/ansible.com.crt
372    csr: /etc/ssl/csr/ansible.com.csr
373    cert_type: EV_SSL
374    cert_expiry: '2020-08-20'
375    request_type: renew
376    remaining_days: 63
377    requester_name: Jo Doe
378    requester_email: jdoe@ansible.com
379    requester_phone: 555-555-5555
380    entrust_api_user: apiusername
381    entrust_api_key: a^lv*32!cd9LnT
382    entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
383    entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
384
385- name: If there is no certificate present in path, download certificate
386        specified by tracking_id if it is still valid. Otherwise, if the
387        certificate is within 79 days of expiration, request a renew of that
388        certificate and save it in path. This can be used to "migrate" a
389        certificate to be Ansible managed.
390  ecs_certificate:
391    path: /etc/ssl/crt/ansible.com.crt
392    csr: /etc/ssl/csr/ansible.com.csr
393    tracking_id: 2378915
394    request_type: renew
395    remaining_days: 79
396    entrust_api_user: apiusername
397    entrust_api_key: a^lv*32!cd9LnT
398    entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
399    entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
400
401- name: Force a reissue of the certificate specified by tracking_id.
402  ecs_certificate:
403    path: /etc/ssl/crt/ansible.com.crt
404    force: true
405    tracking_id: 2378915
406    request_type: reissue
407    entrust_api_user: apiusername
408    entrust_api_key: a^lv*32!cd9LnT
409    entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
410    entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
411
412- name: Request a new certificate with an alternative client. Note that the
413        issued certificate will have it's Subject Distinguished Name use the
414        organization details associated with that client, rather than what is
415        in the CSR.
416  ecs_certificate:
417    path: /etc/ssl/crt/ansible.com.crt
418    csr: /etc/ssl/csr/ansible.com.csr
419    client_id: 2
420    requester_name: Jo Doe
421    requester_email: jdoe@ansible.com
422    requester_phone: 555-555-5555
423    entrust_api_user: apiusername
424    entrust_api_key: a^lv*32!cd9LnT
425    entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
426    entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
427
428- name: Request a new certificate with a number of CSR parameters overridden
429        and tracking information
430  ecs_certificate:
431    path: /etc/ssl/crt/ansible.com.crt
432    full_chain_path: /etc/ssl/crt/ansible.com.chain.crt
433    csr: /etc/ssl/csr/ansible.com.csr
434    subject_alt_name:
435      - ansible.testcertificates.com
436      - www.testcertificates.com
437    eku: SERVER_AND_CLIENT_AUTH
438    ct_log: true
439    org: Test Organization Inc.
440    ou:
441      - Administration
442    tracking_info: "Submitted via Ansible"
443    additional_emails:
444      - itsupport@testcertificates.com
445      - jsmith@ansible.com
446    custom_fields:
447      text1: Admin
448      text2: Invoice 25
449      number1: 342
450      date1: '2018-01-01'
451      email1: sales@ansible.testcertificates.com
452      dropdown1: red
453    cert_expiry: '2020-08-15'
454    requester_name: Jo Doe
455    requester_email: jdoe@ansible.com
456    requester_phone: 555-555-5555
457    entrust_api_user: apiusername
458    entrust_api_key: a^lv*32!cd9LnT
459    entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
460    entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
461
462'''
463
464RETURN = '''
465filename:
466    description: The destination path for the generated certificate.
467    returned: changed or success
468    type: str
469    sample: /etc/ssl/crt/www.ansible.com.crt
470backup_file:
471    description: Name of backup file created for the certificate.
472    returned: changed and if I(backup) is C(true)
473    type: str
474    sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~
475backup_full_chain_file:
476    description: Name of the backup file created for the certificate chain.
477    returned: changed and if I(backup) is C(true) and I(full_chain_path) is set.
478    type: str
479    sample: /path/to/ca.chain.crt.2019-03-09@11:22~
480tracking_id:
481    description: The tracking ID to reference and track the certificate in ECS.
482    returned: success
483    type: int
484    sample: 380079
485serial_number:
486    description: The serial number of the issued certificate.
487    returned: success
488    type: int
489    sample: 1235262234164342
490cert_days:
491    description: The number of days the certificate remains valid.
492    returned: success
493    type: int
494    sample: 253
495cert_status:
496    description:
497        - The certificate status in ECS.
498        - 'Current possible values (which may be expanded in the future) are: C(ACTIVE), C(APPROVED), C(DEACTIVATED), C(DECLINED), C(EXPIRED), C(NA),
499          C(PENDING), C(PENDING_QUORUM), C(READY), C(REISSUED), C(REISSUING), C(RENEWED), C(RENEWING), C(REVOKED), C(SUSPENDED)'
500    returned: success
501    type: str
502    sample: ACTIVE
503cert_details:
504    description:
505        - The full response JSON from the Get Certificate call of the ECS API.
506        - 'While the response contents are guaranteed to be forwards compatible with new ECS API releases, Entrust recommends that you do not make any
507          playbooks take actions based on the content of this field. However it may be useful for debugging, logging, or auditing purposes.'
508    returned: success
509    type: dict
510
511'''
512
513from ansible.module_utils.ecs.api import (
514    ecs_client_argument_spec,
515    ECSClient,
516    RestOperationException,
517    SessionConfigurationException,
518)
519
520import datetime
521import json
522import os
523import re
524import time
525import traceback
526from distutils.version import LooseVersion
527
528from ansible.module_utils import crypto as crypto_utils
529from ansible.module_utils.basic import AnsibleModule, missing_required_lib
530from ansible.module_utils._text import to_native, to_bytes
531
532CRYPTOGRAPHY_IMP_ERR = None
533try:
534    import cryptography
535    CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
536except ImportError:
537    CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
538    CRYPTOGRAPHY_FOUND = False
539else:
540    CRYPTOGRAPHY_FOUND = True
541
542MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
543
544
545def validate_cert_expiry(cert_expiry):
546    search_string_partial = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])\Z')
547    search_string_full = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):'
548                                    r'([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))\Z')
549    if search_string_partial.match(cert_expiry) or search_string_full.match(cert_expiry):
550        return True
551    return False
552
553
554def calculate_cert_days(expires_after):
555    cert_days = 0
556    if expires_after:
557        expires_after_datetime = datetime.datetime.strptime(expires_after, '%Y-%m-%dT%H:%M:%SZ')
558        cert_days = (expires_after_datetime - datetime.datetime.now()).days
559    return cert_days
560
561
562# Populate the value of body[dict_param_name] with the JSON equivalent of
563# module parameter of param_name if that parameter is present, otherwise leave field
564# out of resulting dict
565def convert_module_param_to_json_bool(module, dict_param_name, param_name):
566    body = {}
567    if module.params[param_name] is not None:
568        if module.params[param_name]:
569            body[dict_param_name] = 'true'
570        else:
571            body[dict_param_name] = 'false'
572    return body
573
574
575class EcsCertificate(object):
576    '''
577    Entrust Certificate Services certificate class.
578    '''
579
580    def __init__(self, module):
581        self.path = module.params['path']
582        self.full_chain_path = module.params['full_chain_path']
583        self.force = module.params['force']
584        self.backup = module.params['backup']
585        self.request_type = module.params['request_type']
586        self.csr = module.params['csr']
587
588        # All return values
589        self.changed = False
590        self.filename = None
591        self.tracking_id = None
592        self.cert_status = None
593        self.serial_number = None
594        self.cert_days = None
595        self.cert_details = None
596        self.backup_file = None
597        self.backup_full_chain_file = None
598
599        self.cert = None
600        self.ecs_client = None
601        if self.path and os.path.exists(self.path):
602            try:
603                self.cert = crypto_utils.load_certificate(self.path, backend='cryptography')
604            except Exception as dummy:
605                self.cert = None
606        # Instantiate the ECS client and then try a no-op connection to verify credentials are valid
607        try:
608            self.ecs_client = ECSClient(
609                entrust_api_user=module.params['entrust_api_user'],
610                entrust_api_key=module.params['entrust_api_key'],
611                entrust_api_cert=module.params['entrust_api_client_cert_path'],
612                entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'],
613                entrust_api_specification_path=module.params['entrust_api_specification_path']
614            )
615        except SessionConfigurationException as e:
616            module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e)))
617        try:
618            self.ecs_client.GetAppVersion()
619        except RestOperationException as e:
620            module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message)))
621
622    # Conversion of the fields that go into the 'tracking' parameter of the request object
623    def convert_tracking_params(self, module):
624        body = {}
625        tracking = {}
626        if module.params['requester_name']:
627            tracking['requesterName'] = module.params['requester_name']
628        if module.params['requester_email']:
629            tracking['requesterEmail'] = module.params['requester_email']
630        if module.params['requester_phone']:
631            tracking['requesterPhone'] = module.params['requester_phone']
632        if module.params['tracking_info']:
633            tracking['trackingInfo'] = module.params['tracking_info']
634        if module.params['custom_fields']:
635            # Omit custom fields from submitted dict if not present, instead of submitting them with value of 'null'
636            # The ECS API does technically accept null without error, but it complicates debugging user escalations and is unnecessary bandwidth.
637            custom_fields = {}
638            for k, v in module.params['custom_fields'].items():
639                if v is not None:
640                    custom_fields[k] = v
641            tracking['customFields'] = custom_fields
642        if module.params['additional_emails']:
643            tracking['additionalEmails'] = module.params['additional_emails']
644        body['tracking'] = tracking
645        return body
646
647    def convert_cert_subject_params(self, module):
648        body = {}
649        if module.params['subject_alt_name']:
650            body['subjectAltName'] = module.params['subject_alt_name']
651        if module.params['org']:
652            body['org'] = module.params['org']
653        if module.params['ou']:
654            body['ou'] = module.params['ou']
655        return body
656
657    def convert_general_params(self, module):
658        body = {}
659        if module.params['eku']:
660            body['eku'] = module.params['eku']
661        if self.request_type == 'new':
662            body['certType'] = module.params['cert_type']
663        body['clientId'] = module.params['client_id']
664        body.update(convert_module_param_to_json_bool(module, 'ctLog', 'ct_log'))
665        body.update(convert_module_param_to_json_bool(module, 'endUserKeyStorageAgreement', 'end_user_key_storage_agreement'))
666        return body
667
668    def convert_expiry_params(self, module):
669        body = {}
670        if module.params['cert_lifetime']:
671            body['certLifetime'] = module.params['cert_lifetime']
672        elif module.params['cert_expiry']:
673            body['certExpiryDate'] = module.params['cert_expiry']
674        # If neither cerTLifetime or certExpiryDate was specified and the request type is new, default to 365 days
675        elif self.request_type != 'reissue':
676            gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
677            expiry = gmt_now + datetime.timedelta(days=365)
678            body['certExpiryDate'] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
679        return body
680
681    def set_tracking_id_by_serial_number(self, module):
682        try:
683            # Use serial_number to identify if certificate is an Entrust Certificate
684            # with an associated tracking ID
685            serial_number = "{0:X}".format(self.cert.serial_number)
686            cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {})
687            if len(cert_results) == 1:
688                self.tracking_id = cert_results[0].get('trackingId')
689        except RestOperationException as dummy:
690            # If we fail to find a cert by serial number, that's fine, we just don't set self.tracking_id
691            return
692
693    def set_cert_details(self, module):
694        try:
695            self.cert_details = self.ecs_client.GetCertificate(trackingId=self.tracking_id)
696            self.cert_status = self.cert_details.get('status')
697            self.serial_number = self.cert_details.get('serialNumber')
698            self.cert_days = calculate_cert_days(self.cert_details.get('expiresAfter'))
699        except RestOperationException as e:
700            module.fail_json('Failed to get details of certificate with tracking_id="{0}", Error: '.format(self.tracking_id), to_native(e.message))
701
702    def check(self, module):
703        if self.cert:
704            # We will only treat a certificate as valid if it is found as a managed entrust cert.
705            # We will only set updated tracking ID based on certificate in "path" if it is managed by entrust.
706            self.set_tracking_id_by_serial_number(module)
707
708            if module.params['tracking_id'] and self.tracking_id and module.params['tracking_id'] != self.tracking_id:
709                module.warn('tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with '
710                            'tracking_id of "{2}".'.format(module.params['tracking_id'], self.path, self.tracking_id))
711
712        # If we did not end up setting tracking_id based on existing cert, get from module params
713        if not self.tracking_id:
714            self.tracking_id = module.params['tracking_id']
715
716        if not self.tracking_id:
717            return False
718
719        self.set_cert_details(module)
720
721        if self.cert_status == 'EXPIRED' or self.cert_status == 'SUSPENDED' or self.cert_status == 'REVOKED':
722            return False
723        if self.cert_days < module.params['remaining_days']:
724            return False
725
726        return True
727
728    def request_cert(self, module):
729        if not self.check(module) or self.force:
730            body = {}
731
732            # Read the CSR contents
733            if self.csr and os.path.exists(self.csr):
734                with open(self.csr, 'r') as csr_file:
735                    body['csr'] = csr_file.read()
736
737            # Check if the path is already a cert
738            # tracking_id may be set as a parameter or by get_cert_details if an entrust cert is in 'path'. If tracking ID is null
739            # We will be performing a reissue operation.
740            if self.request_type != 'new' and not self.tracking_id:
741                module.warn('No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task'
742                            'run. Future playbook runs that point to the pathination file in {1} will use request_type={2}'
743                            .format(self.path, self.path, self.request_type))
744                self.request_type = 'new'
745            elif self.request_type == 'new' and self.tracking_id:
746                module.warn('Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a'
747                            'reissue or renew')
748            # Use cases where request type is new and no existing certificate, or where request type is reissue/renew and a valid
749            # existing certificate is found, do not need warnings.
750
751            body.update(self.convert_tracking_params(module))
752            body.update(self.convert_cert_subject_params(module))
753            body.update(self.convert_general_params(module))
754            body.update(self.convert_expiry_params(module))
755
756            if not module.check_mode:
757                try:
758                    if self.request_type == 'validate_only':
759                        body['validateOnly'] = 'true'
760                        result = self.ecs_client.NewCertRequest(Body=body)
761                    if self.request_type == 'new':
762                        result = self.ecs_client.NewCertRequest(Body=body)
763                    elif self.request_type == 'renew':
764                        result = self.ecs_client.RenewCertRequest(trackingId=self.tracking_id, Body=body)
765                    elif self.request_type == 'reissue':
766                        result = self.ecs_client.ReissueCertRequest(trackingId=self.tracking_id, Body=body)
767                    self.tracking_id = result.get('trackingId')
768                    self.set_cert_details(module)
769                except RestOperationException as e:
770                    module.fail_json(msg='Failed to request new certificate from Entrust (ECS) {0}'.format(e.message))
771
772                if self.request_type != 'validate_only':
773                    if self.backup:
774                        self.backup_file = module.backup_local(self.path)
775                    crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert')))
776                    if self.full_chain_path and self.cert_details.get('chainCerts'):
777                        if self.backup:
778                            self.backup_full_chain_file = module.backup_local(self.full_chain_path)
779                        chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n'
780                        crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path)
781                    self.changed = True
782        # If there is no certificate present in path but a tracking ID was specified, save it to disk
783        elif not os.path.exists(self.path) and self.tracking_id:
784            if not module.check_mode:
785                crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert')))
786                if self.full_chain_path and self.cert_details.get('chainCerts'):
787                    chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n'
788                    crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path)
789            self.changed = True
790
791    def dump(self):
792        result = {
793            'changed': self.changed,
794            'filename': self.path,
795            'tracking_id': self.tracking_id,
796            'cert_status': self.cert_status,
797            'serial_number': self.serial_number,
798            'cert_days': self.cert_days,
799            'cert_details': self.cert_details,
800        }
801        if self.backup_file:
802            result['backup_file'] = self.backup_file
803            result['backup_full_chain_file'] = self.backup_full_chain_file
804        return result
805
806
807def custom_fields_spec():
808    return dict(
809        text1=dict(type='str'),
810        text2=dict(type='str'),
811        text3=dict(type='str'),
812        text4=dict(type='str'),
813        text5=dict(type='str'),
814        text6=dict(type='str'),
815        text7=dict(type='str'),
816        text8=dict(type='str'),
817        text9=dict(type='str'),
818        text10=dict(type='str'),
819        text11=dict(type='str'),
820        text12=dict(type='str'),
821        text13=dict(type='str'),
822        text14=dict(type='str'),
823        text15=dict(type='str'),
824        number1=dict(type='float'),
825        number2=dict(type='float'),
826        number3=dict(type='float'),
827        number4=dict(type='float'),
828        number5=dict(type='float'),
829        date1=dict(type='str'),
830        date2=dict(type='str'),
831        date3=dict(type='str'),
832        date4=dict(type='str'),
833        date5=dict(type='str'),
834        email1=dict(type='str'),
835        email2=dict(type='str'),
836        email3=dict(type='str'),
837        email4=dict(type='str'),
838        email5=dict(type='str'),
839        dropdown1=dict(type='str'),
840        dropdown2=dict(type='str'),
841        dropdown3=dict(type='str'),
842        dropdown4=dict(type='str'),
843        dropdown5=dict(type='str'),
844    )
845
846
847def ecs_certificate_argument_spec():
848    return dict(
849        backup=dict(type='bool', default=False),
850        force=dict(type='bool', default=False),
851        path=dict(type='path', required=True),
852        full_chain_path=dict(type='path'),
853        tracking_id=dict(type='int'),
854        remaining_days=dict(type='int', default=30),
855        request_type=dict(type='str', default='new', choices=['new', 'renew', 'reissue', 'validate_only']),
856        cert_type=dict(type='str', choices=['STANDARD_SSL',
857                                            'ADVANTAGE_SSL',
858                                            'UC_SSL',
859                                            'EV_SSL',
860                                            'WILDCARD_SSL',
861                                            'PRIVATE_SSL',
862                                            'PD_SSL',
863                                            'CODE_SIGNING',
864                                            'EV_CODE_SIGNING',
865                                            'CDS_INDIVIDUAL',
866                                            'CDS_GROUP',
867                                            'CDS_ENT_LITE',
868                                            'CDS_ENT_PRO',
869                                            'SMIME_ENT',
870                                            ]),
871        csr=dict(type='str'),
872        subject_alt_name=dict(type='list', elements='str'),
873        eku=dict(type='str', choices=['SERVER_AUTH', 'CLIENT_AUTH', 'SERVER_AND_CLIENT_AUTH']),
874        ct_log=dict(type='bool'),
875        client_id=dict(type='int', default=1),
876        org=dict(type='str'),
877        ou=dict(type='list', elements='str'),
878        end_user_key_storage_agreement=dict(type='bool'),
879        tracking_info=dict(type='str'),
880        requester_name=dict(type='str', required=True),
881        requester_email=dict(type='str', required=True),
882        requester_phone=dict(type='str', required=True),
883        additional_emails=dict(type='list', elements='str'),
884        custom_fields=dict(type='dict', default=None, options=custom_fields_spec()),
885        cert_expiry=dict(type='str'),
886        cert_lifetime=dict(type='str', choices=['P1Y', 'P2Y', 'P3Y']),
887    )
888
889
890def main():
891    ecs_argument_spec = ecs_client_argument_spec()
892    ecs_argument_spec.update(ecs_certificate_argument_spec())
893    module = AnsibleModule(
894        argument_spec=ecs_argument_spec,
895        required_if=(
896            ['request_type', 'new', ['cert_type']],
897            ['request_type', 'validate_only', ['cert_type']],
898            ['cert_type', 'CODE_SIGNING', ['end_user_key_storage_agreement']],
899            ['cert_type', 'EV_CODE_SIGNING', ['end_user_key_storage_agreement']],
900        ),
901        mutually_exclusive=(
902            ['cert_expiry', 'cert_lifetime'],
903        ),
904        supports_check_mode=True,
905    )
906
907    if not CRYPTOGRAPHY_FOUND or CRYPTOGRAPHY_VERSION < LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION):
908        module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
909                         exception=CRYPTOGRAPHY_IMP_ERR)
910
911    # If validate_only is used, pointing to an existing tracking_id is an invalid operation
912    if module.params['tracking_id']:
913        if module.params['request_type'] == 'new' or module.params['request_type'] == 'validate_only':
914            module.fail_json(msg='The tracking_id field is invalid when request_type="{0}".'.format(module.params['request_type']))
915
916    # A reissued request can not specify an expiration date or lifetime
917    if module.params['request_type'] == 'reissue':
918        if module.params['cert_expiry']:
919            module.fail_json(msg='The cert_expiry field is invalid when request_type="reissue".')
920        elif module.params['cert_lifetime']:
921            module.fail_json(msg='The cert_lifetime field is invalid when request_type="reissue".')
922    # Only a reissued request can omit the CSR
923    else:
924        module_params_csr = module.params['csr']
925        if module_params_csr is None:
926            module.fail_json(msg='The csr field is required when request_type={0}'.format(module.params['request_type']))
927        elif not os.path.exists(module_params_csr):
928            module.fail_json(msg='The csr field of {0} was not a valid path. csr is required when request_type={1}'.format(
929                module_params_csr, module.params['request_type']))
930
931    if module.params['ou'] and len(module.params['ou']) > 1:
932        module.fail_json(msg='Multiple "ou" values are not currently supported.')
933
934    if module.params['end_user_key_storage_agreement']:
935        if module.params['cert_type'] != 'CODE_SIGNING' and module.params['cert_type'] != 'EV_CODE_SIGNING':
936            module.fail_json(msg='Parameter "end_user_key_storage_agreement" is valid only for cert_types "CODE_SIGNING" and "EV_CODE_SIGNING"')
937
938    if module.params['org'] and module.params['client_id'] != 1 and module.params['cert_type'] != 'PD_SSL':
939        module.fail_json(msg='The "org" parameter is not supported when client_id parameter is set to a value other than 1, unless cert_type is "PD_SSL".')
940
941    if module.params['cert_expiry']:
942        if not validate_cert_expiry(module.params['cert_expiry']):
943            module.fail_json(msg='The "cert_expiry" parameter of "{0}" is not a valid date or date-time'.format(module.params['cert_expiry']))
944
945    certificate = EcsCertificate(module)
946    certificate.request_cert(module)
947    result = certificate.dump()
948    module.exit_json(**result)
949
950
951if __name__ == '__main__':
952    main()
953