Public/Compare-Keytab.ps1
|
<#
SPDX-License-Identifier: Apache-2.0 Copyright (c) 2025 Stefan Ploch #> function Compare-Keytab { <# .SYNOPSIS Compare two keytab files with optional timestamp-insensitive and key-byte comparisons. .DESCRIPTION Reads both keytabs, canonicalizes their entries (optionally ignoring timestamps), and compares structure and key bytes (by default). Returns an object with an Equal flag and a Differences collection describing mismatches. Use -IgnoreKeyBytes for structure-only comparisons. Use -RevealKeys to include sensitive key bytes in the diff (avoid in logs and CI artifacts). .PARAMETER ReferencePath Path to the baseline (reference) keytab. .PARAMETER CandidatePath Path to the candidate keytab to compare against the reference. .PARAMETER IgnoreTimestamp Ignore per-entry timestamps when comparing (useful for reproducible checks). .PARAMETER IgnoreKeyBytes Only compare structure (principal, name type, encryption type, KVNO). Do not compare key bytes. .PARAMETER RevealKeys Include raw key bytes in difference output. Sensitive—avoid in shared logs. .INPUTS System.String (file paths) or objects with FilePathRef/FilePathCand properties. .OUTPUTS PSCustomObject with properties Equal (bool) and Differences (collection). .EXAMPLE Compare-Keytab -ReferencePath .\tests\output\user.keytab -CandidatePath .\tests\output\roundtrip.keytab -IgnoreTimestamp Compare two keytabs while ignoring timestamps. .EXAMPLE Compare-Keytab -ReferencePath a.keytab -CandidatePath b.keytab -IgnoreKeyBytes Perform a structure-only comparison (no key-byte check). .NOTES #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position=0)] [Alias('FullNameRef','FilePathRef')] [ValidateNotNullOrEmpty()] [string]$ReferencePath, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position=1)] [Alias('FullNameCand','FilePathCand')] [ValidateNotNullOrEmpty()] [string]$CandidatePath, [switch]$IgnoreTimestamp, [switch]$IgnoreKeyBytes, # structure-only compare [switch]$RevealKeys # controls whether key bytes are included in diff output ) begin { Set-StrictMode -Version Latest if ($RevealKeys) { Write-Warning 'RevealKeys is sensitive: raw key bytes may appear in differences.' } $inRef = Resolve-PathUniversal -Path $ReferencePath -Purpose Input $inCand = Resolve-PathUniversal -Path $CandidatePath -Purpose Input function New-JoinKey { param([pscustomobject]$E) '{0}|{1}|{2}|etype={3}|kvno={4}' -f $E.Realm, ($E.Components -join '/'), $E.NameType, $E.Etype, $E.Kvno } function Sanitize-ForOutput { param([pscustomobject]$E, [bool]$IncludeKeys) if ($IncludeKeys) { return $E } # Return a copy without Key bytes [pscustomobject]@{ Realm = $E.Realm Components = @($E.Components) NameType = $E.NameType Kvno = $E.Kvno Etype = $E.Etype TimestampUtc = $E.TimestampUtc Key = $null } } } process { $readKeys = -not $IgnoreKeyBytes $refParsed = Read-Keytab -Path $inRef -RevealKeys:$readKeys $candParsed = Read-Keytab -Path $inCand -RevealKeys:$readKeys $refEntries = if ($refParsed -is [System.Array]) { $refParsed } elseif ($refParsed.Entries) { $refParsed.Entries } else { @($refParsed) } $candEntries = if ($candParsed -is [System.Array]) { $candParsed } elseif ($candParsed.Entries) { $candParsed.Entries } else { @($candParsed) } $ref = ConvertEntriesTo-KeytabCanonicalModel -Entries $refEntries -IgnoreTimestamp:$IgnoreTimestamp $cand = ConvertEntriesTo-KeytabCanonicalModel -Entries $candEntries -IgnoreTimestamp:$IgnoreTimestamp $mapRef = @{}; foreach ($e in $ref) { $mapRef[(New-JoinKey $e)] = $e } $mapCand = @{}; foreach ($e in $cand) { $mapCand[(New-JoinKey $e)] = $e } $allKeys = @($mapRef.Keys + $mapCand.Keys | Sort-Object -Unique) $diffs = New-Object System.Collections.Generic.List[object] $allKeys = @($mapRef.Keys + $mapCand.Keys | Sort-Object -Unique) $diffs = New-Object System.Collections.Generic.List[object] foreach ($k in $allKeys) { $a = $mapRef[$k]; $b = $mapCand[$k] if ($null -eq $a -and $null -ne $b) { $diffs.Add([pscustomobject]@{ Key =$k Status ='OnlyInCandidate' Detail =(Sanitize-ForOutput -E $b -IncludeKeys:$RevealKeys) }) | Out-Null; continue } if ($null -eq $b -and $null -ne $a) { $diffs.Add([pscustomobject]@{ Key =$k Status ='OnlyInReference' Detail =(Sanitize-ForOutput -E $a -IncludeKeys:$RevealKeys) }) | Out-Null; continue } if (-not $IgnoreKeyBytes) { $ka = $a.Key; $kb = $b.Key $eq = ($null -ne $ka -and $null -ne $kb -and $ka.Length -eq $kb.Length -and [System.Linq.Enumerable]::SequenceEqual([byte[]]$ka, [byte[]]$kb)) # throws an exception when the two arguments are different types if (-not $eq) { $diffs.Add([pscustomobject]@{ Key = $k Status = 'KeyMismatch' Reference = (Sanitize-ForOutput -E $a -IncludeKeys:$RevealKeys) Candidate = (Sanitize-ForOutput -E $b -IncludeKeys:$RevealKeys) }) | Out-Null } } } } end { return [pscustomobject]@{ Equal = ($diffs.Count -eq 0) Differences = $diffs } } } |