openssl s_client ... but in PowerShell?

One of my favorite SSL/TLS troubleshooting tools is the openssl s_client CLI context - but what if I want to pull peer certificate information from a client that doesn't have openssl binaries installed? Can we get similar functionality out of say, PowerShell 5.1 or PowerShell 7 on a vanilla Win10? Let's find out!

A glimpse into the long-long-ago

Many moons ago (in the naughts), before I figured out that you could make a legitimate career out of enterprise computering, I was obsessed with web development - so much in fact that the first real tech gig I got, my job was to write CSS(2) stylesheets from scratch and implement dynamic menu animation behavior in javascript. When I say javascript, I mean pure, unadulterated, stand-alone inline javascript - jQuery was not yet a thing. Well, it was actually JScript for all I knew, as we only had Windows 98 in my home growing up, and Internet Explorer 7 was the fanciest browser around when I first got the job.

jscript

In any case, the company I was working for went bankrupt in early 2008, just as I was getting ready to drop out of high school and work full time, yay! It left me slightly bitter, and so I sought out new challenges, working at a large managed hosting provider-type company and thought to myself, smugly, that I'd never have to worry about web stuff again.

At the same time however, everyone else took a great deal of interest in all things web, and all of a sudden HTTP was the new old hotness - not just on the web, but in highly specialized systems on closed-circuit enterprise networks as well. And of course all our big enterprise clients had public facing websites, intranet portals, extranet platforms and so on. So, the career I thought I'd left behind kept haunting me, and I ended up becoming the "web security" person of interest at my then-employer, and got the responsibility of optimizing our SSL Certificate sales and deployment processes, along with another junior Sysadmin.

Figuring out what tools and processes best fit the needs of our clients, negotiating re-selling contracts with vendors, and designing (and sometimes building) a lot of the tooling and automation required for it was a great experience, as it pushed me to challenge my own understanding of the intracacies of PKI, X509 and SSL/TLS - my head almost exploded (10-12 years later, I'm still not sure I'd consider myself an X509 or TLS "expert").


Pick the right tool; level up your cool

One of the most important lessons I learned early on through this experience can be summed up as:

"Identify the tools that help you get the job done; truly familiarize yourself with them"

After shadowing one of our unix admins months prior, I'd noticed that he managed to print the full SSL certificate associated with an SSL-terminated non-HTTP endpoint using the openssl command line tool:

openssl s_client -connect somefqdn:1234 -showcerts

Say what? At that point I'd naively assumed - having known no other way to do it - that you needed a browser to diagnose configuration issues with certificates (open browser -> navigate to endpoint -> observe potential browser error or open the certificate UI from the browser).

I quickly downloaded a Win32 port of the openssl binaries and started playing with the s_client and x509 contexts, and compared the output to the behavior i was seeing in different browsers. And I tell you, man did it paid off. Soon enough I was regarded as some sort of black wizard for having the ability to "predict", within seconds of receiving endpoint information, what exact browser warnings a clients customers might expect to see.

invincible

With SSL/TLS moving from a "nice-to-have" thing of 10-15 years ago, to a straight-up cornerstone of basic network security today, I've had the chance to share the super-power that is simply knowing about openssl s_client with a lot of other people.

But as someone who dabbles in Microsoft technologies more than anything else, and maybe also prides themself on being able to do almost anything in PowerShell, it always pained my a little to start with the sentence "So, go download this unofficial win32 build of openssl off the internet" in response to "how can I troubleshoot endpoint certificate issues?"

So today I wanna show you how we can build our own little openssl s_client-like certificate dumping utility in PowerShell, with no external dependencies. Sounds cool? Let's get crackin'!

What does openssl s_client do?

openssl s_client -connect FQDN:port:

  • Connects to FQDN on port port
  • Attempts to fulfil an SSL/TLS handshake
  • Prints the following:
    1. Connection status
    2. Chain verification status
    3. Certificate chain (as sent by the server)
    4. The peer certificate (base64 encoded)
    5. Details about the result of the handshake

By adding the -showcerts switch, openssl will print the full certificate chain in place of (4)

In the screenshot below you can see the first 3 (and a half) output sections from having connected to PowerShellGallery from WSL on my laptop:

