Size: 6.6 KB Modified: 5/07/2026 6:09 PM
#requires -Version 7.0

param(
    [string]$PfxPath = "",
    [string]$RootCertificatePath = "",
    [string]$Password = "",
    [string[]]$DnsName = @(),
    [string[]]$IpAddress = @(),
    [int]$Years = 3,
    [switch]$TrustCurrentUser
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if ([string]::IsNullOrWhiteSpace($PfxPath)) {
    $PfxPath = Join-Path $repoRoot ".kaichat-lan.pfx"
}
if ([string]::IsNullOrWhiteSpace($RootCertificatePath)) {
    $RootCertificatePath = Join-Path $repoRoot ".kaichat-lan-root.cer"
}

$hostNames = New-Object System.Collections.Generic.List[string]
$hostNames.Add("localhost")
if (-not [string]::IsNullOrWhiteSpace($env:COMPUTERNAME)) {
    $hostNames.Add($env:COMPUTERNAME)
    $hostNames.Add("$env:COMPUTERNAME.local")
}
foreach ($name in $DnsName) {
    if (-not [string]::IsNullOrWhiteSpace($name)) {
        $hostNames.Add($name.Trim())
    }
}

$ipNames = New-Object System.Collections.Generic.List[string]
$ipNames.Add("127.0.0.1")
try {
    Get-NetIPAddress -AddressFamily IPv4 |
        Where-Object { $_.IPAddress -and $_.IPAddress -ne "127.0.0.1" -and $_.IPAddress -notlike "169.254*" } |
        ForEach-Object { $ipNames.Add($_.IPAddress) }
} catch {
    Write-Warning "Could not auto-detect local IP addresses: $($_.Exception.Message)"
}
foreach ($ip in $IpAddress) {
    if (-not [string]::IsNullOrWhiteSpace($ip)) {
        $ipNames.Add($ip.Trim())
    }
}

$uniqueHostNames = $hostNames | Sort-Object -Unique
$uniqueIpNames = $ipNames | Sort-Object -Unique
$notBefore = [DateTimeOffset]::UtcNow.AddDays(-1)
$notAfter = [DateTimeOffset]::UtcNow.AddYears([Math]::Max(1, $Years))

$rootKey = [System.Security.Cryptography.RSA]::Create(4096)
$serverKey = [System.Security.Cryptography.RSA]::Create(2048)

try {
    $rootSubject = [System.Security.Cryptography.X509Certificates.X500DistinguishedName]::new("CN=KaiChat Local Root CA")
    $rootRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
        $rootSubject,
        $rootKey,
        [System.Security.Cryptography.HashAlgorithmName]::SHA256,
        [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
    $rootRequest.CertificateExtensions.Add(
        [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true))
    $rootRequest.CertificateExtensions.Add(
        [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new(
            [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyCertSign -bor
            [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::CrlSign,
            $true))
    $rootRequest.CertificateExtensions.Add(
        [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new(
            $rootRequest.PublicKey,
            $false))
    $rootCertificate = $rootRequest.CreateSelfSigned($notBefore, $notAfter)

    $serverSubject = [System.Security.Cryptography.X509Certificates.X500DistinguishedName]::new("CN=KaiChat LAN")
    $serverRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
        $serverSubject,
        $serverKey,
        [System.Security.Cryptography.HashAlgorithmName]::SHA256,
        [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
    $serverRequest.CertificateExtensions.Add(
        [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($false, $false, 0, $true))
    $serverRequest.CertificateExtensions.Add(
        [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new(
            [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature -bor
            [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment,
            $true))

    $eku = [System.Security.Cryptography.OidCollection]::new()
    [void]$eku.Add([System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.1", "Server Authentication"))
    $serverRequest.CertificateExtensions.Add(
        [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($eku, $false))

    $san = [System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder]::new()
    foreach ($name in $uniqueHostNames) {
        $san.AddDnsName($name)
    }
    foreach ($ip in $uniqueIpNames) {
        $parsedIp = $null
        if ([System.Net.IPAddress]::TryParse($ip, [ref]$parsedIp)) {
            $san.AddIpAddress($parsedIp)
        } else {
            Write-Warning "Skipping invalid IP address '$ip'."
        }
    }
    $serverRequest.CertificateExtensions.Add($san.Build())

    $serial = New-Object byte[] 16
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    try {
        $rng.GetBytes($serial)
    } finally {
        $rng.Dispose()
    }
    $serial[0] = $serial[0] -band 0x7F

    $serverCertificate = $serverRequest.Create($rootCertificate, $notBefore, $notAfter, $serial)
    $serverCertificateWithKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey(
        $serverCertificate,
        $serverKey)

    $pfxBytes = $serverCertificateWithKey.Export(
        [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx,
        $Password)
    [System.IO.File]::WriteAllBytes($PfxPath, $pfxBytes)

    $rootBytes = $rootCertificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
    [System.IO.File]::WriteAllBytes($RootCertificatePath, $rootBytes)

    if ($TrustCurrentUser) {
        $store = [System.Security.Cryptography.X509Certificates.X509Store]::new(
            [System.Security.Cryptography.X509Certificates.StoreName]::Root,
            [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser)
        try {
            $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
            $store.Add($rootCertificate)
        } finally {
            $store.Close()
        }
    }

    Write-Host "Created KaiChat LAN HTTPS certificate:"
    Write-Host "  PFX:  $PfxPath"
    Write-Host "  Root: $RootCertificatePath"
    Write-Host ""
    Write-Host "Certificate names:"
    foreach ($name in $uniqueHostNames) { Write-Host "  DNS: $name" }
    foreach ($ip in $uniqueIpNames) { Write-Host "  IP:  $ip" }
    Write-Host ""
    Write-Host "Restart KaiChat without --urls, then open https://localhost:5201 or https://<lan-ip>:5201."
    Write-Host "Trust/import the root .cer on each browser/device that needs to accept this certificate."
} finally {
    $rootKey.Dispose()
    $serverKey.Dispose()
}
Offline