Powershell Script to Install Certificate Into Active Directory Store - powershell

I'm trying to write a powershell script to install a certificate into the active directory certificate store,
Here are the steps to do this manually, any help would be greatly appreciated.
On a Windows 2008R2 domain controller,
Click Start -> Run
type MMC
click ok
Click File -> Add/Remove Snap-In
Select "Certificates" -> Add
Select "Service Account"
Click Next
Select "Local Computer"
Click Next
Select "Active Directory Domain Services"
Click Finish
Click Ok
I want the script to install the certificate into :
NTDS\Personal
I would post an image but I don't have enough "reputation" apparently, so I can only provide text instructions.
So basically what I've tried is, I've used this powershell function below to import a certificate into the Local Machine -> Personal Store, which is where most certificates go, and the code works.
But I need to install the certificate into the "NTDS\Personal" store on a domain controller, but the $certRootStore only accepts localmachine or CurrentUser, so I'm stuck : /
function Import-PfxCertificate
{
param
(
[String]$certPath,
[String]$certRootStore = "localmachine",
[String]$certStore = "My",
$pfxPass = $null
)
$pfx = new-object System.Security.Cryptography.X509Certificates.X509Certificate2
if ($pfxPass -eq $null)
{
$pfxPass = read-host "Password" -assecurestring
}
$pfx.import($certPath,$pfxPass,"Exportable,PersistKeySet")
$store = new-object System.Security.Cryptography.X509Certificates.X509Store($certStore,$certRootStore)
$store.open("MaxAllowed")
$store.add($pfx)
$store.close()
}
Import-PfxCertificate -certPath "d:\Certificate.pfx"
Regards Alex

Using a combination of what you already had above and the registry keys for the two certificate stores this works.
The only other thing is that I don't know how NTDS determines which certificate to use when there are multiple in the certificate store.
function Import-NTDSCertificate {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$PFXFile,
[Parameter(Mandatory)]
[string]$PFXPassword,
#Remove certificate from LocalMachine\Personal certificate store
[switch]$Cleanup
)
begin{
Write-Verbose -Message "Importing PFX file."
$PFXObject = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
$PFXObject.Import($PFXFile,$PFXPassword,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
$thumbprint = $PFXObject.Thumbprint
}
process{
Write-Verbose -Message "Importing certificate into LocalMachine\Personal"
$certificateStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store('My','LocalMachine')
$certificateStore.Open('MaxAllowed')
$certificateStore.Add($PFXObject)
$certificateStore.Close()
Write-Verbose -Message "Copying certificate from LocalMachine\Personal to NTDS\Personal"
$copyParameters = #{
'Path' = "HKLM:\Software\Microsoft\SystemCertificates\MY\Certificates\$thumbprint"
'Destination' = "HKLM:\SOFTWARE\Microsoft\Cryptography\Services\NTDS\SystemCertificates\My\Certificates\$thumbprint"
'Recurse' = $true
}
Copy-Item #copyParameters
}
end{
if ($Cleanup){
Write-Verbose -Message "Removing certificate from LocalMachine\Personal"
$removalParameters = #{
'Path' = "HKLM:\SOFTWARE\Microsoft\SystemCertificates\MY\Certificates\$thumbprint"
'Recurse' = $true
}
Remove-Item #removalParameters
}
}
}