openssl_gallery

You can see that it verified that the issuer of the top-level certificate in the issuance chain (the CN=Baltimore CyberTrust Root CA) is trusted ("verified", against my local ca files), and each trust relationship all the way down to the peer (or endpoint) certificate for www.powershellgallery.com.

These are obviously extremely important details when attempting to authenticate a remote endpoint, but for the purposes of this blog post and demonstration, I'm only interested in printing/returning the peer certificate itself. We're basically going for something like this:

PS C:\> Get-OpenSSLSClientReplacement -As Base64 
-----BEGIN CERTIFICATE-----
MII...
# base64
# base64
# base64
# and so on...
-----END CERTIFICATE-----

Time to break out SslStream!

Where to even begin, you ask? First of all, we need to be able to connect to our remote endpoint. For this, we can use a TcpClient - which in PowerShell might look something like this:

using namespace System.Net.Sockets

$fqdn = "www.powershellgallery.com"
$port = 443

$client = [TcpClient]::new()
try {
  # Connect to remote endpoint
  $client.Connect($fqdn, $port)
  
  # Obtain r/w stream
  $stream = $client.GetStream()

  # Write a request
  $stream.Write($sendBuffer, 0, $sendBuffer.Length)
}
finally{
  # clean up
  if($stream -is [IDisposable]){
    $stream.Dispose()
  }
  $client.Dispose()
}

Next obvious question: what does one write in this case? Had it been a regular non-SSL/TLS HTTP endpoint, we could have just written what we wanted - the second T in HTTP does stand for Text anyway:

$request = [System.Text.Encoding]::Ascii.GetBytes("GET / HTTP/1.1`n`n")
$stream.Write($request, 0, $request.Length)

But in this example, we're interested in information exchanged during the SSL/TLS handshake, long before we can worry about HTTP. Do you speak TLS Handshake Protocol? I know I don't, and I'm pretty sure I'd fail badly if I tried to implement it by hand, in PowerShell.

This is where the SslStream class comes in handy - as the name implies, it derives from Stream, and it's designed to wrap around an inner application-level Stream (like a stream of HTTP transactions), taking care of both the handshake and ongoing record encryption. It also happens to expose the remote peer certificate. In short, we're going to offload all the hard parts about this to SslStream. Nice!

Wrapping the underlying connection is as easy as passing the $stream we obtained earlier to the SslStream constructor:

using namespace System.Net.Security

#...
$sslStream = [SslStream]::new($stream)

Now that we have a thing that speaks SSL/TLS, we can proceed with the handshake with a single method call:

try {
  # We need to pass the expected host name to `AuthenticateAsClient`
  $sslStream.AuthenticateAsClient($fqdn)
  
  # Handshake succeeded
}
catch [System.Security.Authentication.AuthenticationException] {
  # Verification failed
}
catch {
  # Something else went terribly wrong
}

Finally, assuming the handshake succeeded in authenticating the remote endpoint, we can grab the remote peer certificate like this:

using namespace System.Security.Cryptography.X509Certificates

#...
$cert = [X509Certificate2]$sslStream.RemoteCertificate

I'm deliberately casting the RemoteCertificate property to [X509Certificate2], because:

  1. I can, and
  2. It has a way nicer interface than [X509Certificate].

Now we just need one final thing, support for outputting a base64-encoded version of the certificate as a string. Fear not, we don't need to sort out how to ASN.1 encode the thing first, we can simply call X509certificate2.Export() with an appropriate X509ContentType argument and then convert to base64 with line breaks:

function ConvertFrom-X509Certificate
{
  param([X509Certificate2]$Certificate)

  @(
    '-----BEGIN CERTIFICATE-----'
    [Convert]::ToBase64String(
      $Certificate.Export([X509ContentType]::Cert),
      [Base64FormattingOptions]::InsertLineBreaks
    )
    '-----END CERTIFICATE-----'
  ) -join [Environment]::NewLine
}

Putting it all together, we might end up with something that actually gets the job done!

s_client_ps1

This is obviously only a fraction of the functionality we get from openssl s_client, I'll be the first to admit, but still pretty cool :)

s_client.ps1 can be found here if you can't see it below