Files
SGU-CredentialProvider/scripts/Install-CredentialProvider.ps1
T

240 lines
9.9 KiB
PowerShell

[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, 60)]
[int]$TimeoutSeconds = 35,
[switch]$DoNotSetAsDefaultCredentialProvider,
[switch]$InstallDotNetRuntime,
[string]$DotNetRuntimeInstallerPath
)
$ErrorActionPreference = 'Stop'
$providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
$installRoot = 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"
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
$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."
}
}
$resolvedPublishPath = (Resolve-Path -LiteralPath $PublishPath).Path.TrimEnd('\')
$packageManifest = Get-ChildItem -LiteralPath $resolvedPublishPath -Recurse -File |
Sort-Object FullName |
ForEach-Object {
$relativePath = $_.FullName.Substring($resolvedPublishPath.Length).TrimStart('\')
'{0}={1}' -f $relativePath, (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash
}
$manifestBytes = [Text.Encoding]::UTF8.GetBytes(($packageManifest -join "`n"))
$sha256 = [Security.Cryptography.SHA256]::Create()
try {
$packageHash = -join ($sha256.ComputeHash($manifestBytes) | ForEach-Object { $_.ToString('x2') })
}
finally {
$sha256.Dispose()
}
$versionId = $packageHash.Substring(0, 16)
$installPath = Join-Path $installRoot "versions\$versionId"
$completeMarker = Join-Path $installPath '.complete'
if ((Test-Path -LiteralPath $installPath) -and -not (Test-Path -LiteralPath $completeMarker)) {
$installPath = '{0}-{1}' -f $installPath, ([Guid]::NewGuid().ToString('N').Substring(0, 8))
$completeMarker = Join-Path $installPath '.complete'
}
$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')) {
if (-not (Test-Path -LiteralPath $completeMarker)) {
New-Item -ItemType Directory -Path $installPath -Force | Out-Null
Copy-Item -Path (Join-Path $resolvedPublishPath '*') -Destination $installPath -Recurse -Force
[IO.File]::WriteAllText($completeMarker, $packageHash, [Text.UTF8Encoding]::new($false))
}
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 'Universidad La Salle · Acceso SGU'
if (-not $DoNotSetAsDefaultCredentialProvider) {
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
}
New-ItemProperty -Path $defaultProviderPolicyPath `
-Name DefaultCredentialProvider `
-Value $providerClassId `
-PropertyType String `
-Force | Out-Null
}
# Do not leave a signed-out SGU identity exposed as a persistent user tile.
# The Microsoft password provider remains registered and supplies Other user.
if (-not (Test-Path -LiteralPath $interactiveLogonPolicyPath)) {
New-Item -Path $interactiveLogonPolicyPath -Force | Out-Null
}
New-ItemProperty -Path $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName `
-Value 1 `
-PropertyType DWord `
-Force | Out-Null
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
}
New-ItemProperty -Path $defaultProviderPolicyPath `
-Name EnumerateLocalUsers `
-Value 0 `
-PropertyType DWord `
-Force | Out-Null
}
$defaultProviderConfigured = $false
try {
$defaultProviderConfigured = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath `
-Name DefaultCredentialProvider `
-ErrorAction Stop) -eq $providerClassId
}
catch {
# An explicitly opted-out installation has no default-provider policy.
}
[pscustomobject]@{
ProviderClassId = $providerClassId
InstallPath = $installPath
SettingsPath = $settingsPath
Registered = Test-Path -LiteralPath $providerRegistryPath
DefaultProviderConfigured = $defaultProviderConfigured
LastSignedInUserHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName) -eq 1
LocalUserEnumerationDisabled = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath `
-Name EnumerateLocalUsers) -eq 0
SystemPasswordProviderPreserved = $true
}