win_certificate_store: added new module (#33980)

* win_certificate_store: added new module

* added warning about become or credssp for pfx
This commit is contained in:
Jordan Borean 2018-01-09 05:44:24 +10:00 committed by GitHub
commit b2a415daae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1471 additions and 0 deletions

View file

@ -0,0 +1,252 @@
#!powershell
# This file is part of Ansible
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy
$ErrorActionPreference = "Stop"
$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues()
$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues()
$params = Parse-Args $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "exported", "present"
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty ($state -eq "present" -or $state -eq "exported")
$thumbprint = Get-AnsibleParam -obj $params -name "thumbprint" -type "str" -failifempty ($state -eq "exported")
$store_name = Get-AnsibleParam -obj $params -name "store_name" -type "str" -default "My" -validateset $store_name_values
$store_location = Get-AnsibleParam -obj $params -name "store_location" -type "str" -default "LocalMachine" -validateset $store_location_values
$password = Get-AnsibleParam -obj $params -name "password" -type "str"
$key_exportable = Get-AnsibleParam -obj $params -name "key_exportable" -type "bool" -default $true
$key_storage = Get-AnsibleParam -obj $param -name "key_storage" -type "str" -default "default" -validateset "default", "machine", "user"
$file_type = Get-AnsibleParam -obj $params -name "file_type" -type "str" -default "der" -validateset "der", "pem", "pkcs12"
$result = @{
changed = $false
thumbprints = @()
}
Function Get-CertFile($path, $password, $key_exportable, $key_storage) {
# parses a certificate file and returns X509Certificate2Collection
if (-not (Test-Path -Path $path -PathType Leaf)) {
Fail-Json -obj $result -message "File at '$path' either does not exist or is not a file"
}
# must set at least the PersistKeySet flag so that the PrivateKey
# is stored in a permanent container and not deleted once the handle
# is gone.
$store_flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
$key_storage = $key_storage.substring(0,1).ToUpper() + $key_storage.substring(1).ToLower()
$store_flags = $store_flags -bor [Enum]::Parse([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags], "$($key_storage)KeySet")
if ($key_exportable) {
$store_flags = $store_flags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
}
# TODO: If I'm feeling adventurours, write code to parse PKCS#12 PEM encoded
# file as .NET does not have an easy way to import this
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
try {
$certs.Import($path, $password, $store_flags)
} catch {
Fail-Json -obj $result -message "Failed to load cert from file: $($_.Exception.Message)"
}
return $certs
}
Function New-CertFile($cert, $path, $type, $password) {
$content_type = switch ($type) {
"pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
"der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
"pkcs12" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12 }
}
if ($type -eq "pkcs12") {
$missing_key = $false
if ($cert.PrivateKey -eq $null) {
$missing_key = $true
} elseif ($cert.PrivateKey.CspKeyContainerInfo.Exportable -eq $false) {
$missing_key = $true
}
if ($missing_key) {
Fail-Json -obj $result -message "Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accesible by the current user"
}
}
if (Test-Path -Path $path) {
Remove-Item -Path $path -Force
$result.changed = $true
}
try {
$cert_bytes = $cert.Export($content_type, $password)
} catch {
Fail-Json -obj $result -message "Failed to export certificate as bytes: $($_.Exception.Message)"
}
# Need to manually handle a PEM file
if ($type -eq "pem") {
$cert_content = "-----BEGIN CERTIFICATE-----`r`n"
$base64_string = [System.Convert]::ToBase64String($cert_bytes, [System.Base64FormattingOptions]::InsertLineBreaks)
$cert_content += $base64_string
$cert_content += "`r`n-----END CERTIFICATE-----"
$file_encoding = [System.Text.Encoding]::ASCII
$cert_bytes = $file_encoding.GetBytes($cert_content)
} elseif ($type -eq "pkcs12") {
$result.key_exported = $false
if ($cert.PrivateKey -ne $null) {
$result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable
}
}
if (-not $check_mode) {
try {
[System.IO.File]::WriteAllBytes($path, $cert_bytes)
} catch [System.ArgumentNullException] {
Fail-Json -obj $result -message "Failed to write cert to file, cert was null: $($_.Exception.Message)"
} catch [System.IO.IOException] {
Fail-Json -obj $result -message "Failed to write cert to file due to IO exception: $($_.Exception.Message)"
} catch [System.UnauthorizedAccessException, System>Security.SecurityException] {
Fail-Json -obj $result -message "Failed to write cert to file due to permission: $($_.Exception.Message)"
} catch {
Fail-Json -obj $result -message "Failed to write cert to file: $($_.Exception.Message)"
}
}
$result.changed = $true
}
Function Get-CertFileType($path, $password) {
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
try {
$certs.Import($path, $password, 0)
} catch [System.Security.Cryptography.CryptographicException] {
# the file is a pkcs12 we just had the wrong password
return "pkcs12"
} catch {
return "unknown"
}
$file_contents = Get-Content -Path $path -Raw
if ($file_contents.StartsWith("-----BEGIN CERTIFICATE-----")) {
return "pem"
} elseif ($file_contents.StartsWith("-----BEGIN PKCS7-----")) {
return "pkcs7-ascii"
} elseif ($certs.Count -gt 1) {
# multiple certs must be pkcs7
return "pkcs7-binary"
} elseif ($certs[0].HasPrivateKey) {
return "pkcs12"
} elseif ($path.EndsWith(".pfx") -or $path.EndsWith(".p12")) {
# no way to differenciate a pfx with a der file so we must rely on the
# extension
return "pkcs12"
} else {
return "der"
}
}
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]::$store_name
$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]::$store_location
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
try {
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
} catch [System.Security.Cryptography.CryptographicException] {
Fail-Json -obj $result -message "Unable to open the store as it is not readable: $($_.Exception.Message)"
} catch [System.Security.SecurityException] {
Fail-Json -obj $result -message "Unable to open the store with the current permissions: $($_.Exception.Message)"
} catch {
Fail-Json -obj $result -message "Unable to open the store: $($_.Exception.Message)"
}
$store_certificates = $store.Certificates
try {
if ($state -eq "absent") {
$cert_thumbprints = @()
if ($path -ne $null) {
$certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
foreach ($cert in $certs) {
$cert_thumbprints += $cert.Thumbprint
}
} elseif ($thumbprint -ne $null) {
$cert_thumbprints += $thumbprint
} else {
Fail-Json -obj $result -message "Either path or thumbprint must be set when state=absent"
}
foreach ($cert_thumbprint in $cert_thumbprints) {
$result.thumbprints += $cert_thumbprint
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert_thumbprint, $false)
if ($found_certs.Count -gt 0) {
foreach ($found_cert in $found_certs) {
try {
if (-not $check_mode) {
$store.Remove($found_cert)
}
} catch [System.Security.SecurityException] {
Fail-Json -obj $result -message "Unable to remove cert with thumbprint '$cert_thumbprint' with the current permissions: $($_.Exception.Message)"
} catch {
Fail-Json -obj $result -message "Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)"
}
$result.changed = $true
}
}
}
} elseif ($state -eq "exported") {
# TODO: Add support for PKCS7 and exporting a cert chain
$result.thumbprints += $thumbprint
$export = $true
if (Test-Path -Path $path -PathType Container) {
Fail-Json -obj $result -message "Cannot export cert to path '$path' as it is a directory"
} elseif (Test-Path -Path $path -PathType Leaf) {
$actual_cert_type = Get-CertFileType -path $path -password $password
if ($actual_cert_type -eq $file_type) {
try {
$certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
} catch {
# failed to load the file so we set the thumbprint to something
# that will fail validation
$certs = @{Thumbprint = $null}
}
if ($certs.Thumbprint -eq $thumbprint) {
$export = $false
}
}
}
if ($export) {
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
if ($found_certs.Count -ne 1) {
Fail-Json -obj $result -message "Found $($found_certs.Count) certs when only expecting 1"
}
New-CertFile -cert $found_certs -path $path -type $file_type -password $password
}
} else {
$certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
foreach ($cert in $certs) {
$result.thumbprints += $cert.Thumbprint
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false)
if ($found_certs.Count -eq 0) {
try {
if (-not $check_mode) {
$store.Add($cert)
}
} catch [System.Security.Cryptography.CryptographicException] {
Fail-Json -obj $result -message "Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)"
} catch {
Fail-Json -obj $result -message "Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)"
}
$result.changed = $true
}
}
}
} finally {
$store.Close()
}
Exit-Json -obj $result

