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