Alright, first the bad news. The only managed certificate stores are LocalMachine and CurrentUser, as we have all seen in powershell.
Now, the not so bad news. We know that the 'physical' location store (physical is MS' word, not mine) exists in the registry on the ADDS server, HKLM\Software\Microsoft\Cryptography\Services\NTDS\SystemCertificates. This was dually verified by both
Using procmon while importing a certificate into the store using the mmc snap-in
Scavenging msdn for this nugget
The link in #2 shows that all physical stores for services are stored in the path mentioned above, substituting NTDS for . The real service name, not the display name.
However,
Because of the bad news. Trying to map it in powershell with that reg key as the root and -PSProvider Certificate will prove disappointing, it was the first thing I tried.
What one can try, is using the X509Store constructor that takes an IntPtr to a SystemStore, as described here. Yes, that invovles some unmanaged code, and mixing the two is something I do rarely, but this and googling for HCERTSTORE C# should get you there.

Even though this post is years old, it is still helpful and turns up in searches, so to address the question of "I don't know how NTDS determines which certificate to use when there are multiple in the certificate store", the answer is that you will get unreliable results when there are two or more valid certificates installed that meet the requested criteria so it is recommended to remove the old/unneeded certificate(s) and just leave the newest/best one for the server auth.

Related

Imported pfx certificate is not saved on disk

I wrote a script in PowerShell to import a certificate on Windows Server 2016/2019. The script is added to an Azure DevOps pipeline and the agent is an Environment Agent running as NT AUTHORITY\SYSTEM. It first imports the certificate into the LocalMachine\My store and sets read permissions on the certificate right after that. It comes with quite weird behavior while running as a pipeline or executed manually. I'll write out the different kinds of behavior:
Import certificate on a server (successful)
Check the existence of the file on the physical disk (successful -> found in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys)
Set read permissions for account x (successful)
Import certificate on a server (successful)
Check the existence of the file on the physical disk (failed. File is not stored programdata\machinekeys, but in C:\ProgramData\Microsoft\Crypto\Keys)
Set read permissions for account x (failed --> can't find the file on the proper location)
Import certificate on a server (successful)
Check the existence of the file on the physical disk (successful)
Set read permissions for account x (failed --> can't find the file on the proper location. does not exist on the server )
There is no way to predict which one of the scenario's will occur while running the script.
I monitored server and file behavior with ProcMon (SysInternal Suite) during the import and did see the file being created and saved in while in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys
during both successful and failed attempts.
Import PFX code:
$FilePath = 'e:\folder\certificate.pfx'
$password = 'iLoveChocolateyCookies'
$cert = New-Object system.Security.Cryptography.X509Certificates.X509Certificate2($FilePath, $password, "PersistKeySet,MachineKeySet")
$store = New-object System.Security.Cryptography.X509Certificates.X509Store -argumentlist "My", "LocalMachine"
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::"ReadWrite")
$store.Add($cert)
$store.Close()
Physical disk location check code:
$checkCert = Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.thumbprint -eq '<insertThumbhere>' }
$rsaCertCheck = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($checkCert)
$checkCertDirectory = (Get-ChildItem -Path 'C:\Programdata\Microsoft\Crypto' -Include $rsaCertCheck.Key.UniqueName -File -Recurse).DirectoryName
if ($checkCertDirectory -eq "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys") {
Write-Host "Certificate found on physical disk."
} else {
throw 'nope'
}
Import-PfxCertificate showed the same behaviour. That is why I decided to use the .Net approach after consulting StackOverflow and Google, but ended up with the same issues.
A helpful hand will be very much appreciated :)

Bypass Internet Explorer certificate warning

My Question is pretty straight forward.
Can I somehow bypass this warning in PowerShell?
Right now I'm loading the website with the InternetExplorer.Application ComObject, but I could switch to Invoke-Webrequest etc. if that's needed
EDIT: Additional information
This is just for a function I wrote to open ILO of our HP Servers via PowerShell. that's the function:
function Open-ILO {
param(
[Parameter(
Position = 0,
Mandatory = $true
)]
[string]$computer,
[switch]$show
)
$hash = #{
"Server1" = "http://10.0.0.49/"
"Server2" = "http://10.0.0.50/"
"Server3" = "http://10.0.0.56/"
}
$Wert = $hash.get_item($computer)
if (!$show.IsPresent)
{
$ie = new-object -com InternetExplorer.Application
$ie.Visible = $true
$ie.Navigate($Wert)
}
else { Write-Host $Wert }
}
The issue here isn't PowerShell it's Internet Explorer.
The ILO comes with a self-signed certificate which IE does not trust and so shows you the error. Self-signed certificates are not trusted as they are self-generated and require no verification from a certificate authority.
You can either generate a new cert for the ILO from an internal certificate authority to replace the self signed cert. If you are using Active Directory you will have a CA.
Or you can install the self-signed certificate so that IE trusts it.
Depending on your workplace security policy there might be security concerns with the second option, as your computer will trust content that is signed with that certificate. Most businesses will be ok with this but some with high security might not.

X509Store.Open() throwing an Exception

