[CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$PublishPath, [Parameter(Mandatory)] [ValidatePattern('^https://')] [string]$BrokerEndpoint, [Parameter(Mandatory)] [ValidatePattern('^[0-9A-Fa-f ]{40,59}$')] [string]$ClientCertificateThumbprint, [Parameter(Mandatory)] [ValidatePattern('^[0-9A-Fa-f ]{40,59}$')] [string]$ServerCertificateThumbprint, [string]$DomainNetbios = 'LCI', [ValidateRange(2, 30)] [int]$TimeoutSeconds = 6, [switch]$InstallDotNetRuntime, [string]$DotNetRuntimeInstallerPath ) $ErrorActionPreference = 'Stop' $providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}' $installPath = Join-Path $env:ProgramFiles 'SGU\CredentialProvider' $settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json' $providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId" $classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32" $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this script from an elevated PowerShell session.' } function Test-DotNet10Runtime { $dotnetCandidates = @( (Get-Command dotnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue), (Join-Path $env:ProgramFiles 'dotnet\dotnet.exe') ) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) } | Select-Object -Unique foreach ($dotnet in $dotnetCandidates) { if (& $dotnet --list-runtimes | Select-String '^Microsoft\.NETCore\.App 10\.') { return $true } } return $false } if (-not (Test-DotNet10Runtime)) { if (-not $InstallDotNetRuntime) { throw 'Microsoft .NET 10 x64 runtime is required. Re-run with -InstallDotNetRuntime or install it first.' } if ($DotNetRuntimeInstallerPath) { if (-not (Test-Path -LiteralPath $DotNetRuntimeInstallerPath -PathType Leaf)) { throw 'DotNetRuntimeInstallerPath does not exist.' } $runtimeInstaller = Start-Process -FilePath $DotNetRuntimeInstallerPath ` -ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru if ($runtimeInstaller.ExitCode -notin @(0, 1641, 3010)) { throw "The Microsoft .NET 10 runtime installer returned $($runtimeInstaller.ExitCode)." } } else { $winget = Get-Command winget -ErrorAction SilentlyContinue if (-not $winget) { throw 'winget is unavailable. Supply the offline installer with -DotNetRuntimeInstallerPath.' } & $winget.Source install --id Microsoft.DotNet.Runtime.10 --exact --silent ` --accept-package-agreements --accept-source-agreements --disable-interactivity if ($LASTEXITCODE -ne 0) { throw 'winget could not install the Microsoft .NET 10 runtime.' } } if (-not (Test-DotNet10Runtime)) { throw 'The Microsoft .NET 10 runtime installation failed.' } } $requiredFiles = @( 'SGU.CredentialProvider.dll', 'SGU.CredentialProvider.comhost.dll', 'SGU.CredentialProvider.runtimeconfig.json', 'SGU.CredentialProvider.deps.json', 'Lithnet.CredentialProvider.dll', 'SGU.AuthBroker.Core.dll' ) foreach ($file in $requiredFiles) { if (-not (Test-Path -LiteralPath (Join-Path $PublishPath $file))) { throw "PublishPath is missing $file." } } $clientThumbprint = $ClientCertificateThumbprint -replace ' ', '' $serverThumbprint = $ServerCertificateThumbprint -replace ' ', '' if ($clientThumbprint.Length -ne 40 -or $serverThumbprint.Length -ne 40) { throw 'Certificate thumbprints must contain exactly 40 hexadecimal characters.' } $clientCertificate = Get-ChildItem Cert:\LocalMachine\My | Where-Object Thumbprint -eq $clientThumbprint | Select-Object -First 1 if (-not $clientCertificate -or -not $clientCertificate.HasPrivateKey) { throw 'The client certificate with private key is not installed in LocalMachine\My.' } if (-not $clientCertificate.Verify()) { throw 'The client certificate chain is not trusted or is outside its validity period. Import the issuing CA chain; for a self-signed lab certificate, trust its public .cer in LocalMachine\Root.' } $serverCertificate = Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\CA | Where-Object Thumbprint -eq $serverThumbprint if (-not $serverCertificate) { throw 'The broker server certificate or its issuing CA is not trusted by LocalMachine.' } if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credential Provider')) { New-Item -ItemType Directory -Path $installPath -Force | Out-Null Copy-Item -Path (Join-Path $PublishPath '*') -Destination $installPath -Recurse -Force New-Item -ItemType Directory -Path (Split-Path $settingsPath -Parent) -Force | Out-Null $settingsJson = @{ BrokerEndpoint = $BrokerEndpoint DomainNetbios = $DomainNetbios TimeoutSeconds = $TimeoutSeconds ClientCertificateThumbprint = $clientThumbprint ServerCertificateThumbprint = $serverThumbprint } | ConvertTo-Json $utf8WithoutBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($settingsPath, $settingsJson, $utf8WithoutBom) $acl = Get-Acl -LiteralPath (Split-Path $settingsPath -Parent) $acl.SetAccessRuleProtection($true, $false) $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( 'SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')) $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( 'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')) Set-Acl -LiteralPath (Split-Path $settingsPath -Parent) -AclObject $acl New-Item -Path $classRegistryPath -Force | Out-Null Set-Item -Path $classRegistryPath -Value (Join-Path $installPath 'SGU.CredentialProvider.comhost.dll') New-ItemProperty -Path $classRegistryPath -Name ThreadingModel -Value Both -PropertyType String -Force | Out-Null New-Item -Path $providerRegistryPath -Force | Out-Null Set-Item -Path $providerRegistryPath -Value 'SGU Institutional Login' } [pscustomobject]@{ ProviderClassId = $providerClassId InstallPath = $installPath SettingsPath = $settingsPath Registered = Test-Path -LiteralPath $providerRegistryPath SystemPasswordProviderPreserved = $true }