diff --git a/lib/ansible/modules/windows/win_certificate_info.ps1 b/lib/ansible/modules/windows/win_certificate_info.ps1 new file mode 100644 index 00000000000..b1ff876479c --- /dev/null +++ b/lib/ansible/modules/windows/win_certificate_info.ps1 @@ -0,0 +1,132 @@ +#!powershell + +# Copyright: (c) 2019, Micah Hunsberger +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +function ConvertTo-Timestamp($start_date, $end_date) +{ + if ($start_date -and $end_date) + { + return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds + } +} + +function Format-Date([DateTime]$date) +{ + return $date.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssK') +} + +function Get-CertificateInfo ($cert) +{ + $epoch_date = Get-Date -Date "01/01/1970" + + $cert_info = @{ extensions = @() } + $cert_info.friendly_name = $cert.FriendlyName + $cert_info.thumbprint = $cert.Thumbprint + $cert_info.subject = $cert.Subject + $cert_info.issuer = $cert.Issuer + $cert_info.valid_from = (ConvertTo-Timestamp -start_date $epoch_date -end_date $cert.NotBefore.ToUniversalTime()) + $cert_info.valid_from_iso8601 = Format-Date -date $cert.NotBefore + $cert_info.valid_to = (ConvertTo-Timestamp -start_date $epoch_date -end_date $cert.NotAfter.ToUniversalTime()) + $cert_info.valid_to_iso8601 = Format-Date -date $cert.NotAfter + $cert_info.serial_number = $cert.SerialNumber + $cert_info.archived = $cert.Archived + $cert_info.version = $cert.Version + $cert_info.has_private_key = $cert.HasPrivateKey + $cert_info.issued_by = $cert.GetNameInfo('SimpleName', $true) + $cert_info.issued_to = $cert.GetNameInfo('SimpleName', $false) + $cert_info.signature_algorithm = $cert.SignatureAlgorithm.FriendlyName + $cert_info.dns_names = [System.Collections.Generic.List`1[String]]@($cert_info.issued_to) + $cert_info.raw = [System.Convert]::ToBase64String($cert.GetRawCertData()) + $cert_info.public_key = [System.Convert]::ToBase64String($cert.GetPublicKey()) + if ($cert.Extensions.Count -gt 0) + { + [array]$cert_info.extensions = foreach ($extension in $cert.Extensions) + { + $extension_info = @{ + critical = $extension.Critical + field = $extension.Oid.FriendlyName + value = $extension.Format($false) + } + if ($extension -is [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]) + { + $cert_info.is_ca = $extension.CertificateAuthority + $cert_info.path_length_constraint = $extension.PathLengthConstraint + } + elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]) + { + $cert_info.intended_purposes = $extension.EnhancedKeyUsages.FriendlyName -as [string[]] + } + elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]) + { + $cert_info.key_usages = $extension.KeyUsages.ToString().Split(',').Trim() -as [string[]] + } + elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]) + { + $cert_info.ski = $extension.SubjectKeyIdentifier + } + elseif ($extension.Oid.value -eq '2.5.29.17') + { + $sans = $extension.Format($true).Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries) + foreach ($san in $sans) + { + $san_parts = $san.Split("=") + if ($san_parts.Length -ge 2 -and $san_parts[0].Trim() -eq 'DNS Name') + { + $cert_info.dns_names.Add($san_parts[1].Trim()) + } + } + } + $extension_info + } + } + return $cert_info +} + +$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() } + +$spec = @{ + options = @{ + thumbprint = @{ type = "str"; required = $false } + store_name = @{ type = "str"; default = "My"; } + store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values; } + } +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$thumbprint = $module.Params.thumbprint +$store_name = $module.Params.store_name +$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)" + +$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location +$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) + +$module.Result.exists = $false +$module.Result.certificates = @() + +try +{ + if ($null -ne $thumbprint) + { + $found_certs = $store.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false) + } + else + { + $found_certs = $store.Certificates + } + + if ($found_certs.Count -gt 0) + { + $module.Result.exists = $true + [array]$module.Result.certificates = $found_certs | ForEach-Object { Get-CertificateInfo -cert $_ } | Sort-Object -Property { $_.thumbprint } + } +} +finally +{ + $store.Close() +} + +$module.ExitJson() diff --git a/lib/ansible/modules/windows/win_certificate_info.py b/lib/ansible/modules/windows/win_certificate_info.py new file mode 100644 index 00000000000..c8a75731cac --- /dev/null +++ b/lib/ansible/modules/windows/win_certificate_info.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Ansible, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_certificate_info +version_added: "2.10" +short_description: Get information on certificates from a Windows Certificate Store +description: +- Returns information about certificates in a Windows Certificate Store. +options: + thumbprint: + description: + - The thumbprint as a hex string of a certificate to find. + - When specified, filters the I(certificates) return value to a single certificate + - See the examples for how to format the thumbprint. + type: str + required: no + store_name: + description: + - The name of the store to search. + - See U(https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.storename) + for a list of built-in store names. + type: str + default: My + store_location: + description: + - The location of the store to search. + type: str + choices: [ CurrentUser, LocalMachine ] + default: LocalMachine +seealso: +- module: win_certificate_store +author: +- Micah Hunsberger (@mhunsber) +''' + +EXAMPLES = r''' +- name: Obtain information about a particular certificate in the computer's personal store + win_certificate_info: + thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27 + register: mycert + +# thumbprint can also be lower case +- name: Obtain information about a particular certificate in the computer's personal store + win_certificate_info: + thumbprint: bd7af104cf1872bdb518d95c9534ea941665fd27 + register: mycert + +- name: Obtain information about all certificates in the root store + win_certificate_info: + store_name: Root + register: ca + +# Import a pfx and then get information on the certificates +- name: Import pfx certificate that is password protected + win_certificate_store: + path: C:\Temp\cert.pfx + state: present + password: VeryStrongPasswordHere! + become: yes + become_method: runas + register: mycert + +- name: Obtain information on each certificate that was touched + win_certificate_info: + thumbprint: "{{ item }}" + register: mycert_stats + loop: "{{ mycert.thumbprints }}" +''' + +RETURN = r''' +exists: + description: + - Whether any certificates were found in the store. + - When I(thumbprint) is specified, returns true only if the certificate mathing the thumbprint exists. + returned: success + type: bool + sample: true +certificates: + description: + - A list of information about certificates found in the store, sorted by thumbprint. + returned: success + type: list + elements: dict + contains: + archived: + description: Indicates that the certificate is archived. + type: bool + sample: false + dns_names: + description: Lists the registered dns names for the certificate. + type: list + elements: str + sample: [ '*.m.wikiquote.org', '*.wikipedia.org' ] + extensions: + description: The collection of the certificates extensions. + type: list + elements: dict + sample: [ + { + "critical": false, + "field": "Subject Key Identifier", + "value": "88 27 17 09 a9 b6 18 60 8b ec eb ba f6 47 59 c5 52 54 a3 b7" + }, + { + "critical": true, + "field": "Basic Constraints", + "value": "Subject Type=CA, Path Length Constraint=None" + }, + { + "critical": false, + "field": "Authority Key Identifier", + "value": "KeyID=2b d0 69 47 94 76 09 fe f4 6b 8d 2e 40 a6 f7 47 4d 7f 08 5e" + }, + { + "critical": false, + "field": "CRL Distribution Points", + "value": "[1]CRL Distribution Point: Distribution Point Name:Full Name:URL=http://crl.apple.com/root.crl" + }, + { + "critical": true, + "field": "Key Usage", + "value": "Digital Signature, Certificate Signing, Off-line CRL Signing, CRL Signing (86)" + }, + { + "critical": false, + "field": null, + "value": "05 00" + } + ] + friendly_name: + description: The associated alias for the certificate. + type: str + sample: Microsoft Root Authority + has_private_key: + description: Indicates that the certificate contains a private key. + type: bool + sample: false + intended_purposes: + description: lists the intended applications for the certificate. + returned: enhanced key usages extension exists. + type: list + sample: [ "Server Authentication" ] + is_ca: + description: Indicates that the certificate is a certificate authority (CA) certificate. + returned: basic constraints extension exists. + type: bool + sample: true + issued_by: + description: The certificate issuer's common name. + type: str + sample: Apple Root CA + issued_to: + description: The certificate's common name. + type: str + sample: Apple Worldwide Developer Relations Certification Authority + issuer: + description: The certificate issuer's distinguished name. + type: str + sample: 'CN=Apple Root CA, OU=Apple Certification Authority, O=Apple Inc., C=US' + key_usages: + description: + - Defines how the certificate key can be used. + - If this value is not defined, the key can be used for any purpose. + returned: key usages extension exists. + type: list + elements: str + sample: [ "CrlSign", "KeyCertSign", "DigitalSignature" ] + path_length_constraint: + description: + - The number of levels allowed in a certificates path. + - If this value is 0, the certificate does not have a restriction. + returned: basic constraints extension exists + type: int + sample: 0 + public_key: + description: The base64 encoded public key of the certificate. + type: str + cert_data: + description: The base64 encoded data of the entire certificate. + type: str + serial_number: + description: The serial number of the certificate represented as a hexadecimal string + type: str + sample: 01DEBCC4396DA010 + signature_algorithm: + description: The algorithm used to create the certificate's signature + type: str + sample: sha1RSA + ski: + description: The certificate's subject key identifier + returned: subject key identifier extension exists. + type: str + sample: 88271709A9B618608BECEBBAF64759C55254A3B7 + subject: + description: The certificate's distinguished name. + type: str + sample: 'CN=Apple Worldwide Developer Relations Certification Authority, OU=Apple Worldwide Developer Relations, O=Apple Inc., C=US' + thumbprint: + description: + - The thumbprint as a hex string of the certificate. + - The return format will always be upper case. + type: str + sample: FF6797793A3CD798DC5B2ABEF56F73EDC9F83A64 + valid_from: + description: The start date of the certificate represented in seconds since epoch. + type: float + sample: 1360255727 + valid_from_iso8601: + description: The start date of the certificate represented as an iso8601 formatted date. + type: str + sample: '2017-12-15T08:39:32Z' + valid_to: + description: The expiry date of the certificate represented in seconds since epoch. + type: float + sample: 1675788527 + valid_to_iso8601: + description: The expiry date of the certificate represented as an iso8601 formatted date. + type: str + sample: '2086-01-02T08:39:32Z' + version: + description: The x509 format version of the certificate + type: int + sample: 3 +''' diff --git a/test/integration/targets/win_certificate_info/aliases b/test/integration/targets/win_certificate_info/aliases new file mode 100644 index 00000000000..6036e173f1a --- /dev/null +++ b/test/integration/targets/win_certificate_info/aliases @@ -0,0 +1 @@ +shippable/windows/group7 diff --git a/test/integration/targets/win_certificate_info/defaults/main.yml b/test/integration/targets/win_certificate_info/defaults/main.yml new file mode 100644 index 00000000000..b8d5a0f90f2 --- /dev/null +++ b/test/integration/targets/win_certificate_info/defaults/main.yml @@ -0,0 +1,3 @@ +win_cert_dir: '{{win_output_dir}}\win_certificate .ÅÑŚÌβŁÈ [$!@^&test(;)]' +subj_thumbprint: 'BD7AF104CF1872BDB518D95C9534EA941665FD27' +root_thumbprint: 'BC05633694E675449136679A658281F17A191087' diff --git a/test/integration/targets/win_certificate_info/files/root-cert.pem b/test/integration/targets/win_certificate_info/files/root-cert.pem new file mode 100644 index 00000000000..edbe6b86848 --- /dev/null +++ b/test/integration/targets/win_certificate_info/files/root-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDKDCCAhCgAwIBAgIJAP1vIdGgMJv/MA0GCSqGSIb3DQEBCwUAMCgxGTAXBgNV +BAMMEHJvb3QuYW5zaWJsZS5jb20xCzAJBgNVBAYTAlVTMCAXDTE3MTIxNTA4Mzkz +MloYDzIwODYwMTAyMDgzOTMyWjAoMRkwFwYDVQQDDBByb290LmFuc2libGUuY29t +MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMmq +YT8eZY6rFQKnmScUGnnUH1tLQ+3WQpfKiWygCUSb1CNqO3J1u3pGMEqYM58LK4Kr +Mpskv7K1tCV/EMZqGTqXAIfSLy9umlb/9C3AhL9thBPn5I9dam/EmrIZktI9/w5Y +wBXn4toe+OopA3QkMQh9BUjUCPb9fdOI+ir7OGFZMmxXmiM64+BEeywM2oSGsdZ9 +5hU378UBu2IX4+OAV8Fbr2l6VW+Fxg/tKIOo6Bs46Pa4EZgtemOqs3kxYBOltBTb +vFcLsLa4KYVu5Ge5YfB0Axfaem7PoP8IlMs8gxyojZ/r0o5hzxUcYlL/h8GeeoLW +PFFdiAS+UgxWINOqNXMCAwEAAaNTMFEwHQYDVR0OBBYEFLp9k4LmOnAR4ROrqhb+ +CFdbk2+oMB8GA1UdIwQYMBaAFLp9k4LmOnAR4ROrqhb+CFdbk2+oMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGksycHsjGbXfWfuhQh+CvXk/A2v +MoNgiHtNMTGliVNgoVp1B1rj4x9xyZ8YrO8GAmv8jaCwCShd0B5Ul4aZVk1wglVv +lFAwb4IAZN9jv9+fw5BRzQ2tLhkVWIEwx6pZkhGhhjBvMaplLN5JwBtsdZorFbm7 +wuKiUKcFAM28acoOhCmOhgyNNBZpZn5wXaQDY43AthJOhitAV7vph4MPUkwIJnOh +MA5GJXEqS58TE9z9pkhQnn9598G8tmOXyA2erAoM9JAXM3EYHxVpoHBb9QRj6WAw +XVBo6qRXkwjNEM5CbnD4hVIBsdkOGsDrgd4Q5izQZ3x+jFNkdL/zPsXjJFw= +-----END CERTIFICATE----- + diff --git a/test/integration/targets/win_certificate_info/files/subj-cert.pem b/test/integration/targets/win_certificate_info/files/subj-cert.pem new file mode 100644 index 00000000000..6d9ec39c734 --- /dev/null +++ b/test/integration/targets/win_certificate_info/files/subj-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC0TCCAbkCCQC/MtOBa1UDpzANBgkqhkiG9w0BAQsFADAoMRkwFwYDVQQDDBBy +b290LmFuc2libGUuY29tMQswCQYDVQQGEwJVUzAgFw0xNzEyMTUwODU2MzBaGA8y +MDg2MDEwMjA4NTYzMFowKzEcMBoGA1UEAwwTc3ViamVjdC5hbnNpYmxlLmNvbTEL +MAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDszqdF +So3GlVP1xUnN4bSPrFRFiOl/Mqup0Zn5UJJUR9wLnRD+OLcq7kKin6hYqozSu7cC ++BnWQoq7vGSSNVqv7BqFMwzGJt9IBUQv0UqIQkA/duUdKdAiMn2PQRsNDnkWEbTj +4xsitItVNv84cDG0lkZBYyTgfyZlZLZWplkpUQkrZhoFCekZRJ+ODrqNW3W560rr +OUIh+HiQeBqocat6OdxgICBqpUh8EVo1iha3DXjGN08q5utg6gmbIl2VBaVJjfyd +wnUSqHylJwh6WCIEh+HXsn4ndfNWSN/fDqvi5I10V1j6Zos7yqQf8qAezUAm6eSq +hLgZz0odq9DsO4HHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFK5mVIJ2D+kI0kk +sxnW4ibWFjzlYFYPYrZg+2JFIVTbKBg1YzyhuIKm0uztqRxQq5iLn/C/uponHoqF +7KDQI37KAJIQdgSva+mEuO9bZAXg/eegail2hN6np7HjOKlPu23s40dAbFrbcOWP +VbsBEPDP0HLv6OgbQWzNlE9HO1b7pX6ozk3q4ULO7IR85P6OHYsBBThL+qsOTzg/ +gVknuB9+n9hgNqZcAcXBLDetOM9aEmYJCGk0enYP5UGLYpseE+rTXFbRuHTPr1o6 +e8BetiSWS/wcrV4ZF5qr9NiYt5eD6JzTB5Rn5awxxj0FwMtrBu003lLQUWxsuTzz +35/RLY4= +-----END CERTIFICATE----- + diff --git a/test/integration/targets/win_certificate_info/meta/main.yml b/test/integration/targets/win_certificate_info/meta/main.yml new file mode 100644 index 00000000000..bdea853d75a --- /dev/null +++ b/test/integration/targets/win_certificate_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- prepare_win_tests diff --git a/test/integration/targets/win_certificate_info/tasks/main.yml b/test/integration/targets/win_certificate_info/tasks/main.yml new file mode 100644 index 00000000000..06bd6802c6a --- /dev/null +++ b/test/integration/targets/win_certificate_info/tasks/main.yml @@ -0,0 +1,88 @@ +### keys in files/ have been generated with +# generate root private key +# openssl genrsa -aes256 -out enckey.pem 2048 +# openssl rsa -in envkey.pem -out root-key.pem +# +# generate root certificate +# openssl req -x509 -key root-key.pem -days 24855 -out root-vert.pem -subj "/CN=root.ansible.com/C=US" +# +# generate subject private key +# openssl genrsa -aes256 -out enckey.pem 2048 +# openssl rsa -in enckey.pem -out subj-key.pem +# +# generate subject certificate +# openssl req -new -key subj-key.pem -out cert.csr -subj "/CN=subject.ansible.com/C=US" +# openssl x509 -req -in cert.csr -CA root-cert.pem -CAkey root-key.pem -CAcreateserial -out subj-cert.pem -days 24855 +### +--- +- name: ensure test dir is present + win_file: + path: '{{win_cert_dir}}\exported' + state: directory + +- name: copy across test cert files + win_copy: + src: files/ + dest: '{{win_cert_dir}}' + +- name: subject cert imported to personal store + win_certificate_store: + path: '{{win_cert_dir}}\subj-cert.pem' + state: present + store_name: My + +- name: root certificate imported to trusted root + win_certificate_store: + path: '{{win_cert_dir}}\root-cert.pem' + store_name: Root + state: present + +- name: get raw root certificate + shell: 'cat root-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: root_raw + delegate_to: localhost + +- name: get public key of root certificate + shell: 'openssl x509 -pubkey -noout -in root-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: root_pub + delegate_to: localhost + +- name: get subject certificate + shell: 'cat subj-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: subj_raw + delegate_to: localhost + +- name: get public key of subject certificate + shell: 'openssl x509 -pubkey -noout -in subj-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: subj_pub + delegate_to: localhost + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: ensure subject cert removed from personal store + win_certificate_store: + thumbprint: '{{subj_thumbprint}}' + state: absent + store_name: My + + - name: ensure root cert removed from trusted root + win_certificate_store: + thumbprint: '{{root_thumbprint}}' + state: absent + store_name: Root + + - name: ensure test dir is deleted + win_file: + path: '{{win_cert_dir}}' + state: absent diff --git a/test/integration/targets/win_certificate_info/tasks/tests.yml b/test/integration/targets/win_certificate_info/tasks/tests.yml new file mode 100644 index 00000000000..90eb0870bf7 --- /dev/null +++ b/test/integration/targets/win_certificate_info/tasks/tests.yml @@ -0,0 +1,90 @@ +--- + +- name: get stats on a store that doesn't exist + win_certificate_info: + store_name: teststore + register: test_store + +- name: ensure exists is false + assert: + that: + - test_store.exists == false + +- name: get stats on the root certificate store + win_certificate_info: + store_name: Root + register: root_store + +- name: at least one certificate is returned + assert: + that: + - "root_store.exists" + - "root_store.certificates | length > 0" + +- name: get stats on a certificate that doesn't exist + win_certificate_info: + thumbprint: ABC + register: actual + +- name: ensure exists is false + assert: + that: actual.exists == false + +- name: get stats on root certificate + win_certificate_info: + thumbprint: '{{ root_thumbprint }}' + store_name: Root + register: root_stats + +- name: root certificate stats returned are expected values + assert: + that: + - root_stats.exists + - root_stats.certificates[0].archived == false + - root_stats.certificates[0].dns_names == [ 'root.ansible.com' ] + - root_stats.certificates[0].extensions|count == 3 + - root_stats.certificates[0].has_private_key == false + - root_stats.certificates[0].issued_by == 'root.ansible.com' + - root_stats.certificates[0].issued_to == 'root.ansible.com' + - root_stats.certificates[0].issuer == 'C=US, CN=root.ansible.com' + - root_stats.certificates[0].path_length_constraint == 0 +# - root_stats.certificates[0].public_key == (root_pub.stdout_lines|join()) + - root_stats.certificates[0].raw == root_raw.stdout_lines|join() + - root_stats.certificates[0].serial_number == '00FD6F21D1A0309BFF' + - root_stats.certificates[0].signature_algorithm == 'sha256RSA' + - root_stats.certificates[0].ski == 'BA7D9382E63A7011E113ABAA16FE08575B936FA8' + - root_stats.certificates[0].subject == 'C=US, CN=root.ansible.com' + - root_stats.certificates[0].valid_from == 1513327172 + - root_stats.certificates[0].valid_from_iso8601 == '2017-12-15T08:39:32Z' + - root_stats.certificates[0].valid_to == 3660799172 + - root_stats.certificates[0].valid_to_iso8601 == '2086-01-02T08:39:32Z' + - root_stats.certificates[0].version == 3 + +- name: get stats on subject certificate + win_certificate_info: + thumbprint: '{{ subj_thumbprint }}' + register: subj_stats + +- name: subject certificate stats returned are expected values + assert: + that: + - subj_stats.exists + - subj_stats.certificates[0].archived == false + - subj_stats.certificates[0].dns_names == [ 'subject.ansible.com' ] + - subj_stats.certificates[0].extensions|count == 0 + - subj_stats.certificates[0].has_private_key == false + - subj_stats.certificates[0].issued_by == 'root.ansible.com' + - subj_stats.certificates[0].issued_to == 'subject.ansible.com' + - subj_stats.certificates[0].issuer == 'C=US, CN=root.ansible.com' + - subj_stats.certificates[0].path_length_constraint is undefined +# - subj_stats.certificates[0].public_key == subj_pub.stdout_lines|join() + - subj_stats.certificates[0].raw == subj_raw.stdout_lines|join() + - subj_stats.certificates[0].serial_number == '00BF32D3816B5503A7' + - subj_stats.certificates[0].signature_algorithm == 'sha256RSA' + - subj_stats.certificates[0].ski is undefined + - subj_stats.certificates[0].subject == 'C=US, CN=subject.ansible.com' + - subj_stats.certificates[0].valid_from == 1513328190 + - subj_stats.certificates[0].valid_from_iso8601 == '2017-12-15T08:56:30Z' + - subj_stats.certificates[0].valid_to == 3660800190 + - subj_stats.certificates[0].valid_to_iso8601 == '2086-01-02T08:56:30Z' + - subj_stats.certificates[0].version == 1