why does $store.Open($openFlags) throw an exception, and is there a better way than my "work around" to make it work?
<#
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Cert:\CurrentUser\My")
$openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::MaxAllowed
$store.Open($openFlags) #Exception calling "Open" with "1" argument(s): "The parameter is incorrect.
#>
#Work Around:
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Cert:\CurrentUser\My")
$openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::MaxAllowed
$startIndexOfStoreName = $store.Name.LastIndexOf("\") + 1
$lengthOfStoreName = $store.Name.Length - $startIndexOfStoreName
$storeNameString = $store.Name.Substring($startIndexOfStoreName, $lengthOfStoreName)
$storeName = [System.Security.Cryptography.X509Certificates.StoreName]$storeNameString
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($storeName, $store.Location)
$store.Open($openFlags) #No Exception thrown!
Update: Seems as though when using the X509Store(String) constructor, you are NOT allowed to have any slashes (correct me if I'm wrong). So $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My") works.
Define you certificate store using
$store = Get-Item "Cert:\CurrentUser\My"
instead of
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Cert:\CurrentUser\My")
To be honest I'm still trying to figure out why it works, or how.
The first method returns a $store called "My", so I'm assuming that it targets the store specifically and you can open it with
$store.Open($openFlags)
The second method returns a $store called "Cert:\CurrentUser\My". Open method on this will fail.
I wanted to comment on this, since, as is already pointed out, "the mixing of .NET Framework and the use of PowerShell Providers" in the previous examples. For me, I needed this to work as a pure .NET way of getting the certs to test out some C# equivalent code without the full development environment on a users computer.
Here's what I came up with, which worked:
$Location = [Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser
$StoreName = [Security.Cryptography.X509Certificates.StoreName]::My
$Store = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $StoreName, $Location
$OpenFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly
$Store.Open($OpenFlags)
$Store.Certificates
Actually, you are mixing methods. One is via a provider (Cert:) the other is a .Net type (X509Store). Very different processes for attaching to the cert stores and pulling cert details.
Think of "Cert:" like a PSDrive (which it basically is). So you can get-childitem, etc. and don't need to "open" the store. In this mindset, the cert store locations are folders, and certs are individual objects:
# List the store locations
gci Cert:\
# List store names in CurrentUser store location
gci Cert:\CurrentUser
# List certs in the My store of CurrentUser store location
gci Cert:\CurrentUser\My | format-list
The catch to using the Cert: provider is that if you want to work with certs on remote systems, remoting (WinRM) needs to be enabled so you can "Invoke-Command". Not every environment allows this. That is where the .Net X509Store comes in. Not sure how well it works with "CurrentUser", but I've never been concerned about that - I am more interested in what is in the "LocalMachine" stores (specifically "My" since that is where the system holds web and auth certs). Modified snippet to list these certs (pulled from a script I built for interrogating all the servers in SharePoint farms).
# Change as necessary
$strTarget = $env:computername
$strCertStoreLocation = 'LocalMachine'
$strCertStoreName = 'My'
# Set up store parameters, connect and open store
[System.Security.Cryptography.X509Certificates.StoreLocation]$strStoreLoc = [String]$strCertStoreLocation
[System.Security.Cryptography.X509Certificates.StoreName]$strStoreName = [String]$strCertStoreName
$objCertStore = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList "\\$($strTarget)\$($strStoreName)", $strStoreLoc
$objCertStore.Open('ReadOnly')
# List cert details in bulk
$objCertStore.Certificates | Format-List
# List specific props
foreach ($Cert in $objCertStore.Certificates) {
"Subject: $($Cert.Subject)"
"Issuer: $($Cert.Issuer)"
"Issued: $($Cert.NotBefore)"
"Expires: $($Cert.NotAfter)"
""
}
For a bit more details about each, hit up your favorite tech repository (MSDN, PowerShell.org, Hey Scripting Guy, etc.) :)

Using PowerShell to Create Self-Signed Certificate

I'm using code similar to that found here to create a self-signed certificate for use in IIS:
http://blogs.technet.com/b/vishalagarwal/archive/2009/08/22/generating-a-certificate-self-signed-using-powershell-and-certenroll-interfaces.aspx
Works fine except I want to give it a friendly name to make locating it easier when I want to assign the certificate to a dynamically created site.
Anyone know how to change the above to set the friendly name (I've tried what seemed obvious to no avail).
Got a better way to create a cert via PowerShell that does not prompt the user for information?
Followup on the script I am using - based on the url above but turned into a cmdlet:
function Add-SelfSignedCertificate
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)]
[Alias('cn')]
[string]$CommonName
)
$name = new-object -com "X509Enrollment.CX500DistinguishedName.1"
$name.Encode("CN=$CommonName", 0)
$key = new-object -com "X509Enrollment.CX509PrivateKey.1"
$key.ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
$key.KeySpec = 1
$key.Length = 1024
$key.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)"
$key.MachineContext = 1
$key.Create()
$serverauthoid = new-object -com "X509Enrollment.CObjectId.1"
$serverauthoid.InitializeFromValue("1.3.6.1.5.5.7.3.1")
$ekuoids = new-object -com "X509Enrollment.CObjectIds.1"
$ekuoids.add($serverauthoid)
$ekuext = new-object -com "X509Enrollment.CX509ExtensionEnhancedKeyUsage.1"
$ekuext.InitializeEncode($ekuoids)
$cert = new-object -com "X509Enrollment.CX509CertificateRequestCertificate.1"
$cert.InitializeFromPrivateKey(2, $key, "")
$cert.Subject = $name
$cert.Issuer = $cert.Subject
$cert.NotBefore = get-date
$cert.NotAfter = $cert.NotBefore.AddDays(90)
$cert.X509Extensions.Add($ekuext)
$cert.Encode()
$enrollment = new-object -com "X509Enrollment.CX509Enrollment.1"
$enrollment.InitializeFromRequest($cert)
$certdata = $enrollment.CreateRequest(0)
$enrollment.InstallResponse(2, $certdata, 0, "")
}
It might not help for your specific use, but there is a new Powershell CmdLet installed in Windows 8.1 and Server 2012 that is pretty quick and easy to use:
New-SelfSignedCertificate [-CertStoreLocation <String> ] [-CloneCert <Certificate> ] [-DnsName <String> ] [-Confirm] [-WhatIf] [ <CommonParameters>]
More details can be found here: https://learn.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps
In my usage, the friendly name of the cert has always been set as the first DnsName specified in the CmdLet.
Example that places the certificate in your Local Computer's Personal store:
New-SelfSignedCertificate -CertStoreLocation cert:\LocalMachine\My -DnsName www.example.com
Note: Powershell has to be started with admin rights for this to work.
You can set the CertificateFriendlyName directly in you code, you just need to know where to do it:
$enrollment.InitializeFromRequest($cert)
$enrollment.CertificateFriendlyName = 'whatever'
$certdata = $enrollment.CreateRequest(0)
$key has a FriendlyName but I don't see that showing up anywhere so I don't think it helps you.
Scott Hanselman wrote up a nice blog post on how to create a self-signed cert using the SDK tool makecert.exe. That tool looks to be a good bit easer to use than the code in the post you reference. With makecert.exe you can use the -n option to specify a subject name. I've used that subject name to refer to the certificate in other tools like signtool.exe. Although, I've found that subject names don't have to be unique so I tend to use the Thumbprint value which appears to be unique. Signtool will also accept a thumbprint (via the /sha1 parameter) to identify the cert.

