Publish preview version (#42)

* adds certificate store location

* add additional certificate store tests

* add cert store tests for New-CredentialStoreItem

* fix test

* add error handling for credential store path

* add Import-CSCertificate helper function

* Import new certificate if param is given

* fix extension filter

* add linux error message

* fix pester test for linux

* update cert helper functions

* export helper functions

* fix cs cert import

* simplify cs cret lookup

* remove obsolete functions

* fix pester test for linux

* fix error type for linux

* fix var name

* fix pester test

* disable travis artifact upload

* update cert lookup for item functions

* debug build error

* use cert instance constructor for linux

* disable debug output

* remove obsolete exports
This commit is contained in:
OCram85 2019-04-04 17:02:17 +02:00 committed by GitHub
parent 5a68527061
commit d92d963979
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 422 additions and 166 deletions

View File

@ -20,10 +20,9 @@ matrix:
fast_finish: true
addons:
artifacts:
#paths: $(ls ./../dist/PowerShellGet.zip | tr "\n" ":")
paths: ./dist/PowerShellGet.zip
#addons:
# artifacts:
# paths: ./dist/PowerShellGet.zip
install:

View File

@ -0,0 +1,81 @@
function Get-CSCertificate {
<#
.SYNOPSIS
Returns the certificate object given by thumbprint.
.DESCRIPTION
You can use this function to get a stored certificate. Search for the object by its unique thumbprint.
.PARAMETER Thumbprint
Provide one or more thumprints.
.PARAMETER StoreName
Select the store name in which you want to search the certificates.
.PARAMETER StoreLocation
Select between the both available locations CurrentUser odr LocalMachine.
.INPUTS
[string]
.OUTPUTS
[System.Security.Cryptography.X509Certificates.X509Certificate2[]]
.EXAMPLE
Get-CSCertificate -Thumbprint '12345678' -StoreName 'My' -StoreLocation 'CurrentUser'
.NOTES
File Name : Get-CSCertificate.ps1
Author : Marco Blessing - marco.blessing@googlemail.com
Requires :
.LINK
https://github.com/OCram85/PSCredentialStore
#>
[CmdletBinding()]
[OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string[]]$Thumbprint,
[Parameter(Mandatory = $false)]
[ValidateSet(
'AddressBook',
'AuthRoot',
'CertificateAuthority',
'Disallowed',
'My',
'Root',
'TrustedPeople',
'TrustedPublisher'
)]
[string]$StoreName = 'My',
[Parameter(Mandatory = $false)]
[ValidateSet(
'CurrentUser',
'LocalMachine'
)]
[string]$StoreLocation = 'CurrentUser'
)
begin {
$Store = [System.Security.Cryptography.X509Certificates.X509Store]::New($StoreName, $StoreLocation)
try {
$Store.Open('ReadOnly')
}
catch {
$_.Exception.Message | Write-Error -ErrorAction Stop
}
}
process {
foreach ($Thumb in $Thumbprint) {
Write-Output $Store.Certificates | Where-Object { $_.Thumbprint -eq $Thumb }
}
}
end {
$Store.Close()
}
}

View File

