RafaMarrara
RafaMarrara

Reputation: 851

How to download the SSL certificate from a website using PowerShell?

I want to download the SSL certificate from, say https://www.outlook.com, using PowerShell. Is it possible? Could someone help me?

Upvotes: 21

Views: 66574

Answers (7)

mlhDev
mlhDev

Reputation: 2385

After reading these I realized we customized MSFT's solution for Azure and it eliminates the need for app domain clutter in PowerShell 7 or compiling C# code.

param(
  [Parameter(mandatory = $true)]
  [string]$fqdn,
  [int]$port = 443
)

$tcpSocket = New-Object Net.Sockets.TcpClient($fqdn, $port)
$tcpStream = $tcpSocket.GetStream()
$sslStream = New-Object -TypeName Net.Security.SslStream($tcpStream, $false, {param($s, $ce, $ch, $e) $true }) # bypass cert validation

try {
  $sslStream.AuthenticateAsClient($fqdn)  # If not valid, will display "remote certificate is invalid".
}
catch {
  Write-Error "Unable to establish SSL connection to $($fqdn):$port`n$($_.Exception.Message)"
  $tcpSocket.Close()
  return
}

$certinfo = New-Object -TypeName Security.Cryptography.X509Certificates.X509Certificate2($sslStream.RemoteCertificate)
$tcpSocket.Close()
return $certinfo

The constructor for SslStream where it passes in that lambda expression is how invalid certificates are permitted and don't throw an error:

{param($s, $ce, $ch, $e) $true }

Usage would look like:

$cert = .\Get-WebCertificate.ps1 -fqdn expired.intranet.local
$cert.NotAfter

Sunday, December 31, 2000 11:59:00 PM

Upvotes: 0

Lockszmith
Lockszmith

Reputation: 2551

I got here from a ServerFault question, but found the accepted answer a bit outdated.

Below is an updated (and simplified) version of Get-WebsiteCertificate function (from another answer) based on all the answers and comments I've read here.
The main difference, this will work the same in both native Windows PowerShell (aka powershell.exe) and PowerShell Core (aka pwsh).

The output is a [System.Security.Cryptography.X509Certificates.X509Certificate2] type object, which contains all of the certificate information.

You can then export the certificate using .Export, example syntax:

$certificate = Get-WebsiteCertificate 'https://www.example.com'
$certBytes = $certificate.Export( [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert )
Set-Content -Value $certBytes -Encoding byte -Path '<.cer output file-path'

The Code

function Get-WebsiteCertificate {
[CmdletBinding()] param(
    [Parameter(Mandatory=$true)]
    [System.Uri] $Uri
)
    $certificate = $null
    
    if( $PSVersionTable.PSEdition -eq 'Desktop' ) {
        $webRequest = [Net.WebRequest]::Create($Uri)
        try { $null = $webRequest.GetResponse() } catch {}
        $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($webRequest.ServicePoint.Certificate)
    } else {
        function TestValidatorType {
            $appDomainHasAssembly = ([System.AppDomain]::CurrentDomain.GetAssemblies()) |
            ForEach-Object ExportedTypes |
            Where-Object FullName -match '^My.PSUtils.Net.Validator'
            $appDomainHasAssembly
        }

        if ( -not ( TestValidatorType ) ) {    
            $csSource = @"
    using System;
    using System.Net;
    using System.Net.Security;
    using System.Security.Cryptography.X509Certificates;
    
    namespace My.PSUtils.Net
    {
        public class Validator {  
            public static bool SkipCertificateCheckValidator(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
            return true;
            }
        }
    }
"@ # This closing quote must not be indented (it must be on the 1st column)
        
            Add-Type -ReferencedAssemblies @() -TypeDefinition $csSource -Language CSharp -WarningAction SilentlyContinue
        }

        if ( -not ( TestValidatorType ) ) { throw "Validator missing"; }

        $local:client = [System.Net.Sockets.TcpClient]::new($Uri.Host, $Uri.Port)
        $local:clientStream = $client.GetStream()
        $local:stream = [System.Net.Security.SslStream]::new($clientStream, $true, [My.PSUtils.Net.Validator]::SkipCertificateCheckValidator)
        if( $stream ) {
            try {
                if( -not $stream.RemoteCertificate ) {
                    Write-Verbose "Authenticating"
                    $stream.AuthenticateAsClient($Uri.Host)
                }
                $certificate = $stream.RemoteCertificate
            } finally {
                $stream.Close()
                $stream.Dispose()
            }
        }
    }
    if( $certificate ) { $certificate }
}

Upvotes: 0

Gzeh Niert
Gzeh Niert

Reputation: 168

@FireFlying, here is a solution which works on .NET Core based PowerShell version.

First part is to be run once per console or script

$csSource = @"
using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace My.PSUtils.Net
{
  public class Validator {  
    public static bool SkipCertificateCheckValidator(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
      return true;
    }
  }
}
"@
$appDomainHasAssembly = ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { @($_.ExportedTypes | ForEach-Object { $_.FullName }) -Contains "My.PSUtils.Net" }).Count -gt 0
if (-not $appDomainHasAssembly) {  
  Add-Type -ReferencedAssemblies @() -TypeDefinition $csSource -Language CSharp -WarningAction SilentlyContinue
}

