<# .SYNOPSIS Issue-WebADMCert-OpenSSL.ps1 .DESCRIPTION Vollautomatische Ausstellung + Installation eines Computer-/Client-Zertifikats (EAP-TLS) über RCDevs WebADM CA. Features / Ziele: - Kompatibel mit Windows PowerShell 5.1 (kein PS7-Syntax, kein System.Net.Http nötig) - Erzeugt RSA2048 Key + CSR via OpenSSL inkl. Extensions und Custom OIDs - Signiert CSR via WebADM JSON-RPC (manag) -> Sign_Certificate_Request - Optional: WebADM Cert_Auto_Confirm (Auto-Confirm Mode) mit IP-Restriktion - Optional: Erstellt PFX und importiert in Cert:\LocalMachine\My - Renewal/NOOP: Erzeugt kein neues Zertifikat wenn ein passendes noch gültig ist - UNC-Ausführung: OpenSSL von Share wird lokal gecached (nur exe + benötigte DLLs) - VC++ Runtime Handling: Kopiert fehlende VC++ Runtime DLLs aus Share lokal (optional) .RENEWAL LOGIK - ValidityDays (Default 90): Zertifikat wird so lange ausgestellt - RenewBeforeDays (Default 30): wenn Restlaufzeit <= 30 Tage => neu ausstellen - Matching bestehender Zertifikate erfolgt in Cert:\LocalMachine\My: * HasPrivateKey = True * EKU enthält Client Authentication * Subject CN entspricht -CN (Default: $env:COMPUTERNAME) * Issuer CN entspricht -IssuerCN (Default: "mfa.firma.de") * Optional: RequireWebAdmTypeValue matcht OID 1.3.6.1.4.1.34617.3.1.1 (ASCII) .SUBJECT / DESCRIPTION - Standardmäßig wird im Subject zusätzlich "description=" gesetzt. Damit sieht man im CSR/Cert eindeutig, dass es ein CLIENT/SERVER/... ist. - Abschaltbar mit -NoDescriptionRdn .OIDs / VLAN - OID 1.3.6.1.4.1.34617.3.1.1 (WebAdmType): Default WebAdmTypeValue="CLIENT" Abschaltbar mit -DisableWebAdmTypeExtension - OID 1.3.6.1.4.1.34617.3.1.2 (Tunnel/VLAN): Aktivierbar via -Vlan oder -TunnelExtensionValue "" -Vlan baut automatisch: Tunnel-Type=13,Tunnel-Medium-Type=6,Tunnel-Private-Group-ID= .WEBADM TYPE=OTHER PROBLEM WebADM schreibt Type=OTHER wenn params.type nicht gesendet wird. Dieses Script sendet IMMER: params.type, params.application, params.request, params.expires .OPENSSL UNC CACHE Wenn -OpenSslPath ein UNC ist, wird lokal nach -OpenSSLLocalDir gecached: openssl.exe + libcrypto*.dll + libssl*.dll Danach wird NUR die lokale exe ausgeführt (stabil, keine UNC DLL-Ladevorgänge). .VC++ RUNTIME (OpenSSL 3.x) ExitCode -1073741515 (0xC0000135) => Loader konnte Abhängigkeiten nicht laden. Typisch fehlt "Microsoft Visual C++ Redistributable 2015-2022 (x64)". Alternativ kann man benötigte DLLs neben openssl.exe legen. Dieses Script kann die wichtigsten DLLs aus -VcRuntimeSourceDir nach -VcRuntimeLocalDir kopieren. .PARAMETERS (Kurzbeschreibung) -OpenSslPath (req): Pfad zu openssl.exe (lokal oder UNC) -OpenSSLLocalDir: lokaler Cache-Pfad für OpenSSL (Default ProgramData\...\OpenSSL\bin) -ForceOpenSSLUpdate: Cache update erzwingen -VcRuntimeSourceDir: Quelle (Share) mit Runtime DLLs (VCRUNTIME140*.dll, MSVCP140.dll, ucrtbase.dll) -VcRuntimeLocalDir: Zielordner für Runtime DLLs (Default: OpenSSLLocalDir) -WebAdmUrl (req): https://.../manag/ -Credential (req): Passwortquelle (BasicAuth nutzt AuthUserDn + Credential.Password) -AuthUserDn (req): CN=... DN für WebADM Basic Auth -CertType (req): CLIENT / SERVER / OTHER -Application: openotp / spankey / smshub (Default openotp) -ValidityDays: 90 -RenewBeforeDays: 30 -ForceEnroll: immer neu ausstellen (Test) -IssuerCN: Filter (Default mfa.firma.de) -RequireWebAdmTypeValue: Filter auf bestehende Cert OID 34617.3.1.1 -Vlan: VLAN ID (1-4094) -> setzt Tunnel OID -TunnelExtensionValue: Alternative manuelle Tunnel Definition (hat Vorrang) -WebAdmTypeValue: Wert für OID 34617.3.1.1 (Default CLIENT) -DisableWebAdmTypeExtension: deaktiviert OID 34617.3.1.1 -NoDescriptionRdn: kein description= im Subject -OutputDir: Artefakte (cnf/key/csr/cert/pfx) -AutoInstallToLocalMachine: PFX erstellen + Import in LocalMachine\My -AutoGeneratePfxPassword: generiert Passwort automatisch (Kompatibilität zu alten Calls) -PersistEncryptedPfxPassword: speichert Passwort DPAPI-verschlüsselt in -PfxPasswordFile -PfxPasswordFile: Pfad für gespeichertes Passwort -PfxPassword: eigenes SecureString Passwort (optional) -EnableAutoConfirm: ruft Cert_Auto_Confirm (expires = AutoConfirmMinutes) -AutoConfirmMinutes: 1/5/10/15/30/60 (Default 5) -AutoConfirmApplication: Default = -Application -AutoConfirmAddresses: Default = lokale Host-IP Richtung WebAdmUrl -DisableAutoConfirmAfter: nach Signierung AutoConfirm wieder deaktivieren (expires=0) -TimeoutSec: HTTP Timeout -DebugMode: verbose Debug Logs inkl. FULL JSON .EXAMPLE & '.\Issue-WebADMCert-OpenSSL.ps1' ` -OpenSslPath "\\server.firma.de\SHARE\IT\OpenSSL-Win64\bin\openssl.exe" ` -OpenSSLLocalDir "C:\ProgramData\FIRMA\WebADM-PKI\OpenSSL\bin" ` -ForceOpenSSLUpdate ` -VcRuntimeSourceDir "\\server.firma.de\SHARE\IT\vcruntime-x64" ` -WebAdmUrl "https://mfa.firma.de/manag/" ` -Credential $cred ` -AuthUserDn "cn=operator,ou=benutzer,ou=it,dc=firma,dc=de" ` -CertType CLIENT ` -Application openotp ` -Vlan 123 ` -AutoInstallToLocalMachine ` -AutoGeneratePfxPassword ` -PersistEncryptedPfxPassword ` -EnableAutoConfirm ` -DisableAutoConfirmAfter ` -AutoConfirmMinutes 5 ` -IssuerCN "mfa.firma.de" ` -ValidityDays 90 ` -RenewBeforeDays 30 ` -DebugMode #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$OpenSslPath, [Parameter()] [string]$OpenSSLLocalDir = "$env:ProgramData\FIRMA\WebADM-PKI\OpenSSL\bin", [Parameter()] [switch]$ForceOpenSSLUpdate, [Parameter()] [string]$VcRuntimeSourceDir, [Parameter()] [string]$VcRuntimeLocalDir = "$env:ProgramData\FIRMA\WebADM-PKI\OpenSSL\bin", [Parameter(Mandatory=$true)] [string]$WebAdmUrl, [Parameter(Mandatory=$true)] [pscredential]$Credential, [Parameter(Mandatory=$true)] [string]$AuthUserDn, [Parameter(Mandatory=$true)] [string]$CertType, [Parameter()] [string]$Application = "openotp", [Parameter()] [int]$ValidityDays = 90, [Parameter()] [int]$RenewBeforeDays = 30, [Parameter()] [switch]$ForceEnroll, [Parameter()] [string]$IssuerCN = "mfa.firma.de", [Parameter()] [string]$RequireWebAdmTypeValue, [Parameter()] [ValidateRange(1,4094)] [int]$Vlan, [Parameter()] [string]$TunnelExtensionValue, [Parameter()] [switch]$NoDescriptionRdn, [Parameter()] [string]$WebAdmTypeValue = "CLIENT", [Parameter()] [switch]$DisableWebAdmTypeExtension, [Parameter()] [string]$CN = $env:COMPUTERNAME, [Parameter()] [string]$O = "Firma GmbH", [Parameter()] [string]$ST = "XXXX", [Parameter()] [string]$L = "XXXX", [Parameter()] [string]$C = "DE", [Parameter()] [string]$Email = "test@firma.de", [Parameter()] [string]$OutputDir = "$env:ProgramData\FIRMA\WebADM-PKI\out", [Parameter()] [switch]$AutoInstallToLocalMachine, [Parameter()] [switch]$AutoGeneratePfxPassword, [Parameter()] [switch]$PersistEncryptedPfxPassword, [Parameter()] [string]$PfxPasswordFile = ("$env:ProgramData\FIRMA\WebADM-PKI\pfxpwd_{0}.txt" -f $env:COMPUTERNAME), [Parameter()] [securestring]$PfxPassword, [Parameter()] [switch]$EnableAutoConfirm, [Parameter()] [ValidateSet(1,5,10,15,30,60)] [int]$AutoConfirmMinutes = 5, [Parameter()] [string]$AutoConfirmApplication, [Parameter()] [string]$AutoConfirmAddresses, [Parameter()] [switch]$DisableAutoConfirmAfter, [Parameter()] [int]$TimeoutSec = 180, [Parameter()] [switch]$DebugMode ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # --------------------------- # Logging # --------------------------- function Log([string]$msg) { $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") Write-Host "[$ts] $msg" } function LogDbg([string]$msg) { if ($DebugMode) { Log $msg } } # --------------------------- # Helpers # --------------------------- function Ensure-Dir([string]$path) { if (-not (Test-Path -LiteralPath $path)) { New-Item -ItemType Directory -Path $path -Force | Out-Null } } function Read-FileRaw([string]$path) { Get-Content -LiteralPath $path -Raw } function Get-FileSHA256([string]$path) { if (-not (Test-Path -LiteralPath $path)) { return $null } (Get-FileHash -Algorithm SHA256 -LiteralPath $path).Hash } function Test-IsUncPath([string]$path) { return ($path -match '^[\\]{2}[^\\]+[\\].+') } function Test-OpenSslDllsPresent([string]$binDir) { $crypto = @(Get-ChildItem -LiteralPath $binDir -Filter "libcrypto*.dll" -ErrorAction SilentlyContinue | Select-Object -First 1) $ssl = @(Get-ChildItem -LiteralPath $binDir -Filter "libssl*.dll" -ErrorAction SilentlyContinue | Select-Object -First 1) return ($crypto.Count -gt 0 -and $ssl.Count -gt 0) } # DER: 04 function Convert-ToDerOctetStringHex { param([Parameter(Mandatory=$true)][string]$Value) $bytes = [System.Text.Encoding]::ASCII.GetBytes($Value) $len = $bytes.Length if ($len -lt 128) { $lenHex = "{0:X2}" -f $len } elseif ($len -lt 256) { $lenHex = "81{0:X2}" -f $len } else { $lenHex = "82{0:X4}" -f $len } $dataHex = ($bytes | ForEach-Object { "{0:X2}" -f $_ }) -join "" return "04$lenHex$dataHex" } function Get-LocalIPv4ForDestination { param([Parameter(Mandatory=$true)][string]$DestinationHost,[int]$DestinationPort = 443) try { $ips = [System.Net.Dns]::GetHostAddresses($DestinationHost) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } if (-not $ips -or $ips.Count -lt 1) { return $null } $destIp = $ips[0] $client = New-Object System.Net.Sockets.UdpClient $client.Connect($destIp, $DestinationPort) $localEp = [System.Net.IPEndPoint]$client.Client.LocalEndPoint $client.Close() return $localEp.Address.IPAddressToString } catch { return $null } } # --------------------------- # VC++ Runtime support (portable copy) # --------------------------- function Test-VcRuntimePresent { param([string]$SearchDir) $need = @("VCRUNTIME140.dll","VCRUNTIME140_1.dll","MSVCP140.dll","ucrtbase.dll") foreach ($d in $need) { $ok = $false if (Test-Path -LiteralPath (Join-Path $env:windir "System32\$d")) { $ok = $true } elseif ($SearchDir -and (Test-Path -LiteralPath (Join-Path $SearchDir $d))) { $ok = $true } if (-not $ok) { return $false } } return $true } function Ensure-VcRuntimeLocal { param( [Parameter(Mandatory=$true)][string]$SourceDir, [Parameter(Mandatory=$true)][string]$TargetDir ) Ensure-Dir $TargetDir if (Test-VcRuntimePresent -SearchDir $TargetDir) { LogDbg "[VCRUNTIME] Present (System32 or TargetDir) -> no action needed." return } if ([string]::IsNullOrWhiteSpace($SourceDir)) { throw @" OpenSSL benötigt VC++ Runtime DLLs (x64), diese fehlen auf dem System. Bitte installiere 'Microsoft Visual C++ Redistributable 2015-2022 (x64)' ODER setze -VcRuntimeSourceDir auf ein Verzeichnis mit: VCRUNTIME140.dll, VCRUNTIME140_1.dll, MSVCP140.dll, ucrtbase.dll "@ } if (-not (Test-Path -LiteralPath $SourceDir)) { throw "[VCRUNTIME] SourceDir not found: $SourceDir" } $need = @("VCRUNTIME140.dll","VCRUNTIME140_1.dll","MSVCP140.dll","ucrtbase.dll") Log "[VCRUNTIME] Missing -> copying VC++ runtime DLLs locally..." LogDbg ("[VCRUNTIME] SourceDir: {0}" -f $SourceDir) LogDbg ("[VCRUNTIME] TargetDir: {0}" -f $TargetDir) foreach ($d in $need) { $src = Join-Path $SourceDir $d $dst = Join-Path $TargetDir $d if (-not (Test-Path -LiteralPath $src)) { throw "[VCRUNTIME] Missing in SourceDir: $src" } Copy-Item -LiteralPath $src -Destination $dst -Force } if ($DebugMode) { Log "[VCRUNTIME] Target directory content (runtime files):" foreach ($d in $need) { $p = Join-Path $TargetDir $d if (Test-Path -LiteralPath $p) { $fi = Get-Item -LiteralPath $p Log ("[VCRUNTIME] {0} {1} bytes {2}" -f $fi.Name, $fi.Length, $fi.LastWriteTime) } } } if (-not (Test-VcRuntimePresent -SearchDir $TargetDir)) { throw "[VCRUNTIME] Copy attempted but runtime still not detected." } Log "[VCRUNTIME] Runtime DLLs copied." } # --------------------------- # OpenSSL UNC cache (minimal) # --------------------------- function Ensure-OpenSSLLocal { param( [Parameter(Mandatory=$true)][string]$SourceOpenSslExe, [Parameter(Mandatory=$true)][string]$LocalBinDir, [switch]$Force ) if (-not (Test-Path -LiteralPath $SourceOpenSslExe)) { throw "OpenSSL not found: $SourceOpenSslExe" } if (-not (Test-IsUncPath -path $SourceOpenSslExe)) { LogDbg "[OpenSSL] Source is local path -> no cache needed." return $SourceOpenSslExe } Ensure-Dir $LocalBinDir $srcDir = Split-Path -Parent $SourceOpenSslExe $dstExe = Join-Path $LocalBinDir "openssl.exe" $stampFile = Join-Path $LocalBinDir "_source_hash.txt" $srcHash = Get-FileSHA256 $SourceOpenSslExe $dstHash = Get-FileSHA256 $dstExe $lastSrcHash = $null if (Test-Path -LiteralPath $stampFile) { $lastSrcHash = (Get-Content -LiteralPath $stampFile -Raw).Trim() } $needCopy = $Force.IsPresent if (-not $needCopy) { if (-not (Test-Path -LiteralPath $dstExe)) { $needCopy = $true } elseif (-not (Test-OpenSslDllsPresent -binDir $LocalBinDir)) { $needCopy = $true } elseif ($srcHash -and $lastSrcHash -and ($srcHash -ne $lastSrcHash)) { $needCopy = $true } elseif ($srcHash -and $dstHash -and ($srcHash -ne $dstHash)) { $needCopy = $true } } if ($needCopy) { Log "[OpenSSL] UNC detected -> caching OpenSSL minimal set locally (openssl.exe + libcrypto/libssl)..." LogDbg ("[OpenSSL] SourceDir: {0}" -f $srcDir) LogDbg ("[OpenSSL] TargetDir: {0}" -f $LocalBinDir) Copy-Item -LiteralPath $SourceOpenSslExe -Destination $dstExe -Force $crypto = @(Get-ChildItem -LiteralPath $srcDir -Filter "libcrypto*.dll" -File -ErrorAction SilentlyContinue) $ssl = @(Get-ChildItem -LiteralPath $srcDir -Filter "libssl*.dll" -File -ErrorAction SilentlyContinue) if (-not $crypto -or $crypto.Count -lt 1) { throw "[OpenSSL] Source is missing libcrypto*.dll in $srcDir" } if (-not $ssl -or $ssl.Count -lt 1) { throw "[OpenSSL] Source is missing libssl*.dll in $srcDir" } foreach ($f in $crypto) { Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $LocalBinDir $f.Name) -Force } foreach ($f in $ssl) { Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $LocalBinDir $f.Name) -Force } if ($DebugMode) { Log "[OpenSSL] Target directory content after copy:" Get-ChildItem -LiteralPath $LocalBinDir -File | Select-Object Name,Length,LastWriteTime | ForEach-Object { Log ("[OpenSSL] {0} {1} bytes {2}" -f $_.Name, $_.Length, $_.LastWriteTime) } } if (-not (Test-Path -LiteralPath $dstExe)) { throw "[OpenSSL] Cache copy failed: openssl.exe missing." } if (-not (Test-OpenSslDllsPresent -binDir $LocalBinDir)) { throw "[OpenSSL] Cache copy failed: libcrypto/libssl missing." } if ($srcHash) { Set-Content -LiteralPath $stampFile -Value $srcHash -Encoding ASCII } Log "[OpenSSL] Local cache updated." } else { LogDbg "[OpenSSL] Local cache OK -> no copy needed." } return $dstExe } # --------------------------- # Execute EXE # --------------------------- function Invoke-Exe { param( [Parameter(Mandatory=$true)][string]$Exe, [Parameter(Mandatory=$true)][string[]]$Args, [string]$Label = "EXEC", [switch]$AllowNonZeroExit ) $outFile = Join-Path $env:TEMP ("ps_out_" + [Guid]::NewGuid().ToString("N") + ".txt") $errFile = Join-Path $env:TEMP ("ps_err_" + [Guid]::NewGuid().ToString("N") + ".txt") $wd = Split-Path -Parent $Exe try { if ($DebugMode) { Log ("{0}: {1} {2}" -f $Label, $Exe, ($Args -join " ")) LogDbg ("{0}: WorkingDirectory={1}" -f $Label, $wd) } $p = Start-Process -FilePath $Exe ` -ArgumentList $Args ` -NoNewWindow ` -PassThru ` -Wait ` -WorkingDirectory $wd ` -RedirectStandardOutput $outFile ` -RedirectStandardError $errFile $stdout = "" $stderr = "" if (Test-Path -LiteralPath $outFile) { $stdout = (Get-Content -LiteralPath $outFile -Raw) } if (Test-Path -LiteralPath $errFile) { $stderr = (Get-Content -LiteralPath $errFile -Raw) } if (-not $AllowNonZeroExit -and $p.ExitCode -ne 0) { throw ("$Label failed (ExitCode={0})`nSTDERR:`n{1}`nSTDOUT:`n{2}" -f $p.ExitCode, $stderr, $stdout) } return [pscustomobject]@{ ExitCode = $p.ExitCode; StdOut = $stdout; StdErr = $stderr } } finally { Remove-Item -LiteralPath $outFile -ErrorAction SilentlyContinue Remove-Item -LiteralPath $errFile -ErrorAction SilentlyContinue } } # --------------------------- # PFX helpers # --------------------------- function New-RandomSecurePassword { param([int]$Length = 32) $bytes = New-Object byte[] ($Length) [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) $plain = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+','-').Replace('/','_') return (ConvertTo-SecureString -String $plain -AsPlainText -Force) } function Save-EncryptedSecureStringDPAPI { param([Parameter(Mandatory=$true)][securestring]$Secret,[Parameter(Mandatory=$true)][string]$Path) Ensure-Dir (Split-Path -Parent $Path) $enc = $Secret | ConvertFrom-SecureString [IO.File]::WriteAllText($Path, $enc, (New-Object System.Text.UTF8Encoding($false))) } # --------------------------- # Cert search / filters # --------------------------- function Get-CustomOidAsciiValue { param( [Parameter(Mandatory=$true)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert, [Parameter(Mandatory=$true)][string]$Oid ) try { $ext = $Cert.Extensions | Where-Object { $_.Oid.Value -eq $Oid } | Select-Object -First 1 if (-not $ext) { return $null } $raw = $ext.RawData if (-not $raw -or $raw.Length -lt 2) { return $null } $i = 0 if ($raw[$i] -ne 0x04) { return [Text.Encoding]::ASCII.GetString($raw) } $i++ $lenByte = $raw[$i]; $i++ $len = 0 if ($lenByte -lt 0x80) { $len = $lenByte } elseif ($lenByte -eq 0x81) { $len = $raw[$i]; $i++ } elseif ($lenByte -eq 0x82) { $len = ($raw[$i] -shl 8) + $raw[$i+1]; $i += 2 } else { return $null } if ($i + $len -gt $raw.Length) { return $null } $data = $raw[$i..($i + $len - 1)] return ([Text.Encoding]::ASCII.GetString($data)).Trim() } catch { return $null } } function Find-ExistingClientAuthCert { param( [Parameter(Mandatory=$true)][string]$CnValue, [Parameter(Mandatory=$true)][string]$IssuerCN, [string]$RequireWebAdmTypeValue ) $now = Get-Date $store = "Cert:\LocalMachine\My" # EKU OID für ClientAuth $ekuClientAuthOid = "1.3.6.1.5.5.7.3.2" $all = Get-ChildItem -Path $store -ErrorAction SilentlyContinue $candidates = foreach ($c in $all) { try { if (-not $c.HasPrivateKey) { continue } if ($c.NotAfter -le $now) { continue } # Subject CN / Issuer CN robust extrahieren (sprach-/formatunabhängig) $subCn = $c.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false) $issCn = $c.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $true) if ($subCn -ne $CnValue) { continue } if ($issCn -ne $IssuerCN) { continue } # EKU per OID prüfen (nicht FriendlyName!) $ekuExt = $c.Extensions | Where-Object { $_.Oid.Value -eq "2.5.29.37" } | Select-Object -First 1 if (-not $ekuExt) { continue } $eku = New-Object System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension($ekuExt, $ekuExt.Critical) $hasClientAuth = $false foreach ($oid in $eku.EnhancedKeyUsages) { if ($oid.Value -eq $ekuClientAuthOid) { $hasClientAuth = $true; break } } if (-not $hasClientAuth) { continue } # Optional: WebADMType OID match if ($RequireWebAdmTypeValue) { $v = Get-CustomOidAsciiValue -Cert $c -Oid "1.3.6.1.4.1.34617.3.1.1" if (-not $v -or $v -ne $RequireWebAdmTypeValue) { continue } } $c } catch { continue } } $best = $candidates | Sort-Object NotAfter -Descending | Select-Object -First 1 return $best } # --------------------------- # WebADM JSON-RPC # --------------------------- function New-BasicAuthHeaderValue { param([Parameter(Mandatory=$true)][string]$UserDn,[Parameter(Mandatory=$true)][string]$Password) $raw = "$UserDn`:$Password" $b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($raw)) return "Basic $b64" } function Invoke-WebAdmCertAutoConfirm { param([Parameter(Mandatory=$true)][int]$ExpiresMinutes,[string]$App,[string]$Addresses) $p = '"expires":' + [string]$ExpiresMinutes if (-not [string]::IsNullOrWhiteSpace($App)) { $p += ',"application":"' + ($App -replace '"','') + '"' } if (-not [string]::IsNullOrWhiteSpace($Addresses)) { $addrEsc = $Addresses -replace "\\","\\" $addrEsc = $addrEsc -replace '"','\"' $p += ',"addresses":"' + $addrEsc + '"' } $json = '{' + '"jsonrpc":"2.0",' + '"method":"Cert_Auto_Confirm",' + '"params":{' + $p + '},' + '"id":1' + '}' $headers = @{ Authorization = (New-BasicAuthHeaderValue -UserDn $AuthUserDn -Password $Credential.GetNetworkCredential().Password) Accept = "application/json" } Log ("WebADM: Cert_Auto_Confirm expires={0} app={1} addrs={2}" -f $ExpiresMinutes, $(if($App){$App}else{"(none)"}), $(if($Addresses){$Addresses}else{"(none)"})) if ($DebugMode) { Log ("HTTP: JSON FULL: {0}" -f $json) } $resp = Invoke-WebRequest ` -Method Post ` -Uri $WebAdmUrl ` -Headers $headers ` -ContentType "application/json" ` -Body $json ` -TimeoutSec $TimeoutSec ` -UseBasicParsing $obj = $resp.Content | ConvertFrom-Json if ($obj -and $obj.PSObject.Properties.Match("error").Count -gt 0 -and $obj.error) { throw ("WebADM error (Cert_Auto_Confirm): {0}" -f (($obj.error | ConvertTo-Json -Compress))) } return [bool]$obj.result } function Invoke-WebAdmSignCsr { param([Parameter(Mandatory=$true)][string]$CsrPem) Log "Submitting CSR to WebADM (Sign_Certificate_Request)..." $csrEscaped = $CsrPem -replace "\r","" $csrEscaped = $csrEscaped -replace "\\","\\" $csrEscaped = $csrEscaped -replace '"','\"' $csrEscaped = $csrEscaped -replace "`n","\n" $json = '{' + '"jsonrpc":"2.0",' + '"method":"Sign_Certificate_Request",' + '"params":{' + '"type":"' + ($CertType -replace '"','') + '",' + '"application":"' + ($Application -replace '"','') + '",' + '"request":"' + $csrEscaped + '",' + '"expires":' + [string]$ValidityDays + '},' + '"id":1' + '}' $headers = @{ Authorization = (New-BasicAuthHeaderValue -UserDn $AuthUserDn -Password $Credential.GetNetworkCredential().Password) Accept = "application/json" } LogDbg ("HTTP: payload bytes={0}" -f ([Text.Encoding]::UTF8.GetByteCount($json))) if ($DebugMode) { Log ("HTTP: JSON FULL: {0}" -f $json) } $resp = Invoke-WebRequest ` -Method Post ` -Uri $WebAdmUrl ` -Headers $headers ` -ContentType "application/json" ` -Body $json ` -TimeoutSec $TimeoutSec ` -UseBasicParsing $obj = $resp.Content | ConvertFrom-Json if ($obj -and $obj.PSObject.Properties.Match("error").Count -gt 0 -and $obj.error) { throw ("WebADM error (Sign_Certificate_Request): {0}" -f (($obj.error | ConvertTo-Json -Compress))) } if (-not ($obj.PSObject.Properties.Match("result").Count -gt 0)) { throw ("Unexpected WebADM response (no result). Raw:`n{0}" -f $resp.Content) } return [string]$obj.result } # --------------------------- # MAIN # --------------------------- Ensure-Dir $OutputDir Ensure-Dir (Split-Path -Parent $PfxPasswordFile) Ensure-Dir $OpenSSLLocalDir Ensure-Dir $VcRuntimeLocalDir # Tunnel selection $IncludeTunnelExtension = $false if ($PSBoundParameters.ContainsKey("TunnelExtensionValue") -and -not [string]::IsNullOrWhiteSpace($TunnelExtensionValue)) { $IncludeTunnelExtension = $true LogDbg ("Tunnel: using explicit TunnelExtensionValue: {0}" -f $TunnelExtensionValue) } elseif ($PSBoundParameters.ContainsKey("Vlan")) { $IncludeTunnelExtension = $true $TunnelExtensionValue = "Tunnel-Type=13,Tunnel-Medium-Type=6,Tunnel-Private-Group-ID=$Vlan" Log ("Tunnel: built from VLAN {0}: {1}" -f $Vlan, $TunnelExtensionValue) } # Renewal NOOP check (only meaningful when installing) if ($AutoInstallToLocalMachine -and -not $ForceEnroll) { $existing = Find-ExistingClientAuthCert -CnValue $CN -IssuerCN $IssuerCN -RequireWebAdmTypeValue $RequireWebAdmTypeValue if ($existing) { $now = Get-Date $threshold = $now.AddDays($RenewBeforeDays) if ($existing.NotAfter -gt $threshold) { Log ("NOOP: Existing cert still valid beyond {0} days -> no renewal." -f $RenewBeforeDays) Log ("Existing: Subject={0}" -f $existing.Subject) Log ("Existing: Issuer={0}" -f $existing.Issuer) Log ("Existing: Thumbprint={0}" -f $existing.Thumbprint) Log ("Existing: NotAfter={0}" -f $existing.NotAfter) return } Log ("Renew needed: existing cert expires {0} (<= {1} days remaining)." -f $existing.NotAfter, $RenewBeforeDays) } else { Log "No matching existing cert found -> enrolling new." } } elseif ($ForceEnroll) { Log "FORCE: -ForceEnroll set -> issuing a new certificate regardless of existing one." } else { Log "NOTE: AutoInstall not enabled -> renewal check skipped." } # 1) Ensure OpenSSL local (cache if UNC) $ResolvedOpenSslPath = Ensure-OpenSSLLocal -SourceOpenSslExe $OpenSslPath -LocalBinDir $OpenSSLLocalDir -Force:$ForceOpenSSLUpdate Log ("OpenSSL (source): {0}" -f $OpenSslPath) Log ("OpenSSL (exec) : {0}" -f $ResolvedOpenSslPath) Log ("OutputDir : {0}" -f $OutputDir) # 2) Ensure VC++ runtime present (copy if needed) if (-not (Test-VcRuntimePresent -SearchDir $VcRuntimeLocalDir)) { Ensure-VcRuntimeLocal -SourceDir $VcRuntimeSourceDir -TargetDir $VcRuntimeLocalDir } else { LogDbg "[VCRUNTIME] Present -> OK." } # OpenSSL version (best effort) try { $ver = Invoke-Exe -Exe $ResolvedOpenSslPath -Args @("version") -Label "OPENSSL" if ($ver.StdOut) { Log ("OpenSSL version: {0}" -f ($ver.StdOut.Trim())) } } catch { Log "OpenSSL version check failed (continuing)." LogDbg $_.Exception.Message } # Subject $subject = "/C=$C/ST=$ST/L=$L/O=$O/emailAddress=$Email" if (-not $NoDescriptionRdn) { $subject += "/description=$CertType" } $subject += "/CN=$CN" Log ("Subject: {0}" -f $subject) # Filenames $stamp = (Get-Date).ToString("yyyyMMdd_HHmmss") $base = "{0}_{1}_{2}" -f $CertType, $CN, $stamp $cnfPath = Join-Path $OutputDir ("openssl_{0}.cnf" -f $base) $keyPath = Join-Path $OutputDir ("key_{0}.pem" -f $base) $csrPath = Join-Path $OutputDir ("req_{0}.csr" -f $base) $certPath = Join-Path $OutputDir ("cert_{0}.pem" -f $base) $pfxPath = Join-Path $OutputDir ("cert_{0}.pfx" -f $base) # Build CNF $cnf = New-Object System.Collections.Generic.List[string] $cnf.Add("[ req ]") $cnf.Add("distinguished_name = dn") $cnf.Add("prompt = no") $cnf.Add("req_extensions = v3_req") $cnf.Add("utf8 = yes") $cnf.Add("") $cnf.Add("[ dn ]") $cnf.Add("C = $C") $cnf.Add("ST = $ST") $cnf.Add("L = $L") $cnf.Add("O = $O") #$cnf.Add("emailAddress = $Email") if (-not $NoDescriptionRdn) { $cnf.Add("description = $CertType") } $cnf.Add("CN = $CN") $cnf.Add("") $cnf.Add("[ v3_req ]") $cnf.Add("keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment") $cnf.Add("extendedKeyUsage = clientAuth") $cnf.Add("") if (-not $DisableWebAdmTypeExtension) { $typeDer = Convert-ToDerOctetStringHex -Value $WebAdmTypeValue $cnf.Add("1.3.6.1.4.1.34617.3.1.1 = DER:$typeDer") LogDbg ("WebADMType OID enabled: {0}" -f $WebAdmTypeValue) LogDbg ("WebADMType DER: {0}" -f $typeDer) } else { LogDbg "WebADMType OID disabled" } if ($IncludeTunnelExtension) { if ([string]::IsNullOrWhiteSpace($TunnelExtensionValue)) { throw "Tunnel enabled but TunnelExtensionValue is empty (use -Vlan or -TunnelExtensionValue)." } $tunDer = Convert-ToDerOctetStringHex -Value $TunnelExtensionValue $cnf.Add("1.3.6.1.4.1.34617.3.1.2 = DER:$tunDer") LogDbg ("Tunnel OID enabled: {0}" -f $TunnelExtensionValue) LogDbg ("Tunnel DER: {0}" -f $tunDer) } else { LogDbg "Tunnel OID disabled" } Log "OpenSSL config writing..." [IO.File]::WriteAllLines($cnfPath, $cnf.ToArray(), (New-Object System.Text.UTF8Encoding($false))) Log ("OpenSSL config written: {0}" -f $cnfPath) if ($DebugMode) { Log "OpenSSL CNF preview:" foreach ($line in $cnf) { Log ("[CNF] " + $line) } } # Generate key + CSR Log "Generating key + CSR..." $reqArgs = @( "req","-new", "-newkey","rsa:2048", "-nodes", "-keyout",$keyPath, "-out",$csrPath, "-config",$cnfPath, "-reqexts","v3_req" ) Invoke-Exe -Exe $ResolvedOpenSslPath -Args $reqArgs -Label "EXEC" | Out-Null if (-not (Test-Path -LiteralPath $csrPath)) { throw "CSR file missing: $csrPath" } if (-not (Test-Path -LiteralPath $keyPath)) { throw "Key file missing: $keyPath" } Log ("CSR created: {0}" -f $csrPath) # AutoConfirm $disableAfter = $false if ($PSBoundParameters.ContainsKey("DisableAutoConfirmAfter")) { $disableAfter = [bool]$DisableAutoConfirmAfter } $appForAuto = $null $addrForAuto = $null if ($EnableAutoConfirm) { $appForAuto = $AutoConfirmApplication if ([string]::IsNullOrWhiteSpace($appForAuto)) { $appForAuto = $Application } $addrForAuto = $AutoConfirmAddresses if ([string]::IsNullOrWhiteSpace($addrForAuto)) { $u = [Uri]$WebAdmUrl $destPort = $u.Port if ($destPort -le 0) { if ($u.Scheme -eq "https") { $destPort = 443 } else { $destPort = 80 } } $myIp = Get-LocalIPv4ForDestination -DestinationHost $u.Host -DestinationPort $destPort if ($myIp) { $addrForAuto = $myIp Log ("AutoConfirm: addresses not set -> using local host IP: {0}" -f $addrForAuto) } else { Log "AutoConfirm: could not determine local IP; leaving addresses empty (less secure)." $addrForAuto = $null } } Invoke-WebAdmCertAutoConfirm -ExpiresMinutes $AutoConfirmMinutes -App $appForAuto -Addresses $addrForAuto | Out-Null } # Sign CSR $csrPem = Read-FileRaw $csrPath try { $certPem = Invoke-WebAdmSignCsr -CsrPem $csrPem } finally { if ($EnableAutoConfirm -and $disableAfter) { Invoke-WebAdmCertAutoConfirm -ExpiresMinutes 0 -App $appForAuto -Addresses $addrForAuto | Out-Null Log "WebADM: AutoConfirm disabled (expires=0)." } } [IO.File]::WriteAllText($certPath, $certPem, (New-Object System.Text.UTF8Encoding($false))) Log ("Certificate saved: {0}" -f $certPath) # Install if ($AutoInstallToLocalMachine) { $effectivePfxPwd = $null if ($PSBoundParameters.ContainsKey("PfxPassword") -and $PfxPassword) { $effectivePfxPwd = $PfxPassword } elseif ($AutoGeneratePfxPassword -or $PersistEncryptedPfxPassword -or $AutoInstallToLocalMachine) { $effectivePfxPwd = New-RandomSecurePassword -Length 32 LogDbg "PFX password auto-generated (not logged)." if ($PersistEncryptedPfxPassword) { Save-EncryptedSecureStringDPAPI -Secret $effectivePfxPwd -Path $PfxPasswordFile Log ("Encrypted PFX password saved (DPAPI): {0}" -f $PfxPasswordFile) } } else { throw "PFX password missing. Provide -PfxPassword or use -AutoGeneratePfxPassword." } $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($effectivePfxPwd) $plain = "" try { $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } Log "Creating PFX (PKCS#12)..." $p12Args = @( "pkcs12","-export", "-inkey",$keyPath, "-in",$certPath, "-out",$pfxPath, "-passout",("pass:$plain") ) Invoke-Exe -Exe $ResolvedOpenSslPath -Args $p12Args -Label "EXEC" | Out-Null if (-not (Test-Path -LiteralPath $pfxPath)) { throw "PFX file missing: $pfxPath" } Log ("PFX saved: {0}" -f $pfxPath) Log "Importing PFX to Cert:\LocalMachine\My ..." Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\LocalMachine\My" -Password $effectivePfxPwd | Out-Null Log "PFX imported to LocalMachine\My." } Log "DONE." $extraPfx = "" if (Test-Path -LiteralPath $pfxPath) { $extraPfx = "`n PFX : $pfxPath" } Log ("Artifacts:`n KEY : {0}`n CSR : {1}`n CERT: {2}{3}" -f $keyPath, $csrPath, $certPath, $extraPfx)