@ -0,0 +1,112 @@
function Import-CSCertificate {
<#
.SYNOPSIS
adds a given pfx certificate file to current uerers personal certificate store.
.DESCRIPTION
This function is used to import existing pfx certificate files. The Import-PFXCertificate cmdle from the
PKI module imports the certficate into a deprecated store. Thus you can't read the private key afterwards or
using it for decrypting data.
.PARAMETER Path
Path to an existing *.pfx certificate file.
.PARAMETER StoreName
Additionally you change change the store where you want the certificate into.
.INPUTS
[None]
.OUTPUTS
[None]
.EXAMPLE
Import-CSCertificate -Path (Join-Path -Path $Env:APPDATA -ChildPath '/PSCredentialStore.pfx')
.NOTES
File Name : Import-CSCertificate.ps1
Author : Marco Blessing - marco.blessing@googlemail.com
Requires :
.LINK
https://github.com/OCram85/PSCredentialStore
#>
[CmdletBinding()]
[OutputType()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[Parameter(Mandatory = $false)]
[ValidateSet(
'AddressBook',
'AuthRoot',
'CertificateAuthority',
'Disallowed',
'My',
'Root',
'TrustedPeople',
'TrustedPublisher'
)]
[string]$StoreName = 'My',
[Parameter(Mandatory = $false)]
[ValidateSet(
'CurrentUser',
'LocalMachine'
)]
[string]$StoreLocation = 'CurrentUser',
[Parameter(Mandatory = $false)]
[ValidateSet(
'ReadOnly',
'ReadWrite',
'MaxAllowed',
'OpenExistingOnly',
'InclueArchived'
)]
[string]$OpenFlags = 'ReadWrite'
)
begin {
$Store = [System.Security.Cryptography.X509Certificates.X509Store]::new($StoreName, $StoreLocation)
try {
$Store.Open($OpenFlags)
}
catch {
$_.Exception.Message | Write-Error -ErrorAction Stop
}
}
process {
try {
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
$Path,
$null,
(
[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bor
[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
)
)
if (Test-CSCertificate -Thumbprint $cert.Thumbprint) {
Write-Warning -Message ('The certificate with thumbprint {0} is already present!' -f $cert.Thumbprint)
}
else {
$Store.Add($cert)
}
}
catch {
$_.Exception.Message | Write-Error -ErrorAction Stop
$ErrorParams = @{
ErrorAction = 'Stop'
Exception = [System.Exception]::new(
'Could not read or add the pfx certificate!'
)
}
Write-Error @ErrorParams
}
}
end {
$Store.Close()
}
}

View File

@ -0,0 +1,86 @@
function Test-CSCertificate {
<#
.SYNOPSIS
Tests if the given certificate exists in a store.
.DESCRIPTION
Use this function to ensure if a certificate is already imported into a given store.
.PARAMETER Thumbprint
Provide one or more thumprints.
.PARAMETER StoreName
Select the store name in which you want to search the certificates.
.PARAMETER StoreLocation
Select between the both available locations CurrentUser odr LocalMachine.
.INPUTS
[None]
.OUTPUTS
[bool]
.EXAMPLE
Test-CSCertificate -Thumbprint '12345678' -StoreName 'My' -StoreLocation 'CurrentUser'
.NOTES
File Name : Test-CSCertificate.ps1
Author : Marco Blessing - marco.blessing@googlemail.com
Requires :
.LINK
https://github.com/OCram85/PSCredentialStore
#>
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]$Thumbprint,
[Parameter(Mandatory = $false)]
[ValidateSet(
'AddressBook',
'AuthRoot',
'CertificateAuthority',
'Disallowed',
'My',
'Root',
'TrustedPeople',
'TrustedPublisher'
)]
[string]$StoreName = 'My',
[Parameter(Mandatory = $false)]
[ValidateSet(
'CurrentUser',
'LocalMachine'
)]
[string]$StoreLocation = 'CurrentUser'
)
begin {
$Store = [System.Security.Cryptography.X509Certificates.X509Store]::New($StoreName, $StoreLocation)
try {
$Store.Open('ReadOnly')
}
catch {
$_.Exception.Message | Write-Error -ErrorAction Stop
}
}
process {
$Cert = $Store.Certificates | Where-Object { $_.Thumbprint -eq $Thumbprint }
if ($null -eq $Cert) {
return $false
}
else {
return $true
}
}
end {
$Store.Close()
}
}

View File