It creates a callback to override certificate checks .NET does by default.

Then the certificate is retrieved using a TCPClient, and this part can be included in a loop if required:

$uriBuilder = [System.UriBuilder]::New("https://www.outlook.com")
$client = [System.Net.Sockets.TcpClient]::new($uriBuilder.Uri.Host, $uriBuilder.Uri.Port)
$stream = [System.Net.Security.SslStream]::new($client.GetStream(), $true, [My.PSUtils.Net.Validator]::SkipCertificateCheckValidator)
$stream.AuthenticateAsClient($uriBuilder.Uri.Host) | Out-Null
$certificate = $stream.RemoteCertificate
$stream.Close()
$stream.Dispose()
$certificate | Format-List Subject,NotBefore,NotAfter,DnsNameList

Tested successfully on PS v7.3.10 on Windows.

Hope it helps.

Upvotes: 0

FireFlying
FireFlying

Reputation: 41

@RafaMarrara's answer worked perfectly for me with one clarification: you cannot perform this from a .NET Core application, so I had to open up PowerShell 5.1 to get this to run. Running this in PowerShell 7 resulted in the Certificate property always being blank.

The reason for this is that .NET Core drops support for the ServicePointManager and ServicePoint classes (reference).

Upvotes: 3

RafaMarrara
RafaMarrara

Reputation: 851

To share more knowledge :-)

$webRequest = [Net.WebRequest]::Create("https://www.outlook.com")
try { $webRequest.GetResponse() } catch {}
$cert = $webRequest.ServicePoint.Certificate
$bytes = $cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)
set-content -value $bytes -encoding byte -path "$pwd\Outlook.Com.cer"

My co-worker Michael J. Lyons shared this with me.

Upvotes: 51

Andrew Savinykh
Andrew Savinykh

Reputation: 26270

From http://poshcode.org/2521:

