From c545ae6d48f25480bf6e814e70a1be6b74642ce9 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Fri, 30 Jan 2026 12:09:34 +1300 Subject: [PATCH] Add INF import and per-INF issuance --- certy.ps1 | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 3 deletions(-) diff --git a/certy.ps1 b/certy.ps1 index 6d91121..daa6c80 100644 --- a/certy.ps1 +++ b/certy.ps1 @@ -234,6 +234,62 @@ function Get-DefaultValue { return $prop.Value } +function Get-HostsFromInfLines { + param([string[]]$Lines) + $set = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase) + if (-not $Lines) { return @() } + + foreach ($line in $Lines) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + if ($line.TrimStart() -match '^[;#]') { continue } + $matches = [regex]::Matches($line, '(?i)\bdns\s*=\s*([^&",\s]+)') + foreach ($match in $matches) { + $value = $match.Groups[1].Value.Trim() + if ($value) { [void]$set.Add($value) } + } + } + + if ($set.Count -gt 0) { return $set | Sort-Object } + + $subjectLine = $Lines | Where-Object { $_ -match '(?i)^\s*subject\s*=' } | Select-Object -First 1 + if ($subjectLine -and ($subjectLine -match '(?i)\bCN\s*=\s*([^,"]+)')) { + $cn = $Matches[1].Trim() + if ($cn) { [void]$set.Add($cn) } + } + + return $set | Sort-Object +} + +function Remove-InfSubjectLines { + param([string[]]$Lines) + $removed = $false + $filtered = foreach ($line in $Lines) { + if ($line -match '(?i)^\s*subject\s*=') { + $removed = $true + continue + } + $line + } + return [pscustomobject]@{ + Lines = $filtered + Removed = $removed + } +} + +function Save-SanitizedInf { + param( + [string]$FileName, + [string[]]$Lines + ) + $dir = Join-Path $env:ProgramData "Certy\\inf-sanitized" + if (-not (Test-Path -Path $dir -PathType Container)) { + New-Item -Path $dir -ItemType Directory -Force | Out-Null + } + $outPath = Join-Path $dir $FileName + Set-Content -Path $outPath -Value $Lines -Encoding ascii + return $outPath +} + function Resolve-HostEntry { param( [string]$Name, @@ -712,6 +768,22 @@ $panel.Controls.Add($csrImportBtn) Style-ButtonSecondary $csrImportBtn $y += $rowHeight + $gap +$infLabel = Add-Label "INF folder (optional)" $xLabel $y $labelWidth $rowHeight +$infFolderBox = Add-TextBox $xInput $y ($inputWidth - ((2 * $buttonWidth) + $buttonGap)) $rowHeight $false +$infBrowseBtn = New-Object System.Windows.Forms.Button +$infBrowseBtn.Text = "Browse" +$infBrowseBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - (2 * $buttonWidth + $buttonGap)), ($y - 1)) +$infBrowseBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) +$panel.Controls.Add($infBrowseBtn) +Style-ButtonSecondary $infBrowseBtn +$infImportBtn = New-Object System.Windows.Forms.Button +$infImportBtn.Text = "Import INF" +$infImportBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) +$infImportBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) +$panel.Controls.Add($infImportBtn) +Style-ButtonSecondary $infImportBtn +$y += $rowHeight + $gap + $useFqdnBox = Add-CheckBox "Input contains FQDNs (otherwise default zone is appended)" $xInput $y $inputWidth $rowHeight $y += $rowHeight + $gap @@ -909,6 +981,8 @@ $sectionAnchors = @{ Logs = $sectionActivity } +$script:infRequests = @() + foreach ($key in $sectionAnchors.Keys) { $label = $navLabels[$key] if (-not $label) { continue } @@ -996,7 +1070,7 @@ function Show-HelpDialog { $stepY = 28 $steps = @( - "Add hostnames (one per line, CSV, or CSR folder) then click 1) Preview.", + "Add hostnames (one per line, CSV, CSR, or INF folder) then click 1) Preview.", "Click 2) Scan and select the DNS server (Primary is 10.106.60.1). Replication runs from there.", "Set the replication wait time. Typical value: 15 minutes.", "Choose output type (PEM or PFX). If PFX, supply a password.", @@ -1192,6 +1266,9 @@ function Apply-Layout { $csrFolderBox.Width = $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) $csrBrowseBtn.Left = $xInput + $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) $csrImportBtn.Left = $xInput + $inputWidthCalc - $buttonWidth + $infFolderBox.Width = $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) + $infBrowseBtn.Left = $xInput + $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) + $infImportBtn.Left = $xInput + $inputWidthCalc - $buttonWidth $zoneBox.Width = $inputWidthCalc $ipBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap) $ipRefreshBtn.Left = $xInput + $inputWidthCalc - $buttonWidth @@ -1276,6 +1353,13 @@ $csrBrowseBtn.Add_Click({ } }) +$infBrowseBtn.Add_Click({ + $dialog = New-Object System.Windows.Forms.FolderBrowserDialog + if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { + $infFolderBox.Text = $dialog.SelectedPath + } +}) + $csrImportBtn.Add_Click({ try { $folder = $csrFolderBox.Text.Trim() @@ -1298,6 +1382,58 @@ $csrImportBtn.Add_Click({ } }) +$infImportBtn.Add_Click({ + try { + $folder = $infFolderBox.Text.Trim() + if (-not $folder) { throw "INF folder is empty." } + if (-not (Test-Path -Path $folder -PathType Container)) { throw "INF folder not found: $folder" } + + $infFiles = Get-ChildItem -Path $folder -Filter *.inf -File -Recurse + if (-not $infFiles) { + & $logAction "No INF files found in $folder" + return + } + + $script:infRequests = @() + $infHosts = @() + $subjectRemovedCount = 0 + + foreach ($infFile in $infFiles) { + $lines = Get-Content -Path $infFile.FullName + $hosts = @(Get-HostsFromInfLines -Lines $lines) + $sanitize = Remove-InfSubjectLines -Lines $lines + $sanitizedPath = Save-SanitizedInf -FileName $infFile.Name -Lines $sanitize.Lines + if ($sanitize.Removed) { $subjectRemovedCount++ } + + if ($hosts.Count -eq 0) { + & $logAction "INF $($infFile.Name): no hostnames detected." + continue + } + + $script:infRequests += [pscustomobject]@{ + File = $infFile.FullName + Hosts = $hosts + Sanitized = $sanitizedPath + } + $infHosts += $hosts + } + + $infHosts = @($infHosts | Where-Object { $_ } | Sort-Object -Unique) + if ($infHosts.Count -gt 0) { + $currentHosts = Split-List $hostsBox.Text + $merged = Merge-Hostnames -Existing $currentHosts -NewItems $infHosts + $hostsBox.Text = ($merged -join [Environment]::NewLine) + } + + & $logAction "Imported $($infFiles.Count) INF file(s), added $($infHosts.Count) hostname(s)." + if ($subjectRemovedCount -gt 0) { + & $logAction "Removed Subject line from $subjectRemovedCount INF file(s) (sanitized copies saved)." + } + } catch { + & $logAction "Error: $($_.Exception.Message)" + } +}) + $dnsScanBtn.Add_Click({ try { $dnsListBox.Items.Clear() @@ -1506,7 +1642,29 @@ $runBtn.Add_Click({ if (-not (Test-Path -Path $wacsPath -PathType Leaf)) { throw "WACS not found at: $wacsPath" } - if ($perHostBox.Checked) { + if ($script:infRequests -and $script:infRequests.Count -gt 0) { + & $logAction "INF requests detected; issuing one certificate per INF file." + foreach ($req in $script:infRequests) { + $reqEntries = @($req.Hosts | ForEach-Object { Resolve-HostEntry -Name $_ -Zone $zone -UseProvidedFqdn $useFqdnBox.Checked } | Where-Object { $_ }) + $reqFqdns = @($reqEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ }) + if ($reqFqdns.Count -eq 0) { + & $logAction "INF $([System.IO.Path]::GetFileName($req.File)) skipped (no hosts)." + continue + } + & $logAction "Requesting certificate for INF $([System.IO.Path]::GetFileName($req.File)) with $($reqFqdns.Count) hostname(s)." + Invoke-Wacs ` + -WacsPath $wacsPath ` + -HostFqdns $reqFqdns ` + -OutputType $outputType ` + -OutputPath $outputPath ` + -PfxPassword ($(if ($usePfxPassword) { $pfxPasswordBox.Text } else { "" })) ` + -BaseUri $baseUriBox.Text.Trim() ` + -Validation $validationBox.Text.Trim() ` + -ValidationPort $validationPortBox.Text.Trim() ` + -Verbose $verboseBox.Checked ` + -Log $logAction + } + } elseif ($perHostBox.Checked) { foreach ($entry in $hostEntries) { & $logAction "Requesting certificate for $($entry.Fqdn)." Invoke-Wacs ` @@ -1522,7 +1680,7 @@ $runBtn.Add_Click({ -Log $logAction } } else { - $hostList = @($hostEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ }) + $hostList = @($hostEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ }) & $logAction "Requesting one certificate with $($hostList.Count) hostname(s)." Invoke-Wacs ` -WacsPath $wacsPath `