$ErrorActionPreference = 'Stop' $certificateDirectory = Join-Path $env:LOCALAPPDATA 'SovereignAI\certs' $rootPem = Join-Path $certificateDirectory 'sovereignai-local-root.pem' $rootDer = Join-Path $certificateDirectory 'sovereignai-local-root.cer' $rootKey = Join-Path $certificateDirectory 'sovereignai-local-root-key.pem' $leafPem = Join-Path $certificateDirectory 'localhost-cert.pem' $leafKey = Join-Path $certificateDirectory 'localhost-key.pem' $requestFile = Join-Path $certificateDirectory 'localhost.csr' $extensionsFile = Join-Path $certificateDirectory 'localhost-extensions.cnf' $serialFile = Join-Path $certificateDirectory 'sovereignai-local-root.srl' $opensslCandidates = @( (Get-Command openssl.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -First 1), (Join-Path $env:ProgramFiles 'Git\usr\bin\openssl.exe'), (Join-Path ${env:ProgramFiles(x86)} 'Git\usr\bin\openssl.exe') ) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) } if (-not $opensslCandidates) { throw 'OpenSSL not found. Install Git for Windows or add openssl.exe to PATH.' } $openssl = [string]($opensslCandidates | Select-Object -First 1) $persistentFiles = @($rootPem, $rootDer, $leafPem, $leafKey) $transientFiles = @($rootKey, $requestFile, $extensionsFile, $serialFile) $existingPersistentFiles = @($persistentFiles | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf }) if ($existingPersistentFiles.Count -gt 0) { if ($existingPersistentFiles.Count -ne $persistentFiles.Count) { throw "A partial local TLS setup exists in $certificateDirectory. Run .\scripts\remove_local_tls.ps1 before retrying." } $existingRoot = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($rootDer) $trustedRoot = Get-ChildItem Cert:\CurrentUser\Root | Where-Object Thumbprint -eq $existingRoot.Thumbprint if (-not $trustedRoot) { throw 'The local development root is not trusted. Run .\scripts\remove_local_tls.ps1, then setup again.' } & "$openssl" verify -CAfile $rootPem $leafPem if ($LASTEXITCODE -ne 0) { throw 'The stored localhost certificate is expired or invalid. Run .\scripts\remove_local_tls.ps1, then setup again.' } Write-Host "Local HTTPS certificate is already installed. Root thumbprint: $($existingRoot.Thumbprint)" Write-Host 'Use .\start-api-https.ps1 to run the API on https://localhost:8443.' exit 0 } New-Item -ItemType Directory -Force -Path $certificateDirectory | Out-Null $createdFiles = [System.Collections.Generic.List[string]]::new() $certificateAdded = $false $rootCertificate = $null try { & "$openssl" req -quiet -x509 -newkey rsa:3072 -sha256 -nodes -days 3650 ` -keyout $rootKey -out $rootPem ` -subj '/CN=SovereignAI Local Development Root' ` -addext 'basicConstraints=critical,CA:TRUE,pathlen:0' ` -addext 'keyUsage=critical,keyCertSign,cRLSign' ` -addext 'subjectKeyIdentifier=hash' if ($LASTEXITCODE -ne 0) { throw "OpenSSL local-root generation failed ($LASTEXITCODE)." } $createdFiles.Add($rootKey) $createdFiles.Add($rootPem) $currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value foreach ($privateKey in @($rootKey, $leafKey)) { if (Test-Path -LiteralPath $privateKey -PathType Leaf) { & icacls.exe $privateKey /inheritance:r /grant:r "*$currentSid`:F" '*S-1-5-18:F' | Out-Null if ($LASTEXITCODE -ne 0) { throw "Could not restrict private-key ACL for $privateKey ($LASTEXITCODE)." } } } & "$openssl" req -quiet -new -newkey rsa:2048 -sha256 -nodes ` -keyout $leafKey -out $requestFile -subj '/CN=localhost' if ($LASTEXITCODE -ne 0) { throw "OpenSSL localhost-key/CSR generation failed ($LASTEXITCODE)." } $createdFiles.Add($leafKey) $createdFiles.Add($requestFile) & icacls.exe $leafKey /inheritance:r /grant:r "*$currentSid`:F" '*S-1-5-18:F' | Out-Null if ($LASTEXITCODE -ne 0) { throw "Could not restrict localhost private-key ACL ($LASTEXITCODE)." } $extensions = @' basicConstraints=critical,CA:FALSE keyUsage=critical,digitalSignature,keyEncipherment extendedKeyUsage=serverAuth subjectAltName=@alt_names subjectKeyIdentifier=hash authorityKeyIdentifier=keyid,issuer [alt_names] DNS.1=localhost IP.1=127.0.0.1 '@ Set-Content -LiteralPath $extensionsFile -Value $extensions -Encoding ascii $createdFiles.Add($extensionsFile) & "$openssl" x509 -req -in $requestFile -CA $rootPem -CAkey $rootKey ` -CAcreateserial -out $leafPem -days 825 -sha256 -extfile $extensionsFile if ($LASTEXITCODE -ne 0) { throw "OpenSSL localhost certificate signing failed ($LASTEXITCODE)." } $createdFiles.Add($leafPem) if (Test-Path -LiteralPath $serialFile -PathType Leaf) { $createdFiles.Add($serialFile) } & "$openssl" x509 -in $rootPem -outform DER -out $rootDer if ($LASTEXITCODE -ne 0) { throw "OpenSSL root DER export failed ($LASTEXITCODE)." } $createdFiles.Add($rootDer) & "$openssl" verify -CAfile $rootPem $leafPem if ($LASTEXITCODE -ne 0) { throw 'The generated localhost certificate failed local-chain verification.' } $rootCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($rootDer) $leafCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($leafPem) $rootConstraints = & "$openssl" x509 -in $rootPem -noout -ext basicConstraints 2>&1 | Out-String if ($LASTEXITCODE -ne 0 -or $rootConstraints -notmatch 'CA:TRUE') { throw 'The local root certificate is not marked as a certificate authority.' } $leafSans = ($leafCertificate.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.17' }).Format($false) if ($leafSans -notmatch 'localhost' -or $leafSans -notmatch '127\.0\.0\.1') { throw "Generated localhost certificate has unexpected SANs: $leafSans" } Import-Certificate -FilePath $rootDer -CertStoreLocation Cert:\CurrentUser\Root | Out-Null $certificateAdded = $true $trustedRoot = Get-ChildItem Cert:\CurrentUser\Root | Where-Object Thumbprint -eq $rootCertificate.Thumbprint if (-not $trustedRoot) { throw 'Windows did not retain the development root in CurrentUser\Root.' } Remove-Item -LiteralPath $rootKey -Force [void]$createdFiles.Remove($rootKey) if (Test-Path -LiteralPath $requestFile -PathType Leaf) { Remove-Item -LiteralPath $requestFile -Force } [void]$createdFiles.Remove($requestFile) if (Test-Path -LiteralPath $extensionsFile -PathType Leaf) { Remove-Item -LiteralPath $extensionsFile -Force } [void]$createdFiles.Remove($extensionsFile) if (Test-Path -LiteralPath $serialFile -PathType Leaf) { Remove-Item -LiteralPath $serialFile -Force } [void]$createdFiles.Remove($serialFile) Write-Host 'Installed a local development CA and localhost-only HTTPS certificate for this Windows user.' Write-Host "Root SHA-1 thumbprint: $($rootCertificate.Thumbprint)" Write-Host "Root public certificate: $rootDer" Write-Host "Server certificate: $leafPem" Write-Host "Server private key (current user and SYSTEM only): $leafKey" Write-Host 'The root private key was deleted after signing the localhost certificate.' Write-Host 'Run .\start-api-https.ps1 to launch the optional HTTPS API on port 8443.' Write-Host 'The existing HTTP API on port 8000 is unchanged. This is for localhost development only.' } catch { if ($certificateAdded -and $rootCertificate) { & certutil.exe -user -delstore Root $rootCertificate.Thumbprint | Out-Null } foreach ($file in $createdFiles) { if (Test-Path -LiteralPath $file -PathType Leaf) { Remove-Item -LiteralPath $file -Force } } throw }