Saturday, November 20, 2021

Keeping Your Google Certs Fresh

Two down, one remains.

A work thing that we ran into is that Google sometimes updates its root certificates. This can cause some panic on the part of some because they need to keep these up-to-date to facilitate inter-application communication. 

So, to that end, I figured it was time to get proactive in a manner by detecting an update so we can at least tell people this particular change has occurred and how to deal with it. The next step might be to actually update the file where it need to live on the system, but let's not get ahead of ourselves.

I work in a hybrid environment and the idea was that we can build this to run as a service or a scheduled job (preferred) is just a given. It's less complicated on a Linux-based system so one solutions is written in bash, but many of our systems run on Windows Server so I figured PowerShell was also wise.

My first attempt was PowerShell and that does the job, but for parity I wrote the bash version to product the same output.

I'm modelling this on a Windows 10 system with the Ubuntu WSL2 for the bash shell. The intent is the script can live in a central location and you'd run it in a folder to capture and store the data/logs.

The command lines:

../pemWatch.sh sh-google.com https://pki.goog/roots.pem

..\pemWatch.ps1 ps-google.com https://pki.google.com/roots.pem

Yes, there is a reason why the URLs are different, it seems PowerShell handles a redirect better as the URL delivers this when I use curl under Linux.

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://pki.goog/roots.pem">here</A>.
</BODY></HTML>

I guess things change on the Internet ;-) ...

As I have said before, and I will always remind you, I'm not a scripting guru, I just find a way. 

The Bash version:

Let's step through the bash script first:

#!/bin/bash
null=""
if [ -n $1 ]
then
    name="$1"
else
    name=default-test
fi

if [ -n $2 ]
then
    url="$2"
else
    url=https://pki.goog/roots.pem
fi

This accepts the command-line arguments and stuff in default if they're not present. Theoretically this could be useful for any monitoring of a file you need to pay attention to out there, so... 

function main {
    rep=.
    with=-
    name="${name//$rep/$with}"

    lastMd5=$(cat ${name}.md5)
    md5=$(curl ${url} | md5sum)
    md5x=$(echo ${md5} | cut -d' ' -f 1)
    if [[ $lastMd5 != $md5x ]]
        then
            echo $md5x $lastMd5
            echo $md5x>$name.md5
            curl ${url} --output $name.root_pem > /dev/null 2>&1
            triggerAlert
    else
            echo $name :: $md5x
    fi
}

The main Function, and I do like the structured coding of functions, handles reformatting the name to be file-safe (replacing and periods with a dash). We move on to loading the last MD5 value from a file for reference as lastMd5 then we collect the most recent version of the file and collect the MD5 on that into the variable, md5.

We need to tidy of the value by trimming the tail off it and trowing it into the variable md5x and we get into the comparison of it all. The key to this is the simple comparison of the lastMd5 variable with md5x, if they don't match the file has changed. If the file has changed we log it on-screen and into the $name.log file using the function triggerAlert.

function triggerAlert {
    echo "Alert!"
    date=$(date '+%Y-%m-%d %H:%M:%S')
    echo "ALERT:: $date :: $lastMd5 => $md5x" 1>> $name.log
    # add any advanced handling here
}

This function, at this time, simply writes a log of the event but it could send an email or potentially trigger some webhook into something more meaningful. 

The script wraps up with a call to main because the functions need to precede their call.

# -------------------- MAIN

main

The PowerShell version:

You will see the parallels here in PowerShell to how the bash script was built. First we handle the command-line parameters.

param(
    [Parameter(Mandatory = $false, Position = 1)][string]$name = $null,
    [Parameter(Mandatory = $false, Position = 2)][string]$url = $null
    ,[Parameter(Mandatory = $false)][switch]$reset = $null
)

As before the main function is where the bulk of the action happens. We ensure the parameters are present and valid, we normalize the name, then fetch the file into the variable $dl (download). We then call a function I found on the Internet to perform an MD5 hash on the file while in memory.

function main{
    if($name + "" -eq ""){
        $name = "google.com"
    }
    if($url +"" -eq ""){
        $url = "https://pki.google.com/roots.pem"
    }
    $name = $name.replace('.','-')
    $name = $name.replace(' ','-')
    $name = $name.ToLower()

    $dl = Invoke-WebRequest -Uri $url -UseBasicParsing
    $md5 = Get-StringHash $dl

    if(Test-Path -Path $($name + ".md5") -PathType Leaf){
        $lastMD5 = Get-Content -Path $($name + ".md5")
    } else {

        }
    if($md5 -ne $lastMD5){
        Set-Content -Path $($name + ".root_pem") -value $dl
        Set-Content -Path $($name + ".md5") -value $md5
        if($lastMD5 + "" -ne ""){
            triggerAlert $name $url
            write-host "ALERT! Checking on $name..."
        } else {
            write-host "New!   No alert on $name..."
        }
    }

    if($reset){
        if(Test-Path -Path $($name + ".root_pem") -Pathtype Leaf){
            $devNull = Remove-Item -Path $($name + ".root_pem") -Force
        }
        if(Test-Path -Path $($name + ".md5") -Pathtype Leaf){
            $devNull = Remove-Item -Path $($name + ".md5") -Force
        }
    } else {
        Set-Content -Path $($name + ".root_pem") -Value $dl

    }
    write-host "$($name.padRight(25,' ')) :: $md5" -ForegroundColor White
}

There's a reset function that I haven't added to the bash script that resets the collected files but that's an extra. The function below, Get-StringHash, will return the MD5 hash for use.

Function Get-StringHash
{
    param
    (
        [String] $String,
        $HashName = "MD5"
    )

    $bytes = [System.Text.Encoding]::UTF8.GetBytes($String)
    $algorithm = [System.Security.Cryptography.HashAlgorithm]::Create('MD5')
    $StringBuilder = New-Object System.Text.StringBuilder
 
    $algorithm.ComputeHash($bytes) |
    ForEach-Object {
        $null = $StringBuilder.Append($_.ToString("x2"))
    }
 
    $StringBuilder.ToString()
}

Just like the bash script the following should be customized to meet your needs. Right now it writes a log just as the bash script does.

Function triggerAlert{
param(
    [Parameter(Mandatory = $true, Position = 1)][string]$name = $null,
    [Parameter(Mandatory = $true, Position = 2)][string]$url = $null
)

    #Send Email
    #Trigger Zabbix

    #Write to Log
    $logData = $(Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + " :: " + $md5.PadLeft(20,' ')
    if(Test-Path -Path $($name + ".log") -PathType Leaf){
        Add-Content -Path $($name + ".log") -Value $logData
    } else {
        Set-Content -Path $($name + ".log") -Value $logData
    }
}

And we cannot forget to call main.

main


I hope this helps you see the parallels of scripting languages, that while we may need to learn the nuances of different languages the logic can often remain the same.

My scripts, shared. Also, about that remaining one, I'll do this again in Python, or perhaps even Java or Rust, all languages I'm not terribly familiar with yet.



No comments:

There is no individual ownership when you are part of a team, it's the sum of the parts that makes you the RESILIENT team you need to be.