Goals
I was working on renewing all the non-prod certs and, mostly, the effort is around renewing a server certificate on Windows 2019. Once I have that though, the application I support has both IIS and Wildfly components. So we need to load the new cert into a few JAVA keystores. There were instructions on how to use keystore explorer but the GUI application needs a certain version and Wildfly is running a certain version; getting those to align on all the machines wasn’t happening!
Since keytool.exe
is included with the JDK, I figured I would just use the CMD to load new certs. Wow, the commands are long, the syntax is unforgiving, and a single typo can blow up my progress. I figured I would just write a wrapper for the commands. Instead of just wrapping keytool
commands, I wound up making an interactive and user-friendly experience. I knew that making it this way, I could hand off the script to other people on the team and they wouldn’t have to learn all about keytool
.
This post isn’t about the intricacies of keytool
, but about the User Experience (UX) patterns that can be built into our command-line tools to make them powerful and pleasant to use.
Smart Scanning and Background Jobs
The first challenge is always, “What file do I need to edit?” Keystores can be anywhere. A good tool helps the user find them without frustration.
1. Guided Input: I started simple. The script needs to know which drive to scan. Instead of just hoping the user types a valid letter, we present them with the available options and loop until we get a valid choice.
# from fix wildfly keystore.ps1
Write-Output "Get a list of existing drive letters..."
$existingDrives = Get-PSDrive | Where-Object { $_.Provider.Name -eq 'FileSystem' } | Select-Object -ExpandProperty Name
Write-Output "Getting list of drives..."
do {
Write-Host "Drives found:`n$($existingDrives -join ', ')"
$driveLetter = Read-Host "`nEnter the drive letter to scan"
if ($existingDrives -contains $driveLetter.ToUpper()) {
Write-Output "Using drive: $driveLetter"
break
}
else {
Write-Warning "Drive letter '$driveLetter' does not exist. Please enter a valid drive letter."
}
} while ($existingDrives -notcontains $driveLetter.ToUpper())
2. Instant Gratification with Background Processing: A full disk scan is slow. Making the user wait is a poor experience. At a suggestion of a colleague, this seemed like a chance at a two-pronged attack.
- Synchronous Scan: I immediately scan common, known locations like
$env:JAVA_HOME
,$env:JBOSS_HOME
, andC:\Wildfly
. This gives the user a list of probable files in seconds. - Asynchronous Scan: Simultaneously, I kick off a PowerShell background job (
Start-Job
) to perform the slow, deep scan of the entire drive.
# from fix wildfly keystore.ps1
# Start the background job for full scan
Write-Output "Starting background scan for keystores on drive $driveLetter..."
$scanJob = Start-Job -ScriptBlock {
param($driveLetter)
$cacerts = Get-ChildItem -Path "$driveLetter`:\" -Filter 'cacer*' -Recurse -File -ErrorAction SilentlyContinue
$keystores = Get-ChildItem -Path "$driveLetter`:\" -Filter '*.jks' -Recurse -File -ErrorAction SilentlyContinue
return @($cacerts + $keystores) | Select-Object FullName -Unique
} -ArgumentList $driveLetter
# ... meanwhile, check known paths immediately ...
The main script loop can then check on the job’s status, notify the user that it’s still running, and merge the results once it’s complete. The user gets to start working right away without a long wait.
Interactive Menus
Once we have a list of files, I switched from discovery to user interaction. While the scan is running in the background, I provide simple, interactive menus. Instead of forcing them to type long file paths or remember complex command flags, each found file is numbered.
1. File Selection: The found keystores are presented in a clean, numbered list. The user simply enters a number.
# from fix wildfly keystore.ps1
Write-Host "Files found:"
for ($i = 0; $i -lt $wildflypath.Count; $i++) {
switch -Wildcard ($wildflypath[$i].FullName) {
"$env:JAVA_HOME*" { $status = "<-- JAVA Home***" }
"$env:JBOSS_HOME\*" { $status = "<-- Wildfly Home***" }
"$driveLetter`:\Wildfly\*" { $status = "<-- Wildfly Default Path***" }
Default { $status = "" }
}
Write-Host ("[{0}] {1} " -f ($i + 1), $wildflypath[$i].FullName, $status) -NoNewline
if ($status -ne '') { Write-Host ("$status") -ForegroundColor Cyan }
else { Write-Host "" }
}
$selection = Read-Host "`nEnter the number of the file to work on..."
2. Highlighting Relevant Entries: After selecting a keystore, we list its contents. But a raw keytool -list
dump often includes dozens of generic CA certificates we don’t care about. The script filters these out using a predefined list of common CA names, presenting the user with only the custom, relevant aliases they are likely to manage.
Special paths (like the active Java or Wildfly home) are highlighted to provide extra context.
3. Action Menu From there, a simple command prompt (Enter # to delete, 'import', 'local', 'done'
) guides the user through the available actions. It’s discoverable, intuitive, and requires zero memorization of keytool
syntax.
Helper Functions: Abstracting Complexity
Some of the tasks are inherently multi-step. I am trying to abstract that complexity away into simple, single-purpose functions.
A perfect example is importing a certificate from the local Windows Certificate Store into a Java keystore. The manual process is cumbersome. The script boils this down to a single command: local
. This is powered by a chain of helper functions:
Invoke-LocalCertificateSelectionDialog
: Presents a numbered list of valid, non-expired certificates from the local machine store for the user to choose from.Export-LocalCertToTempPfx
: Handles theExport-PfxCertificate
cmdlet and temporary file creation.Import-PfxIntoKeystore
: The workhorse that orchestrates thekeytool
commands, intelligently finding the source alias in the PFX and prompting the user for the new destination alias.
By breaking the problem down, we create reusable components and a vastly simplified workflow for the end-user. They just select the cert they want, provide a new alias, and the script handles the rest.
Build Tools for Humans
Command-line tools don’t have to be cryptic and difficult. By applying some basic UX principles—guiding user input, providing timely feedback, using non-blocking operations for long tasks, and abstracting complexity—we can create scripts that are not only powerful but also a pleasure to use.
The next time you’re writing a script for yourself or your team, think about the human on the other side of the keyboard. A little extra effort in building a better experience can pay huge dividends in productivity and reduced frustration.
After all, I wrote this to make editing keystore files intuitive and easy so my teammates, and future me, don’t have to learn about keytool
!