Public/Get-Fido2SshKey.ps1
|
function Get-Fido2SshKey { <# .SYNOPSIS Lists FIDO2 SSH keys currently configured in the local SSH directory. .DESCRIPTION Scans `-SshDirectory` (default `%USERPROFILE%\.ssh` on Windows and `$HOME/.ssh` on Linux/macOS) for FIDO2 SSH key public key files and returns one object per match. Both **resident** keys (stored on the authenticator, filenames contain `_rk`) and **non-resident (software) passkeys** (handle on disk only, no `_rk`) are listed. Use `-ResidentOnly` or `-NonResidentOnly` to narrow the results to one type. Each returned object has an `IsResident` property that indicates whether the key is a resident or non-resident credential. The command also surfaces canonical filename metadata (key type, label, thumbprint) and whether the matching private-key handle file exists. .PARAMETER SshDirectory Source folder. Defaults to `%USERPROFILE%\.ssh` on Windows and `$HOME/.ssh` on Linux/macOS. .PARAMETER Label Optional case-insensitive substring filter against the parsed label segment of the canonical filename. .PARAMETER ResidentOnly Return only resident keys (those with `_rk` in the filename). .PARAMETER NonResidentOnly Return only non-resident (software) passkeys (those without `_rk`). .EXAMPLE Get-Fido2SshKey .EXAMPLE Get-Fido2SshKey -Label work .EXAMPLE Get-Fido2SshKey -NonResidentOnly #> [CmdletBinding(DefaultParameterSetName = 'All')] [OutputType([pscustomobject])] param( [string]$SshDirectory = (Get-Fido2DefaultSshDirectory), [string]$Label, [Parameter(ParameterSetName = 'ResidentOnly')] [switch]$ResidentOnly, [Parameter(ParameterSetName = 'NonResidentOnly')] [switch]$NonResidentOnly ) if (-not (Test-Path -LiteralPath $SshDirectory)) { Write-Verbose "SSH directory not found: $SshDirectory" return } # Collect public key files, keeping track of whether each is resident. # Resident: id_*_sk_rk*.pub # Non-resident: id_*_sk_*.pub that do NOT match the resident pattern. $keyEntries = @() if (-not $NonResidentOnly) { $residentPubs = @(Get-ChildItem -Path $SshDirectory -File -Filter 'id_*_sk_rk*.pub' -ErrorAction SilentlyContinue) foreach ($pub in $residentPubs) { $keyEntries += [pscustomobject]@{ File = $pub; IsResident = $true } } } if (-not $ResidentOnly) { $allSkPubs = @(Get-ChildItem -Path $SshDirectory -File -Filter 'id_*_sk_*.pub' -ErrorAction SilentlyContinue) foreach ($pub in $allSkPubs) { # Skip files already captured as resident keys. if ($pub.Name -match '_rk') { continue } $keyEntries += [pscustomobject]@{ File = $pub; IsResident = $false } } } foreach ($entry in ($keyEntries | Sort-Object { $_.File.Name })) { $pub = $entry.File $isRes = $entry.IsResident $baseName = [System.IO.Path]::GetFileNameWithoutExtension($pub.Name) $privatePath = Join-Path $pub.DirectoryName $baseName $keyType = $null $parsedLabel = $null $thumbprint = $null if ($isRes) { # id_<typeSuffix>_rk[_<label>]_<thumb12> if ($baseName -match '^id_(?<typeSuffix>ed25519_sk|ecdsa_sk)_rk(?:_(?<label>.+))?_(?<thumb>[A-Za-z0-9]{12})$') { $keyType = switch ($matches['typeSuffix']) { 'ed25519_sk' { 'ed25519-sk' } 'ecdsa_sk' { 'ecdsa-sk' } default { $null } } $parsedLabel = $matches['label'] $thumbprint = $matches['thumb'].ToLowerInvariant() } } else { # id_<typeSuffix>_sk[_<label>]_<thumb12> (no _rk) if ($baseName -match '^id_(?<typeSuffix>ed25519_sk|ecdsa_sk)_sk(?:_(?<label>.+))?_(?<thumb>[A-Za-z0-9]{12})$') { $keyType = switch ($matches['typeSuffix']) { 'ed25519_sk' { 'ed25519-sk' } 'ecdsa_sk' { 'ecdsa-sk' } default { $null } } $parsedLabel = $matches['label'] $thumbprint = $matches['thumb'].ToLowerInvariant() } } $labelValue = if ($null -ne $parsedLabel) { $parsedLabel } else { '' } if ($Label -and ($labelValue -notlike "*$Label*")) { continue } $line = Get-Content -LiteralPath $pub.FullName -TotalCount 1 -ErrorAction SilentlyContinue $algorithm = $null $comment = $null if (-not [string]::IsNullOrWhiteSpace($line)) { $parts = $line -split '\s+' if ($parts.Count -ge 1) { $algorithm = $parts[0] } if ($parts.Count -ge 3) { $comment = ($parts[2..($parts.Count - 1)] -join ' ') } } $result = [pscustomobject]@{ Name = $baseName KeyType = $keyType Label = $parsedLabel Thumbprint = $thumbprint Algorithm = $algorithm Comment = $comment IsResident = $isRes PublicKeyPath = $pub.FullName PrivateKeyPath = $privatePath HasPrivateKey = (Test-Path -LiteralPath $privatePath) } $defaultDisplay = New-Object System.Management.Automation.PSPropertySet( 'DefaultDisplayPropertySet', [string[]]@('Label', 'Algorithm', 'IsResident') ) $result | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value ([System.Management.Automation.PSMemberInfo[]]@($defaultDisplay)) $result } } |