#!powershell # 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 $params -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 -LiteralPath $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 -LiteralPath $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] { 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 -LiteralPath $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 ($null -ne $thumbprint) { $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 -LiteralPath $path -PathType Container) { Fail-Json -obj $result -message "Cannot export cert to path '$path' as it is a directory" } elseif (Test-Path -LiteralPath $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