function Get-WebsiteCertificate {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory=$true)] [System.Uri]
      $Uri,
    [Parameter()] [System.IO.FileInfo]
      $OutputFile,
    [Parameter()] [Switch]
      $UseSystemProxy,  
    [Parameter()] [Switch]
      $UseDefaultCredentials,
    [Parameter()] [Switch]
      $TrustAllCertificates
  )
  try {
    $request = [System.Net.WebRequest]::Create($Uri)
    if ($UseSystemProxy) {
      $request.Proxy = [System.Net.WebRequest]::DefaultWebProxy
    }

    if ($UseSystemProxy -and $UseDefaultCredentials) {
      $request.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
    }

    if ($TrustAllCertificates) {
      # Create a compilation environment
      $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider
      $Compiler=$Provider.CreateCompiler()
      $Params=New-Object System.CodeDom.Compiler.CompilerParameters
      $Params.GenerateExecutable=$False
      $Params.GenerateInMemory=$True
      $Params.IncludeDebugInformation=$False
      $Params.ReferencedAssemblies.Add("System.DLL") > $null
      $TASource=@'
        namespace Local.ToolkitExtensions.Net.CertificatePolicy {
          public class TrustAll : System.Net.ICertificatePolicy {
            public TrustAll() { 
            }
            public bool CheckValidationResult(System.Net.ServicePoint sp,
              System.Security.Cryptography.X509Certificates.X509Certificate cert, 
              System.Net.WebRequest req, int problem) {
              return true;
            }
          }
        }
'@ 
      $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)
      $TAAssembly=$TAResults.CompiledAssembly

      ## We now create an instance of the TrustAll and attach it to the ServicePointManager
      $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")
      [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll
    }

    $response = $request.GetResponse()
    $servicePoint = $request.ServicePoint
    $certificate = $servicePoint.Certificate

    if ($OutputFile) {
      $certBytes = $certificate.Export(
          [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert
        )
      [System.IO.File]::WriteAllBytes( $OutputFile, $certBytes )
      $OutputFile.Refresh()
      return $OutputFile
    } else {
      return $certificate
    }
  } catch {
    Write-Error "Failed to get website certificate. The error was '$_'."
    return $null
  }

  <#
    .SYNOPSIS
      Retrieves the certificate used by a website.

    .DESCRIPTION
      Retrieves the certificate used by a website. Returns either an object or file.

    .PARAMETER  Uri
      The URL of the website. This should start with https.

    .PARAMETER  OutputFile
      Specifies what file to save the certificate as.

    .PARAMETER  UseSystemProxy
      Whether or not to use the system proxy settings.

    .PARAMETER  UseDefaultCredentials
      Whether or not to use the system logon credentials for the proxy.

    .PARAMETER  TrustAllCertificates
      Ignore certificate errors for certificates that are expired, have a mismatched common name or are self signed.

    .EXAMPLE
      PS C:\> Get-WebsiteCertificate "https://www.gmail.com" -UseSystemProxy -UseDefaultCredentials -TrustAllCertificates -OutputFile C:\gmail.cer

    .INPUTS
      Does not accept pipeline input.

    .OUTPUTS
      System.Security.Cryptography.X509Certificates.X509Certificate, System.IO.FileInfo
  #>
}

function Import-Certificate {
<#
    .SYNOPSIS
        Imports certificate in specified certificate store.

    .DESCRIPTION
        Imports certificate in specified certificate store.

    .PARAMETER  CertFile
        The certificate file to be imported.

    .PARAMETER  StoreNames
        The certificate store(s) in which the certificate should be imported.

    .PARAMETER  LocalMachine
        Using the local machine certificate store to import the certificate

    .PARAMETER  CurrentUser
        Using the current user certificate store to import the certificate

    .PARAMETER  CertPassword
        The password which may be used to protect the certificate file

    .EXAMPLE
        PS C:\> Import-Certificate C:\Temp\myCert.cer

        Imports certificate file myCert.cer into the current users personal store

    .EXAMPLE
        PS C:\> Import-Certificate -CertFile C:\Temp\myCert.cer -StoreNames my

        Imports certificate file myCert.cer into the current users personal store

    .EXAMPLE
        PS C:\> Import-Certificate -Cert $certificate -StoreNames my -StoreType LocalMachine

        Imports the certificate stored in $certificate into the local machines personal store 

    .EXAMPLE
        PS C:\> Import-Certificate -Cert $certificate -SN my -ST Machine

        Imports the certificate stored in $certificate into the local machines personal store using alias names

    .EXAMPLE
        PS C:\> ls cert:\currentUser\TrustedPublisher | Import-Certificate -ST Machine -SN TrustedPublisher

        Copies the certificates found in current users TrustedPublishers store to local machines TrustedPublisher using alias  

    .INPUTS
        System.String|System.Security.Cryptography.X509Certificates.X509Certificate2, System.String, System.String

    .OUTPUTS
        NA

    .NOTES
        NAME:      Import-Certificate
        AUTHOR:    Patrick Sczepanksi (Original anti121)
        VERSION:   20110502
        #Requires -Version 2.0
    .LINK
        http://poshcode.org/2643
        http://poshcode.org/1937 (Link to original script)

#>

    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline=$true,Mandatory=$true, Position=0, ParameterSetName="CertFile")]
        [System.IO.FileInfo]
        $CertFile,

        [Parameter(ValueFromPipeline=$true,Mandatory=$true, Position=0, ParameterSetName="Cert")]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]
        $Cert,

        [Parameter(Position=1)]
        [Alias("SN")]
        [string[]] $StoreNames = "My",

        [Parameter(Position=2)]
        [Alias("Type","ST")]
        [ValidateSet("LocalMachine","Machine","CurrentUser","User")]
        [string]$StoreType = "CurrentUser",

        [Parameter(Position=3)]
        [Alias("Password","PW")]
        [string] $CertPassword
    )

    begin
    {
        [void][System.Reflection.Assembly]::LoadWithPartialName("System.Security")
    }

    process 
    {
        switch ($pscmdlet.ParameterSetName) {
            "CertFile" {
                try {
                    $Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $($CertFile.FullName),$CertPassword
                }
                catch {   
                    Write-Error ("Error reading '$CertFile': $_ .") -ErrorAction:Continue
                }
            }
            "Cert" {

            }
            default {
                Write-Error "Missing parameter:`nYou need to specify either a certificate or a certificate file name."
            }
        }

        switch -regex ($storeType) {
            "Machine$" { $StoreScope = "LocalMachine" }
            "User$"  { $StoreScope = "CurrentUser" }
        } 

        if ( $Cert ) {
            $StoreNames | ForEach-Object {
                $StoreName = $_
                Write-Verbose " [Import-Certificate] :: $($Cert.Subject) ($($Cert.Thumbprint))"
                Write-Verbose " [Import-Certificate] :: Import into cert:\$StoreScope\$StoreName"

                if (Test-Path "cert:\$StoreScope\$StoreName") {
                    try
                    {
                        $store = New-Object System.Security.Cryptography.X509Certificates.X509Store $StoreName, $StoreScope
                        $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
                        $store.Add($Cert)
                        if ( $CertFile ) {
                            Write-Verbose " [Import-Certificate] :: Successfully added '$CertFile' to 'cert:\$StoreScope\$StoreName'."
                        } else {
                            Write-Verbose " [Import-Certificate] :: Successfully added '$($Cert.Subject) ($($Cert.Thumbprint))' to 'cert:\$StoreScope\$StoreName'."
                        }
                    }
                    catch
                    {
                        Write-Error ("Error adding '$($Cert.Subject) ($($Cert.Thumbprint))' to 'cert:\$StoreScope\$StoreName': $_ .") -ErrorAction:Continue
                    }
                    if ( $store ) {
                        $store.Close()
                    }
                } 
                else {
                    Write-Warning "Certificate store '$StoreName' does not exist. Skipping..."
                }
            }
        } else {
            Write-Warning "No certificates found."
        }
    }

    end { 
        Write-Host "Finished importing certificates." 
    }
}