Automatically sign powershell script using Get-PfxCertificate

I have to sign remote scripts with a certificate from the remote machine from which I have a .pfx file.
I would like to automate the scripting by supplying the password to the Get-PfxCertificate programmatically.
So the question is:
Is it possible to somehow supply programmatically the required password to
Get-PfxCertificate?
$CertPath = "my.pfx"
$CertPass = "mypw"
$Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertPath, $CertPass)
Set-AuthenticodeSignature -Certificate $Cert -TimeStampServer http://timestamp.verisign.com/scripts/timstamp.dll -FilePath $OutputFilename
Make sure you have the proper permissions otherwise you won't be able to create an instance of the X509Certificate2 object.
I did a bit of checking around on this and couldn't find a clean way to provide the password programmatically. I suspect it is meant to be this way for security reasons. Either that or the PowerShell development team just blew it by not including a Credential parameter for this cmdlet. The only other option I can think of is to use someting like SendKeys to send the individual password character key presses to the PowerShell console at the right time via a background job (blech - just threw up in my mouth a little). :-)
Another way of doing this is by loading your certificate directly from your certificate store using PS Providers. Use Get-PSProviders to determine available PSProviders on your machine.
Once you have cert provider loaded, you can now get the certificate using Get-ChildItem
Launch certmgr.msc from run to launch the certificate store
Assuming that your certificate is stored under Personal folder in your cert store and has "Company Name" set in the subject property of the certificate, and there is only certificate in that folder with Company Name in the subject - you can get the certificate like so
$my_cert = Get-ChildItem cert:\CurrentUser\My | ? {$_.Subject -match "Company Name"}
$my_cert will be your certificate object that you can pass directly to Set-AuthenticodeSignature cmdlet
Set-AuthenticodeSignature -Certificate $my_cert -FilePath fqn_to_dll.dll -Timestampserver "http://timestampurl"
post signing, you can retrieve the sign status by querying on the Status property for "Valid" or not like
$result = Set-AuthenticodeSignature -Certificate $my_cert -FilePath fqn_to_dll.dll -Timestampserver "http://timestampurl" | Select Status
if(-Not ($result -eq "Valid")){
Write-Output "Error Signing file: Status: $($result.Status)"
}