View file

@ -0,0 +1,195 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# This file is part of Ansible
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: win_certificate_store
version_added: '2.5'
short_description: Manages the certificate store
description:
- Used to import/export and remove certificates and keys from the local
certificate store.
- This module is not used to create certificates and will only manage existing
certs as a file or in the store.
- It can be used to import PEM, DER, P7B, PKCS12 (PFX) certificates and export
PEM, DER and PKCS12 certificates.
options:
state:
description:
- If C(present), will ensure that the certificate at I(path) is imported
into the certificate store specified.
- If C(absent), will ensure that the certificate specified by I(thumbprint)
or the thumbprint of the cert at I(path) is removed from the store
specified.
- If C(exported), will ensure the file at I(path) is a certificate
specified by I(thumbprint).
- When exporting a certificate, if I(path) is a directory then the module
will fail, otherwise the file will be replaced if needed.
default: present
choices:
- present
- absent
- exported
path:
description:
- The path to a certificate file.
- This is required when I(state) is C(present) or C(exported).
- When I(state) is C(absent) and I(thumbprint) is not specified, the
thumbprint is derived from the certificate at this path.
thumbprint:
description:
- The thumbprint as a hex string to either export or remove.
- See the examples for how to specify the thumbprint.
store_name:
description:
- The store name to use when importing a certificate or searching for a
certificate.
default: My
choices:
- AddressBook
- AuthRoot
- CertificateAuthority
- Disallowed
- My
- Root
- TrustedPeople
- TrustedPublisher
store_location:
description:
- The store location to use when importing a certificate or searching for a
certificate.
default: LocalMachine
choices:
- CurrentUser
- LocalMachine
password:
description:
- The password of the pkcs12 certificate key.
- This is used when reading a pkcs12 certificate file or the password to
set when C(state=exported) and C(file_type=pkcs12).
- If the pkcs12 file has no password set or no password should be set on
the exported file, do not set this option.
key_exportable:
description:
- Whether to allow the private key to be exported.
- If C(no), then this module and other process will only be able to export
the certificate and the private key cannot be exported.
- Used when C(state=present) only.
type: bool
default: 'yes'
key_storage:
description:
- Specifies where Windows will store the private key when it is imported.
- When set to C(default), the default option as set by Windows is used.
- When set to C(machine), the key is stored in a path accessible by various
users.
- When set to C(user), the key is stored in a path only accessible by the
current user.
- Used when C(state=present) only and cannot be changed once imported.
- See U(https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509keystorageflags.aspx)
for more details.
choices:
- default
- machine
- user
default: default
file_type:
description:
- The file type to export the certificate as when C(state=exported).
- C(der) is a binary ASN.1 encoded file.
- C(pem) is a base64 encoded file of a der file in the OpenSSL form.
- C(pkcs12) (also known as pfx) is a binary container that contains both
the certificate and private key unlike the other options.
- When C(pkcs12) is set and the private key is not exportable or accessible
by the current user, it will throw an exception.
choices:
- der
- pem
- pkcs12
default: der
notes:
- Some actions on PKCS12 certificates and keys may fail with the error
C(the specified network password is not correct), either use CredSSP or
Kerberos with credential delegation, or use C(become) to bypass these
restrictions.
- The certificates must be located on the Windows host to be set with I(path).
author:
- Jordan Borean (@jborean93)
'''
EXAMPLES = r'''
- name: import a certificate
win_certificate_store:
path: C:\temp\cert.pem
state: present
- 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
- name: import pfx certificate without password and set private key as un-exportable
win_certificate_store:
path: C:\temp\cert.pfx
state: present
key_exportable: no
# usually you don't set this here but it is for illustrative purposes
vars:
ansible_winrm_transport: credssp
- name: remove a certificate based on file thumbprint
win_certificate_store:
path: C:\temp\cert.pem
state: absent
- name: remove a certificate based on thumbprint
win_certificate_store:
thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
state: absent
- name: remove certificate based on thumbprint is CurrentUser/TrustedPublishers store
win_certificate_store:
thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
state: absent
store_location: CurrentUser
store_name: TrustedPublisher
- name: export certificate as der encoded file
win_certificate_store:
path: C:\temp\cert.cer
state: exported
file_type: der
- name: export certificate and key as pfx encoded file
win_certificate_store:
path: C:\temp\cert.pfx
state: exported
file_type: pkcs12
password: AnotherStrongPass!
become: yes
become_method: runas
become_user: SYSTEM
'''
RETURN = r'''
thumbprints:
description: A list of certificate thumbprints that were touched by the
module.
returned: success
type: list
sample: ["BC05633694E675449136679A658281F17A191087"]
'''