mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-22 21:00:22 -07:00
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:
parent
34206a0402
commit
b2a415daae
11 changed files with 1471 additions and 0 deletions
252
lib/ansible/modules/windows/win_certificate_store.ps1
Normal file
252
lib/ansible/modules/windows/win_certificate_store.ps1
Normal 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
|
195
lib/ansible/modules/windows/win_certificate_store.py
Normal file
195
lib/ansible/modules/windows/win_certificate_store.py
Normal 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"]
|
||||
'''
|
Loading…
Add table
Add a link
Reference in a new issue