I successfully used these functions like this:

##Import self-signed certificate
Get-WebsiteCertificate $baseUrl local.cer -trust | Out-Null
Import-Certificate -certfile local.cer -SN Root  | Out-Null

Upvotes: 3

Robert Westerlund
Robert Westerlund

Reputation: 4838

You should be able to get the public key by using the ServicePoint property on the HttpWebRequest object. This necessary information will be populated once we have made a http request to the site in question.

If the request is made to a site which has an untrusted certificate the GetResponse method will throw an exception, However, the ServicePoint will still contain the Certificate so we want to ensure we ignore WebException if the status is a trust failure.

So something like the following should work:

function Get-PublicKey
{
    [OutputType([byte[]])]
    PARAM (
        [Uri]$Uri
    )

    if (-Not ($uri.Scheme -eq "https"))
    {
        Write-Error "You can only get keys for https addresses"
        return
    }

    $request = [System.Net.HttpWebRequest]::Create($uri)

    try
    {
        #Make the request but ignore (dispose it) the response, since we only care about the service point
        $request.GetResponse().Dispose()
    }
    catch [System.Net.WebException]
    {
        if ($_.Exception.Status -eq [System.Net.WebExceptionStatus]::TrustFailure)
        {
            #We ignore trust failures, since we only want the certificate, and the service point is still populated at this point
        }
        else
        {
            #Let other exceptions bubble up, or write-error the exception and return from this method
            throw
        }
    }

    #The ServicePoint object should now contain the Certificate for the site.
    $servicePoint = $request.ServicePoint
    $key = $servicePoint.Certificate.GetPublicKey()
    Write-Output $key
}

Get-PublicKey -Uri "https://www.bing.com"
Get-PublicKey -Uri "https://www.facebook.com"

If you want to call the method many times and some might have the same address, you might want to improve the function by using the ServicePointManager.FindServicePoint(System.Uri) method, since it will return a cached version if a request has already been made to that site. So you could check if the service point has been populated with information. If it hasn't, make the web request. If it has, just use the already existing information, saving yourself an http request.

Upvotes: 13

Related Questions