PowerShell - Windows Trusted Certificate not Authenticating SSL over FTP - powershell

I complete steps 1-4 of this answer, which adds my certificate to the "Trusted Root Certification Authorities" > "Certificates," and the certificate is granted <All> intended purposes.
Executing the below PowerShell code fails with The remote certificate is invalid according to the validation procedure when $ftp_request.EnableSsl = $true. It succeeds when $ftp_request.EnableSsl = $false.
$file_folder = "C:\Users\username\Desktop"
$file_name = "test.txt"
$file_path = "$file_folder\$file_name"
$ftp_path = "ftp://127.0.0.1/$file_name"
$username = "user"
$pwd = "pass"
# Create a FTPWebRequest object to handle the connection to the ftp server
$ftp_request = [System.Net.FtpWebRequest]::Create($ftp_path)
# set the request's network credentials for an authenticated connection
$ftp_request.Credentials =
New-Object System.Net.NetworkCredential($username, $pwd)
$ftp_request.UseBinary = $true
$ftp_request.UsePassive = $true
$ftp_request.KeepAlive = $false
$ftp_request.EnableSsl = $true
$ftp_request.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$file_contents = Get-Content -en byte $file_path
$ftp_request.ContentLength = $file_contents.Length
$ftp_stream = $ftp_request.GetRequestStream()
$ftp_stream.Write($file_contents, 0, $file_contents.Length)
$ftp_stream.Close()
$ftp_stream.Dispose()
I know that it's possible to manually handle this by writing a handler to ServicePointManager.ServerCertificateValidationCallback, but I would like to have SSL certificates handled automatically by the Windows cert manager.

$ftp_path = "ftp://127.0.0.1/$file_name"
Adding a certificate as trusted for all purposes does not mean that a certificate is trusted for all hosts. The hostname you use to connect still has to match the subject of the certificate. And while you don't provide any information about the certificate itself my guess is that your certificate is not issued for the subject "127.0.0.1".

Related

Remove-MgApplicationKey - delete expired app registration certificates

I am updating my current scripts from the AzureAD module and want to update a script which deletes expired app registration certificates.
I can remove expired secrets using the new module, however the new command Remove-MgApplicationKey requires proof as per Microsoft document: https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.applications/remove-mgapplicationkey?view=graph-powershell-1.0. (As part of the request validation for this method, a proof of possession of an existing key is verified before the action can be performed).
`$params = #{
KeyId = "f0b0b335-1d71-4883-8f98-567911bfdca6"
Proof = "eyJ0eXAiOiJ..."
}
Remove-MgApplicationKey -ApplicationId $applicationId -BodyParameter $params`
Any suggestions on how to code this in PowerShell?
Thanks.
C# example from Microsoft doc: https://learn.microsoft.com/en-us/graph/application-rollkey-prooftoken
Code would look something like this
using assembly System;
using assembly System.Security.Cryptography.X509Certificates;
# Configure the following
$pfxFilePath = "<Path to your certificate file";
$password = "<Certificate password>";
$objectId = "<id of the application or servicePrincipal object>";
# Get signing certificate
#$signingCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([string]$pfxFilePath, [string]$password);
#$signingCert = [System.Security.Cryptography.X509Certificates]::X509Certificate2($pfxFilePath, $password);
$signingCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new();
$signingCert.CreateFromEncryptedPemFile($pfxFilePath, $password, $null)
#$signingCert | Format-Table
#$signingCert.filename = $pfxFilePath;
#$signingCert.password = $password;
# audience
$aud = "00000002-0000-0000-c000-000000000000";
#aud and iss are the only required claims.
$claims = [System.Collections.Generic.Dictionary[string, object]]::new()
$claims.Add("aud", $aud)
$claims.Add("iss", $objectId)
#token validity should not be more than 10 minutes
$now = [DateTime]::UtcNow;
$securityTokenDescriptor = New-Object [System.Security.Cryptography.X509Certificates.SecurityTokenDescriptor]::new()
$securityTokenDescriptor.Claims = $claims,
$securityTokenDescriptor.NotBefore = $now,
$securityTokenDescriptor.Expires = $now.AddMinutes(10),
$securityTokenDescriptor.SigningCredentials = New-Object X509SigningCredentials($signingCert);
$handler = [Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler]::new();
$x = handler.CreateToken($securityTokenDescriptor);
Write-Host x;