@ -87,7 +87,24 @@ function Get-CredentialStoreItem {
$CSMembers = Get-Member -InputObject $CS
# Let's first check if the given remote host exists as object property
if (($CSMembers.MemberType -eq "NoteProperty") -and ($CSMembers.Name -contains $CredentialName)) {
$Cert = Get-PfxCertificate -FilePath $CS.PfXCertificate -ErrorAction Stop
try {
if ($null -eq $CS.PfxCertificate) {
$Cert = Get-CSCertificate -Thumbprint $CS.Thumbprint
}
else {
$Cert = Get-PfxCertificate -FilePath $CS.PfxCertificate -ErrorAction Stop
}
}
catch {
$_.Exception.Message | Write-Error
$ErrorParams = @{
ErrorAction = 'Stop'
Exception = [System.Security.Cryptography.CryptographicException]::new(
'Could not read the given PFX certificate.'
)
}
Write-Error @ErrorParams
}
$DecryptedKey = $Cert.PrivateKey.Decrypt(
[Convert]::FromBase64String($CS.$CredentialName.EncryptedKey),
[System.Security.Cryptography.RSAEncryptionPadding]::Pkcs1

View File

@ -117,7 +117,21 @@ function New-CredentialStoreItem {
if ($Credential.UserName) {
try {
$Cert = Get-PfxCertificate -FilePath $CSContent.PfxCertificate -ErrorAction Stop
if ($null -eq $CSContent.PfxCertificate) {
$Cert = Get-CSCertificate -Thumbprint $CSContent.Thumbprint
if ($null -eq $Cert) {
$ErrorParams = @{
ErrorAction = 'Stop'
Exception = [System.Security.Cryptography.X509Certificates.FileNotFoundException]::new(
('Could not find the linked certificate with thumbprint {0}' -f $CSContent.Thumbprint)
)
}
Write-Error @ErrorParams
}
}
else {
$Cert = Get-PfxCertificate -FilePath $CSContent.PfxCertificate -ErrorAction Stop
}
}
catch {
$_.Exception.Message | Write-Error

View File

@ -103,14 +103,20 @@ function Set-CredentialStoreItem {
if ($Credential.UserName) {
try {
$Cert = Get-PfxCertificate -FilePath $CSContent.PfxCertificate -ErrorAction Stop
if ($null -eq $CSContent.PfxCertificate) {
$Cert = Get-CSCertificate -Thumbprint $CSContent.Thumbprint
}
else {
$Cert = Get-PfxCertificate -FilePath $CSContent.PfxCertificate -ErrorAction Stop
}
}
catch {
$_.Exception.Message | Write-Error
$ErrorParams = @{
Message = 'Could not read the given PFX certificate.'
ErrorAction = 'Stop'
Exception = [System.Security.Cryptography.CryptographicException]::new()
Exception = [System.Security.Cryptography.CryptographicException]::new(
'Could not read the given PFX certificate.'
)
}
Write-Error @ErrorParams
}

View File

@ -63,8 +63,11 @@
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @(
# Certificate
'Get-CSCertificate',
'Import-CSCertificate',
'New-CRTAttribute',
'New-PfxCertificate',
'Test-CSCertificate',
'Use-PfxCertificate',
# Connection
'Connect-To',
@ -79,8 +82,7 @@
# Store
'Get-CredentialStore',
'New-CredentialStore',
'Test-CredentialStore',
'Update-CredentialStore'
'Test-CredentialStore'
)
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.

View File

@ -59,19 +59,23 @@ function New-CredentialStore {
[Parameter(Mandatory = $false, ParameterSetName = "Shared")]
[ValidateNotNullOrEmpty()]
[string]$Path,
[System.IO.FileInfo]$Path,
[Parameter(Mandatory = $false, ParameterSetName = "Private")]
[Parameter(Mandatory = $false, ParameterSetName = "Shared")]
[switch]$Force,
[Switch]$Force,
[Parameter(Mandatory = $false, ParameterSetName = "Private")]
[Parameter(Mandatory = $false, ParameterSetName = "Shared")]
[switch]$PassThru,
[Switch]$PassThru,
[Parameter(Mandatory = $false, ParameterSetName = "Private")]
[Parameter(Mandatory = $false, ParameterSetName = "Shared")]
[Switch]$SkipPFXCertCreation
[Switch]$SkipPFXCertCreation,
[Parameter(Mandatory = $false, ParameterSetName = "Private")]
[Parameter(Mandatory = $false, ParameterSetName = "Shared")]
[Switch]$UseCertStore
)
begin {
@ -80,6 +84,28 @@ function New-CredentialStore {
# Set latest Credential Store version
# Set-Variable -Name "CSVersion" -Value "2.0.0" -Option Constant -Scope
# test if the path input is a valid file path
if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Path')) {
if ($Path.Attributes -contains 'Directory') {
$ErrorParams = @{
ErrorAction = 'Stop'
Exception = [System.IO.InvalidDataException]::new(
'Please provide a full path containing the credential store file name with the .json extension!'
)
}
Write-Error @ErrorParams
}
elseif ( ($null -eq $Path.Extension) -or ($Path.Extension -ne '.json')) {
$ErrorParams = @{
ErrorAction = 'Stop'
Exception = [System.IO.InvalidDataException]::new(
'Your provided path does not conain the required file extension .json !'
)
}
Write-Error @ErrorParams
}
}
}
process {
@ -112,8 +138,8 @@ function New-CredentialStore {
State = 'PSCredentialStore'
City = 'PSCredentialStore'
Organization = 'PSCredentialStore'
OrganizationalUnitName = ' '
CommonName = 'PrivateStore'
OrganizationalUnitName = $PSCmdlet.ParameterSetName
CommonName = 'PSCredentialStore'
}
$CRTAttribute = New-CRTAttribute @CRTParams
@ -133,6 +159,7 @@ function New-CredentialStore {
Confirm = $false
}
# test if there is already a cert
if ((Test-Path $PfxParams.CertName) -and (! $Force.IsPresent)) {
$ErrorParams = @{
Exception = [System.IO.InvalidDataException]::new(
@ -176,8 +203,15 @@ function New-CredentialStore {
Type = $null
}
if (! $SkipPFXCertCreation.IsPresent) {
$ObjProperties.PfXCertificate = $PfxParams.CertName
$ObjProperties.Thumbprint = $FreshCert.Thumbprint
if (!$UseCertStore.IsPresent) {
$ObjProperties.PfxCertificate = $PfxParams.CertName
}
else {
Write-Verbose 'Importing new PFX certificate file...'
Import-CSCertificate -Path $PfxParams.CertName -StoreName My -StoreLocation CurrentUser
}
}
if ($PSCmdlet.ParameterSetName -eq "Shared") {

View File

@ -1,140 +0,0 @@
function Update-CredentialStore {
<#
.SYNOPSIS
A brief description of the function or script.
.DESCRIPTION
Describe the function of the script using a single sentence or more.
.PARAMETER One
Description of the Parameter (what it does)
.INPUTS
Describe the script input parameters (if any), otherwise it may also list the word "[None]".
.OUTPUTS
Describe the script output parameters (if any), otherwise it may also list the word "[None]".
.EXAMPLE
.\Remove-Some-Script.ps1 -One content
.NOTES
File Name : Update-CredentialStore.ps1
Author : Marco Blessing - marco.blessing@googlemail.com
Requires :
.LINK
https://github.com/OCram85/PSCredentialStore
#>
[CmdletBinding()]
[OutputType()]
param(
[Parameter(Mandatory = $false)]
[ValidateNotNullOrEmpty()]
[Version]$From = '1.2.0',
[Parameter(Mandatory = $false)]
[ValidateNotNullOrEmpty()]
[Version]$To = '2.0.0',
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$PfxCertificate
)
begin {
}
process {
if (Test-Path -Path $Path) {
$CSOld = Get-CredentialStore -Shared -Path $Path -ErrorAction Stop
if ($CSOld.Version -ne $From) {
$ErrorParams = @{
Message = 'Can not migrate CredentialStore from version {0} to {1}' -f $From, $To
ErrorAction = 'Stop'
Exception = [System.Exception]::new()
}
Write-Error @ErrorParams
}
$CSNew = [PSCustomObject]@{
PSTypeName = 'PSCredentialStore.Store'
Version = $To
Created = $CurrentDate
PfxCertificate = $null
Thumbprint = $null
Type = $null
}
if ($PWD -eq (Get-DefaultCredentialStorePath)) {
$CSNew.Type = 'Private'
}
elseif ($PWD -eq (Get-DefaultCredentialStorePath -Shared)) {
$CSNew.Type = 'Shared'
}
else {
$ErrorParams = @{
Message = 'Can not determine a valid CredentialStore Type!'
ErrorAction = 'Stop'
Exception = [System.Exception]::new()
}
Write-Error @ErrorParams
}
$Cert = Get-PfxCertificate -FilePath $PfxCertificate -ErrorAction Stop
$CSNew.PfxCertificate = Join-Path -Path $PfxCertificate
$CSNew.Thumbprint = $Cert.Thumbprint
$CredentialItems = $CSOld | Get-Member -MemberType NoteProperty | Where-Object {
$_.Definition -like "*.PSCustomObject*"
} | Select-Object -ExpandProperty Name
# iterate through all existing items
foreach ($Item in $CredentialItems) {
$CurrentDate = Get-Date -UFormat "%Y-%m-%d %H:%M:%S"
$RSAKey = Get-RandomAESKey
$CredentialObj = [PSCustomObject]@{
User = $Item.UserName
Password = $null
Created = $CurrentDate
LastChange = $null
EncryptedKey = [Convert]::ToBase64String(
$Cert.PublicKey.Key.Encrypt(
$RSAKey,
[System.Security.Cryptography.RSAEncryptionPadding]::Pkcs1
)
)
}
if ($CSOld.Type -eq 'Private') {
$CredentialObject.Password = ConvertTo-SecureString -SecureString $Item.Password | ConvertFrom-SecureString -Key $RSAKey
}
elseif ($CSNew.Type -eq 'Shared') {
$ChallengeKey = [io.file]::ReadAllBytes((Join-Path -Path $PWD -ChildPath '/Challenge.bin'))
$CredentialObject.Password = ConvertTo-SecureString -SecureString $Item.Password -Key $ChallengeKey | ConvertFrom-SecureString -Key $RSAKey
}
Add-Member -InputObject $CSNew -Name (
($Item | Get-Variable).Name
) -MemberType NoteProperty -Value $CredentialObj
}
$CSNew | ConvertTo-Json -Depth 5 | Out-File -LiteralPath (
Join-Path -Path $PWD -ChildPath './CredentialStore.json'
) -Encoding utf8 -Confirm:$true
}
else {
$ErrorParams = @{
Message = 'Could not find the given CredentialStore path!'
ErrorAction = 'Stop'
Exception = [System.IO.FileNotFoundException]::new()
}
Write-Error @ErrorParams
}
}
end {
}
}

View File

@ -4,7 +4,7 @@ Describe "New-CredentialStoreItem" {
# Creat a fresh CredentialStore first
New-CredentialStore -Force
[String]$tmp = (65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_}
[String]$tmp = (65..90) + (97..122) | Get-Random -Count 5 | ForEach-Object { [char]$_ }
$tmp = $tmp.Replace(' ', '')
$tmpUser = "MyUser"
$tmpPwd = "fooobarysdfsfs" | ConvertTo-SecureString -AsPlainText -Force
@ -65,7 +65,7 @@ Describe "New-CredentialStoreItem" {
}
Context "General Exception handling" {
Mock Test-CredentialStore {return $false}
Mock Test-CredentialStore { return $false }
It "Missing CredentialStore should throw" {
{ New-CredentialStoreItem -Shared -Path 'C:\missingStore.json' -RemoteHost 'notrelevant' } | Should -Throw "Could not add anything"
}
@ -81,5 +81,29 @@ Describe "New-CredentialStoreItem" {
(Get-CredentialStoreItem -RemoteHost 'PipeHost').UserName | Should -Be 'pipeUser'
}
}
Context "Testing items with certficiate store" {
It "Create item in new store with cert store link" {
New-CredentialStore -UseCertStore -Force
$Path = Get-DefaultCredentialStorePath
$StoreHome = Split-Path -Path $Path -Parent
$CertFile = Join-Path -Path $StoreHome -ChildPath 'PSCredentialStore.pfx'
$Cert = Get-PfxCertificate -FilePath $CertFile
$myStore = [System.Security.Cryptography.X509Certificates.X509Store]::new('My')
$myStore.Open("ReadWrite")
$myStore.Add($Cert)
$MyStore.Close()
$UserName = 'testuser'
$Password = ConvertTo-SecureString -String "mypasswd" -AsPlainText -Force
[PSCredential]::new($UserName, $Password) | New-CredentialStoreItem -RemoteHost 'foobarcerts'
$writtenItem = Get-CredentialStoreItem -RemoteHost 'foobarcerts'
$writtenItem.UserName | Should -Be "testuser"
$writtenItem.GetNetworkCredential().Password | Should -Be 'mypasswd'
}
}
}

View File

@ -46,7 +46,7 @@ Describe "New-CredentialStore" {
Test-Path -Path $sCS | Should -Be $true
}
It "Test2: Try to override existing shared CS" {
{New-CredentialStore -Shared -Confirm:$false} | Should -Throw
{ New-CredentialStore -Shared -Confirm:$false } | Should -Throw
}
It "Test3: Reset shared CredentialStore" {
$now = Get-Date
@ -59,19 +59,40 @@ Describe "New-CredentialStore" {
Context "Custom Shared CS tests" {
$cCS = Join-Path -Path (Get-TempDir) -ChildPath "CredentialStore.json"
It "Test1: Create new custom shared" {
{New-CredentialStore -Path $cCS -Shared -Confirm:$false} | Should -Not -Throw
{ New-CredentialStore -Path $cCS -Shared -Confirm:$false } | Should -Not -Throw
}
It "Test2: Try to override exiting one" {
{New-CredentialStore -Path $cCS -Shared -Confirm:$false} | Should -Throw
{ New-CredentialStore -Path $cCS -Shared -Confirm:$false } | Should -Throw
}
It "Test3: Reset existing custom CredentialStore" {
{New-CredentialStore -Path $cCS -Shared -Force -Confirm:$false} | Should -Not -Throw
{ New-CredentialStore -Path $cCS -Shared -Force -Confirm:$false } | Should -Not -Throw
}
}
Context "Test exception handling" {
Mock Out-File {throw "foobar exception"}
Mock Out-File { throw "foobar exception" }
It "JSON Conversion should fail and throw" {
{ New-CredentialStore -Path (Join-Path -Path (Get-TempDir) -ChildPath '/dummy.json') -Shared -Confirm:$false} | Should -Throw
{ New-CredentialStore -Path (Join-Path -Path (Get-TempDir) -ChildPath '/dummy.json') -Shared -Confirm:$false } | Should -Throw
}
}
Context "Tests for Windows certificate store" {
It "Create new private store and skip certificate linking" {
{ New-CredentialStore -UseCertStore -Force } | Should -Not -Throw
$CS = Get-CredentialStore
$CS.PfxCertificate | Should -Be $null
$CS.Thumbprint | Should -Not -Be $null
$res = Test-CSCertificate -Thumbprint $CS.Thumbprint -StoreName My -StoreLocation CurrentUser
#Write-Verbose -Message ('res: {0}' -f $res) -Verbose
$res | Should -Be $true
}
It "Create new shared store and skipt certificate linking" {
{ New-CredentialStore -Shared -UseCertStore -Force } | Should -Not -Throw
$CS = Get-CredentialStore -Shared
$CS.PfxCertificate | Should -Be $null
$CS.Thumbprint | Should -Not -Be $null
$res = Test-CSCertificate -Thumbprint $CS.Thumbprint -StoreName My -StoreLocation CurrentUser
#Write-Verbose -Message ('res: {0}' -f $res) -Verbose
$res | Should -Be $true
}
}
}