Unable to locate the private key container when imported using Import-PfxCertificate [duplicate]

I used following PowerShell function to import PFX to my Windows 2008 R2 server's certificate store
function Import-PfxCertificate ([String]$certPath,[String]$certificateStoreLocation = "CurrentUser",[String]$certificateStoreName = "My",$pfxPassword = $null)
{
$pfx = new-object System.Security.Cryptography.X509Certificates.X509Certificate2
$pfx.Import($certPath, $pfxPassword, "Exportable,PersistKeySet")
$store = new-object System.Security.Cryptography.X509Certificates.X509Store($certificateStoreName,$certificateStoreLocation)
$store.open("MaxAllowed")
$store.add($pfx)
$store.close()
return $pfx
}
The caller of the function looks like $importedPfxCert = Import-PfxCertificate $pfxFile "LocalMachine" "My" $password I installed it to local machine's My store. Then I granted read permission to my IIS Application pool.
I have a WCF service which needs to use it
<behaviors>
<serviceBehaviors>
<behavior>
<serviceCredentials>
<serviceCertificate findValue="MyCertName" x509FindType="FindBySubjectName" />
<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="MyValidator" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
When I use a client to call the service, I got exception from WCF It is likely that certificate 'CN=MyCertName' may not have a private key that is capable of key exchange or the process may not have access rights for the private key.
If I remove it from MMC, and manually import the same PFX file from Certificate MMC, to same store and grant same permission, my client can call the service without problem.
So it leads me to think, for some reason if I use PowerShell the private key is screwed somehow.
The funny thing is in either way, I go to MMC and double click on my installed certificate I can see You have a private key that corresponds to the certificate. so it looks like private key is loaded even in PowerShell. permission settings are identical.
Any clue or experience?
Have same issue. Next script work:
function InstallCert ($certPath, [System.Security.Cryptography.X509Certificates.StoreName] $storeName)
{
[Reflection.Assembly]::Load("System.Security, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a")
$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, "", $flags)
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($storeName, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite);
$store.Add($cert);
$store.Close();
}
I updated Sergey's answer to the following. Note that the using namespace ... syntax is only valid for PS 5.0 and later. If you need this for an earlier version, you will have to add the full namespace, System.Security.Cryptography.X509Certificates, as needed.
using namespace System.Security
[CmdletBinding()]
param (
[parameter(mandatory=$true)] [string] $CertificateFile,
[parameter(mandatory=$true)] [securestring] $PrivateKeyPassword,
[parameter(mandatory=$true)] [string] $AllowedUsername
)
# Setup certificate
$Flags = [Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet `
-bor [Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet `
-bor [Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
$Certificate = New-Object Cryptography.X509Certificates.X509Certificate2($CertificateFile, $PrivateKeyPassword, $Flags)
# Install certificate into machine store
$Store = New-Object Cryptography.X509Certificates.X509Store(
[Cryptography.X509Certificates.StoreName]::My,
[Cryptography.X509Certificates.StoreLocation]::LocalMachine)
$Store.Open([Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$Store.Add($Certificate)
$Store.Close()
# Allow read permission of private key by user
$PKFile = Get-ChildItem "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$($Certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName)"
$PKAcl = $PKFile.GetAccessControl("Access")
$ReadAccessRule = New-Object AccessControl.FileSystemAccessRule(
$AllowedUsername,
[AccessControl.FileSystemRights]::Read,
[AccessControl.AccessControlType]::Allow
)
$PKAcl.AddAccessRule($ReadAccessRule)
Set-Acl $PKFile.FullName $PKAcl
Save this script to InstallCertificate.ps1, then run it as Administrator:
PS C:\Users\me> .\InstallCertificate.ps1
cmdlet InstallCertificate.ps1 at command pipeline position 1
Supply values for the following parameters:
CertificateFile: c:\my\path\mycert.pfx
PrivateKeyPassword: *********************
AllowedUsername: me
PS C:\Users\me> ls Cert:\LocalMachine\My
<Observe that your cert is now listed here. Get the thumbprint>
PS C:\Users\me> (ls Cert:\LocalMachine\My | ? { $_.Thumbprint -eq $Thumbprint }).PrivateKey
After rebooting, the last line should show that the private key is still installed even as non-Administrator.
Edited to add the ACL step as described in https://stackoverflow.com/a/37402173/7864889.
I had a similar issue on one of our dev servers when importing a certificate through the MMC. My problem was that the Administrators group did not have any permissions on the MachineKeys folder.
C:\Users\All Users\Microsoft\Crypto\RSA\MachineKeys
I added full control on the MachineKeys folder to Administrators and it was able to successfully create the private key when importing the certificate.
Make sure the user you're running Powershell under has access to write to the MachineKeys folder.
The following code referenced below, by Sergey Azarkevich, is what did the trick for me:
$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
I landed on this SO thread as the built in Import-PfxCertificate was not importing CAPI certificates correctly. Unfortunately powerdude's answer cmdlet didnt work for me as it threw The property 'CspKeyContainerInfo' cannot be found on this object. Verify that the property exists. when setting the permissions.
After combining it with this wonderful gist from
milesgratz it worked.
Here is the final modified version made to look like a Import-PfxCertificate substitution.
# Import-CapiPfxCertificate.ps1
# for CNG certificates use built in Import-PfxCertificate
using namespace System.Security
[CmdletBinding()]
param (
[parameter(mandatory=$true)] [string] $FilePath,
[parameter(mandatory=$true)] [securestring] $Password,
[parameter(mandatory=$true)] [string] $CertStoreLocation,
[parameter(mandatory=$false)] [string] $AllowedUsername
)
if (-not ($CertStoreLocation -match '^Cert:\\([A-Z]+)\\([A-Z]+)$')) {
Write-Host "Incorrect CertStoreLocation. See usage in the Import-PfxCertificate documentation" -ForegroundColor Red
exit 1;
}
$StoreName = $Matches.2
$StoreLocation = $Matches.1
# Setup certificate
$Flags = [Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet `
-bor [Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet `
-bor [Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
$Certificate = New-Object Cryptography.X509Certificates.X509Certificate2($FilePath, $Password, $Flags)
# Install certificate into the specified store
$Store = New-Object Cryptography.X509Certificates.X509Store(
$StoreName,
$StoreLocation)
$Store.Open([Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$Store.Add($Certificate)
$Store.Close()
if (-not ([string]::IsNullOrEmpty($AllowedUsername))) {
# Allow read permission of private key by user
$PKUniqueName = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)).key.UniqueName
$PKFile = Get-Item "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$PKUniqueName"
$PKAcl = Get-Acl $PKFile
$PKAcl.AddAccessRule((New-Object AccessControl.FileSystemAccessRule($AllowedUsername, "Read", "Allow")))
Set-Acl $PKFile.FullName $PKAcl
}

Generate Self-signed certificate with Root CA Signer

Scenario: I am using PowerShell on Windows Server 2012r2 to generate a Root certificate and want to use that to sign a newly created Intermediate and Web certificate in dynamic generated (and destroyed) dev/test environments. The scripts are deployed remotely, and the intent is to keep it pure PowerShell if possible. In Windows 10/2016 this is relatively easy, after generating the Root certificate:
$Cert = New-SelfSignedCertificate -Signer $Root -Subject "CN=$Subject"
I've generated the Root certificate using COM X509Enrollment.CX509CertificateRequestCertificate and Security.Cryptography.X509Certificates.X509Certificate2 in a bastardized PS that I've had for some time, mainly because I needed to ensure that the Subject and Usage were set very specifically. I am not quite certain how to use this to sign the standard certificate without the above (which I have used before).
There are some examples using Bouncy Castle (see below) in C# that I could tie into PowerShell, but then I would need to deploy this additionally on the dynamic dev/test environments and I want to be able to do this in Powershell (via COM if needed) with the least dependencies.
C# Generate Self Signed Certificates on the Fly
How do I use Bouncy Castle in Powershell
The ultimate solution in my case, avoiding makecert and openssl was to use Powershell and BouncyCastle. I forked the PSBouncyCastle repo from PSBouncyCastle by RLipscombe and pushed 1.8.1 Bouncy Castle in. My forked version is the one I've used for the script, the fork resides at Forked: PSBouncyCastle.New.
I then used StackOverflow: C# Generate Certificates on the Fly as inspiration to write the following powershell below, I will be adding this to my GitHub and commenting, and I will amend this as soon as I do:
Import-Module -Name PSBouncyCastle.New
function New-SelfSignedCertificate {
[CmdletBinding()]
param (
[string]$SubjectName,
[string]$FriendlyName = "New Certificate",
[object]$Issuer,
[bool]$IsCA = $false,
[int]$KeyStrength = 2048,
[int]$ValidYears = 2,
[hashtable]$EKU = #{}
)
# Needed generators
$random = New-SecureRandom
$certificateGenerator = New-CertificateGenerator
if($Issuer -ne $null -and $Issuer.HasPrivateKey -eq $true)
{
$IssuerName = $Issuer.IssuerName.Name
$IssuerPrivateKey = $Issuer.PrivateKey
}
# Create and set a random certificate serial number
$serial = New-SerialNumber -Random $random
$certificateGenerator.SetSerialNumber($serial)
# The signature algorithm
$certificateGenerator.SetSignatureAlgorithm('SHA256WithRSA')
# Basic Constraints - certificate is allowed to be used as intermediate.
# Powershell requires either a $null or reassignment or it will return this from the function
$certificateGenerator = Add-BasicConstraints -isCertificateAuthority $IsCA -certificateGenerator $certificateGenerator
# Key Usage
if($EKU.Count -gt 0)
{
$certificateGenerator = $certificateGenerator | Add-ExtendedKeyUsage #EKU
}
# Create and set the Issuer and Subject name
$subjectDN = New-X509Name -Name ($SubjectName)
if($Issuer -ne $null) {
$IssuerDN = New-X509Name -Name ($IssuerName)
}
else
{
$IssuerDN = New-X509Name -Name ($SubjectName)
}
$certificateGenerator.SetSubjectDN($subjectDN)
$certificateGenerator.SetIssuerDN($IssuerDN)
# Authority Key and Subject Identifier
if($Issuer -ne $null)
{
$IssuerKeyPair = ConvertTo-BouncyCastleKeyPair -PrivateKey $IssuerPrivateKey
$IssuerSerial = [Org.BouncyCastle.Math.BigInteger]$Issuer.GetSerialNumber()
$authorityKeyIdentifier = New-AuthorityKeyIdentifier -name $Issuer.IssuerName.Name -publicKey $IssuerKeyPair.Public -serialNumber $IssuerSerial
$certificateGenerator = Add-AuthorityKeyIdentifier -certificateGenerator $certificateGenerator -authorityKeyIdentifier $authorityKeyIdentifier
}
# Validity range of the certificate
[DateTime]$notBefore = (Get-Date).AddDays(-1)
if($ValidYears -gt 0) {
[DateTime]$notAfter = $notBefore.AddYears($ValidYears)
}
$certificateGenerator.SetNotBefore($notBefore)
$certificateGenerator.SetNotAfter($notAfter)
# Subject public key ~and private
$subjectKeyPair = New-KeyPair -Strength $keyStrength -Random $random
if($IssuerPrivateKey -ne $null)
{
$IssuerKeyPair = [Org.BouncyCastle.Security.DotNetUtilities]::GetKeyPair($IssuerPrivateKey)
}
else
{
$IssuerKeyPair = $subjectKeyPair
}
$certificateGenerator.SetPublicKey($subjectKeyPair.Public)
# Create the Certificate
$IssuerKeyPair = $subjectKeyPair
$certificate = $certificateGenerator.Generate($IssuerKeyPair.Private, $random)
# At this point you have the certificate and need to convert it and export, I return the private key for signing the next cert
$pfxCertificate = ConvertFrom-BouncyCastleCertificate -certificate $certificate -subjectKeyPair $subjectKeyPair -friendlyName $FriendlyName
return $pfxCertificate
}
A few examples of usage for this powershell would be:
Generate a Root CA
$TestRootCA = New-SelfSignedCertificate -subjectName "CN=TestRootCA" -IsCA $true
Export-Certificate -Certificate $test -OutputFile "TestRootCA.pfx" -X509ContentType Pfx
Generate a Standard Self Signed
$TestSS = New-SelfSignedCertificate -subjectName "CN=TestLocal"
Export-Certificate -Certificate $TestSS -OutputFile "TestLocal.pfx" -X509ContentType Pfx
Generate a certificate, signing with a root certificate
$TestRootCA = New-SelfSignedCertificate -subjectName "CN=TestRootCA" -IsCA $true
$TestSigned = New-SelfSignedCertificate -subjectName "CN=TestSignedByRoot" -issuer $TestRootCA
Export-Certificate -Certificate $test -OutputFile "TestRootCA.pfx" -X509ContentType Pfx
Export-Certificate -Certificate $test -OutputFile "TestRootCA.pfx" -X509ContentType Pfx
Generate a Self-Signed with Specific Usage
$TestServerCert = New-SelfSignedCertificate -subjectName "CN=TestServerCert" -EKU #{ "ServerAuthentication" = $true }
Note that the -EKU parameter accepts via splatting, it does this to ensure that anything added to Add-ExtendedKeyUsage is validly passed. It accepts the following certificate usages:
DigitalSignature
NonRepudiation
KeyEncipherment
DataEncipherment
KeyAgreement
KeyCertSign
CrlSign
EncipherOnly
DecipherOnly
This fits my need and seems to work across all Windows Platforms we are using for dynamic environments.
How about simply doing this:
$cert = New-SelfSignedCertificate -FriendlyName "MyCA"
-KeyExportPolicy ExportableEncrypted
-Provider "Microsoft Strong Cryptographic Provider"
-Subject "SN=TestRootCA" -NotAfter (Get-Date).AddYears($ExpiryInYears)
-CertStoreLocation Cert:\LocalMachine\My -KeyUsageProperty All
-KeyUsage CertSign, CRLSign, DigitalSignature
Important parameters are -KeyUsageProperty and -KeyUsage.
"Itiverba Self-Signed certificate generator" (http://www.itiverba.com/en/software/itisscg.php) is a free GUI tool for Windows that allows you to create your own CA certificates and sign end-certificates with it. You can export the certificates in PEM, CER, DER, PFX file formats.
It's just 3 lines to encode :
Subject: CN="Testcorp - Private CA"
Basic Constraints: V (checked)
Basic Constraints / Subject Type: CA
Give a file name and select a file format, then click on the "create certificate" button. Your Custom CA certificate is done.
The easy way of creating a root certificate would be to do the following. Please note the text extension which makes sure that the certificate is a root certificate. Such a certificate must be placed in a root certificate store to indicate trust. E.g. The 'cert:\LocalMachine\My' store.
Make sure that the KeyUsage is what you want. This can of course be changed, but Microsoft is not that good at documenting why you should do what they suggest.
The moving/copying of the certificate must be done done by exporting the certificate and importing it again. Or create the certificate in the correct place. Note that in general, the certificate will only be created in a My store. Some support commands are described in Certificate Provider PowerShell functions.
The certificate will be exportable by default.
$rootCert = New-SelfSignedCertificate -CertStoreLocation Cert:\CurrentUser\My `
-DnsName "RootCA" `
-TextExtension #("2.5.29.19={text}CA=true") `
-KeyUsage CertSign,CrlSign,DigitalSignature;
The code was lifted from https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-create-temporary-certificates-for-use-during-development

Trusting certificate of HTTPS site in web request

I want to test if our web server (located on the intranet) is online (1) or offline (0) from the server containing it using PowerShell 2.0. For this I have a script that navigates to the page and check if a html-string is available on the page.
function Get-HeartBeat {
$isAlive = $false
try {
$webclient = New-Object WebClient
$userName = "xxx"
$password = "xxx"
$domain = "xxx"
$url = "ourUrl.com"
$html = '<input type="submit">'
$webclient.Credentials = New-Object System.Net.NetworkCredential($userName, $password, $domain)
$webpage = $webclient.DownloadString($url)
$isAlive = $webpage.Contains($html)
} catch {
# A WebException is thrown if the status code is anything but 200 (OK)
Write-Host $_
$isAlive = $false
}
return "$([Int32]$isAlive)"
}
Unfortunately this returns an error:
Exception calling "DownloadString" with "1" argument(s): "The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel."
A way to trust our certificate is to create a type with a certificate policy as follows (modification of this answer):
Add-Type #"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustOurCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint servicePoint, X509Certificate certificate,
WebRequest request, int certificateProblem)
{
return certificate.Issuer.Equals("OUR ISSUER")
&& certificate.Subject.Contains("our application");
}
}
"#
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustOurCertsPolicy
This still feels a bit "stringly typed" instead of 100% secure.
Is this safe? Is there a better way to create a WebClient that has one certificate accepted? The certificate we should trust is available in cert:LocalMachine\My.
The correct way to handle certificate errors is to import the certificate chain (root and intermediat CA certificates) of the webserver's certificate into the local certificate store (as Trusted Root CA and Intermediate CA respectively). If the server certificate is self-signed you need to import the server certificate as a Trusted Root CA.
Another option, depending on what you actually need to check, might be to modify your algorithm. For instance, if you really need just a heartbeat (not verify that an actual request returns a specific result) you could simply try to establish a TCP connection to the port:
function Get-HeartBeat {
$url = 'ourUrl.com'
$port = 443
$clnt = New-Object Net.Sockets.TcpClient
try {
$clnt.Connect($url, $port)
1
} catch {
0
} finally {
$clnt.Dispose()
}
}
On more recent Windows versions you could use the Test-NetConnection cmdlet to the same end:
function Get-HeartBeat {
$url = 'ourUrl.com'
$port = 443
[int](Test-Net-Connection -Computer $url -Port $port -InformationLevel Quiet)
}

Saving credentials for reuse by powershell and error ConvertTo-SecureString : Key not valid for use in specified state

I was doing something like described in this post to save credentials in a secured file so our automated process can use that to run remote PS scripts via Invoke-command:
http://blogs.technet.com/b/robcost/archive/2008/05/01/powershell-tip-storing-and-using-password-credentials.aspx
This works great when I run this under my account - password is read from encrypted file, passed to Invoke-command and everything is fine.
Today, when my script was ready for its prime time, I tried to run it under windows account that will be used by automated process and got this error below while my script was trying to read secured password from a file:
ConvertTo-SecureString : Key not valid for use in specified state.
At \\remoted\script.ps1:210 char:87
+ $password = get-content $PathToFolderWithCredentials\pass.txt | convertto-sec
urestring <<<<
+ CategoryInfo : InvalidArgument: (:) [ConvertTo-SecureString], C
ryptographicException
+ FullyQualifiedErrorId : ImportSecureString_InvalidArgument_Cryptographic
Error,Microsoft.PowerShell.Commands.ConvertToSecureStringCommand
Asked my workmate to run under his account and he got the same error.
This is the code I am using to save credentials:
$PathToFolderWithCredentials = "\\path\removed"
write-host "Enter login as domain\login:"
read-host | out-file $PathToFolderWithCredentials\login.txt
write-host "Enter password:"
read-host -assecurestring | convertfrom-securestring | out-file $PathToFolderWithCredentials\pass.txt
write-host "*** Credentials have been saved to $pathtofolder ***"
This is the code in the script to run by automated process to read them to use in Invoke-command:
$login= get-content $PathToFolderWithCredentials\login.txt
$password = get-content $PathToFolderWithCredentials\pass.txt | convertto-securestring
$credentials = new-object -typename System.Management.Automation.PSCredential -argumentlist $login,$password
Error happens on line $password = get-content $PathToFolderWithCredentials\pass.txt | convertto-securestring
Any ideas?
You have to create the password string on the same computer and with the same login that you will use to run it.
ConvertFrom-SecureString takes a Key ( and SecureKey) parameter. You can specify the key to save the encrypted standard string and then use the key again in ConvertTo-SecureString to get back the secure string, irrespective of the user account.
http://technet.microsoft.com/en-us/library/dd315356.aspx
In a project, I have implemented asymmetric encryption, whereby people encrypt the password using the public key and the automation process has the private key to decrypt passwords: Handling passwords in production config for automated deployment
The below will allow credentials to be saved as a file, then those credentials to be used by another script being run by a different user, remotely.
The code was taken from a great article produced by David Lee, with only some minor adjustments from myself https://blog.kloud.com.au/2016/04/21/using-saved-credentials-securely-in-powershell-scripts/
First step is to save a a secure password to a file using AES. The below will run as a stand alone script:
# Prompt you to enter the username and password
$credObject = Get-Credential
# The credObject now holds the password in a ‘securestring’ format
$passwordSecureString = $credObject.password
# Define a location to store the AESKey
$AESKeyFilePath = “aeskey.txt”
# Define a location to store the file that hosts the encrypted password
$credentialFilePath = “credpassword.txt”
# Generate a random AES Encryption Key.
$AESKey = New-Object Byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($AESKey)
# Store the AESKey into a file. This file should be protected! (e.g. ACL on the file to allow only select people to read)
Set-Content $AESKeyFilePath $AESKey # Any existing AES Key file will be overwritten
$password = $passwordSecureString | ConvertFrom-SecureString -Key $AESKey
Add-Content $credentialFilePath $password
Then in your script where you need to use credentials use the following:
#set up path and user variables
$AESKeyFilePath = “aeskey.txt” # location of the AESKey
$SecurePwdFilePath = “credpassword.txt” # location of the file that hosts the encrypted password
$userUPN = "domain\userName" # User account login
#use key and password to create local secure password
$AESKey = Get-Content -Path $AESKeyFilePath
$pwdTxt = Get-Content -Path $SecurePwdFilePath
$securePass = $pwdTxt | ConvertTo-SecureString -Key $AESKey
#crete a new psCredential object with required username and password
$adminCreds = New-Object System.Management.Automation.PSCredential($userUPN, $securePass)
#use the $adminCreds for some task
some-Task-that-needs-credentials -Credential $adminCreds
Please be aware that if the user can get access to the password file and the key file, they can decrypt the password for the user.
Another approach would be to protect the data using scope 'LocalMachine' instead of 'CurrentUser' which is the one used by ConvertFrom-SecureString.
public static string Protect(SecureString input, DataProtectionScope dataProtectionScope = DataProtectionScope.CurrentUser, byte[] optionalEntropy = null)
{
byte[] data = SecureStringToByteArray(input);
byte[] data2 = ProtectedData.Protect(data, optionalEntropy, dataProtectionScope);
for (int i = 0; i < data.Length; i++)
{
data[i] = 0;
}
return ByteArrayToString(data2);
}
private static byte[] SecureStringToByteArray(SecureString s)
{
var array = new byte[s.Length * 2];
if (s.Length > 0)
{
IntPtr intPtr = Marshal.SecureStringToGlobalAllocUnicode(s);
try
{
Marshal.Copy(intPtr, array, 0, array.Length);
}
finally
{
Marshal.FreeHGlobal(intPtr);
}
}
return array;
}
private static string ByteArrayToString(byte[] data)
{
var stringBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
stringBuilder.Append(data[i].ToString("x2", CultureInfo.InvariantCulture));
}
return stringBuilder.ToString();
}
The encrypted string can be used by ConvertTo-SecureString which is using scope 'CurrentUser'.
Assuming you have a known list of N users who will use the credentials (e.g. one developer userMe and a system/service user userSys) you can just (get those users to) make N copies of the pass.txt file: one for each user.
So the password of userX will result in e.g. 2 *.pass.txt files:
userX.userMe.pass.txt
userX.userSys.pass.txt
When userMe wants the creds he/she reads userX.userMe.pass.txt etc.