# install-online.ps1 — One-shot online installer for LemDesk on Windows. # # USAGE (paste into a PowerShell prompt — admin not required, the script # self-elevates): # # iex (irm https://install.lemorange.com/install.ps1) # # Or pin a specific version: # # & ([scriptblock]::Create((irm https://install.lemorange.com/install.ps1))) -Version 1.0.1 # # WHAT IT DOES: # 1. Self-elevates if not running as admin. # 2. Imports the embedded Lemorange Ltd code-signing cert into LocalMachine # Root + TrustedPublisher (so UAC reads "Lemorange Ltd", not "Unknown # Publisher"). # 3. Downloads the signed MSI via Invoke-WebRequest. PowerShell does NOT # attach Mark-of-the-Web to the file, so SmartScreen's reputation # check does not engage. # 4. Verifies the MSI's Authenticode signature thumbprint matches the # embedded cert before running msiexec. # 5. Runs msiexec /i with basic UI. # # WHY EMBED THE CERT: # The cert is small (~1.4 KB) and embedding it eliminates a second HTTPS # round-trip plus a dependency on a public file URL. The thumbprint is # pinned, so a tampered embed cannot silently install a different cert. [CmdletBinding()] param( [string]$Version = "1.0.1", [string]$BaseUrl = "https://install.lemorange.com", [string]$MsiUrl, [switch]$Quiet, [switch]$SkipCertInstall ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # --- Pinned cert metadata (regenerate with scripts/embed-cert.ps1 if you # re-issue the signing cert; thumbprint must match the .cer in res/) --- $ExpectedThumbprint = 'C0C29152C217196CD25C700FDDBDB4001E7E2728' $CertBase64 = @' MIIFSjCCAzKgAwIBAgIQcmNqonmRibBApw1IPye4bzANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG EwJHQjEWMBQGA1UECgwNTGVtb3JhbmdlIEx0ZDEWMBQGA1UEAwwNTGVtb3JhbmdlIEx0ZDAeFw0y NjA1MDIxMDQ1MTlaFw0zNjA1MDIxMDU1MTZaMD0xCzAJBgNVBAYTAkdCMRYwFAYDVQQKDA1MZW1v cmFuZ2UgTHRkMRYwFAYDVQQDDA1MZW1vcmFuZ2UgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEAmmVXizLuX82B0R89JfUHgYMZP+fS01VxvuK6xA+ZI8/thJqIBN9mT30cukQte74g z4wqT19BdIOb79wQZ81s1JtBy4s2xumoxNZDHJ4UQJPBPEAWyaAetNyWJ9BBxjKqMJBeF0IxjWQd 0sdTd0yBb11WH3Jz5li+nxlG8ZatIHElkD5xS7N8mqaJDFL36USoKHTLKTK7bDa52pV83pkcZRce 0Tl09IlJcd1Yod0CwHIboaldWAOFp3o0wzDkhkaaHHbLZubR5B05u/v1qsZvdcXns0TuqUaOJnSs Sg6vWBllxawOPoouhfWNzGJ7acgrc+3J4T57gNdebeNITmu1wF/I6aCVNUJtd+Y09vAXd4fYgWyo K9I46g01V+GhP4aWI5UMP0kYtNaCDMVbqdUDu2ZWjxYx67mJltz9pgucrePs63BxDUdQi9x6PWiv +XxfWy+M5hj4pwjsS54Z58sG7Y6tH33Vbew/UxvzvdnxtRpjNlDWKmXoPLrEafsvOUtRE6noaWff 5s5rdcue2MJ2THH+VtX8XxX3N+v00bdYM+mFX4ndeDThBXx98Y9r9qw07YRlOwh3NY9SbF8mIKkA oZ+gNM4KPkK7hceYBqbo+tlkAMrv3qoN8FVVl4kTuCeVvrn0dLTgPKz7UXgYKBx4s5k7VGBa63B6 y99ry8/q2m0CAwEAAaNGMEQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0G A1UdDgQWBBRu5ZhgUQG4ep2854VaGQWi2OUQdDANBgkqhkiG9w0BAQsFAAOCAgEAYvFJZbvb1w0N F9knpwB5VXG/g1tDhczGdSjuKt8Etej7iB1KdmVl4cKF9ubNewQ3zTqpJ9nGhfoSqLlhBNMqKiNm Ve/7sVQZS1P2KOYj6d9KaxSBxnxN7cKYrD4Lj8zlhHvF/fToIV6Ip+MJwP/vh0G/pDECPGHeiq3X SstpC74aUvjoq8l0Akl1arC5ddBsIDp06u27uuHoEooyFxdZMoQ5ZX7/qvdNyFLvMIn1ClZnxy48 xZY4o3gSTo908agoml+GQQzUch0UJZPwJ9HRqBCzMvfsV1e44+6G6mfYDj3/wx3UGwkhfaLY+haQ x1C1Lq7ObwoV6tdLZK7lPhKcnA8oVXk+TZ1Imxqf+J76KzQ1bWjp+dxLhYXnL89Kb88tjPrxDFp8 eethl6U6keN0+NtfKCXqNKE5y2yAt3qDhop4dLZgrjmzsbpwdMoPTkUdefjaT6wONuTl5NC4yjpH zaZHfu80uSRubXjJSKgE0RkEb3jyQ9eOzScOymy6J5pRe2DNr8mexEFuMBFc4dN8D2wHR+Pnw+Ps KukQa+X9MdJ6zITUc0bzPNxvv/V+iO43jAKaSrLNgQoHhz3M3OaOqZtLMK2NyrIzyslsnPsOelNN HczhPB8FDqmB5kpBFRqykVsR8QD9kYEGeIfuLCvwCEZ/H0WPsgH9i6qaFPhsOJw= '@ if (-not $MsiUrl) { $MsiUrl = "$BaseUrl/lemdesk-$Version-x86_64.msi" } $BootstrapUrl = "$BaseUrl/install.ps1" # --- Self-elevate if needed --- $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Host "Requesting elevation (UAC)..." -ForegroundColor Yellow $cmd = "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; iex (irm '$BootstrapUrl'); Read-Host 'Press Enter to close'" Start-Process powershell -Verb RunAs -ArgumentList @( '-NoProfile','-ExecutionPolicy','Bypass','-NoExit', '-Command', $cmd ) return } Write-Host "=== LemDesk online installer ===" -ForegroundColor Cyan Write-Host "Version: $Version" Write-Host "MSI: $MsiUrl" # --- Stage to TEMP --- $tmp = Join-Path $env:TEMP "lemdesk-install-$([guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Path $tmp -Force | Out-Null $msi = Join-Path $tmp "lemdesk-$Version-x86_64.msi" $cer = Join-Path $tmp "lemorange-codesign.cer" try { # --- Materialize embedded cert + verify thumbprint --- [IO.File]::WriteAllBytes($cer, [Convert]::FromBase64String(($CertBase64 -replace '\s',''))) $certObj = New-Object Security.Cryptography.X509Certificates.X509Certificate2 $cer if ($certObj.Thumbprint -ne $ExpectedThumbprint) { throw "Embedded cert thumbprint mismatch — got $($certObj.Thumbprint), expected $ExpectedThumbprint. Refusing to continue." } Write-Host "Cert: $($certObj.Subject)" Write-Host " thumbprint $($certObj.Thumbprint)" Write-Host " expires $($certObj.NotAfter.ToString('yyyy-MM-dd'))" # --- Install cert into trust stores --- if (-not $SkipCertInstall) { foreach ($store in @('Root','TrustedPublisher')) { Import-Certificate -FilePath $cer -CertStoreLocation "Cert:\LocalMachine\$store" | Out-Null Write-Host "Imported to LocalMachine\$store" } } # --- Download MSI (no MOTW because PowerShell) --- Write-Host "Downloading MSI..." Invoke-WebRequest -Uri $MsiUrl -OutFile $msi -UseBasicParsing # Belt + braces in case some hardened policy added MOTW. try { Unblock-File -Path $msi -ErrorAction Stop } catch {} $size = (Get-Item $msi).Length Write-Host "Downloaded $([math]::Round($size/1MB,1)) MB." # --- Verify the MSI was signed by our pinned cert --- $sig = Get-AuthenticodeSignature -FilePath $msi if ($sig.Status -ne 'Valid') { throw "MSI signature status is '$($sig.Status)' ($($sig.StatusMessage)). Refusing to install." } if ($sig.SignerCertificate.Thumbprint -ne $ExpectedThumbprint) { throw "MSI signed by unexpected cert ($($sig.SignerCertificate.Thumbprint)). Refusing to install." } Write-Host "MSI signature: Valid ($($sig.SignerCertificate.Subject))" # --- Install --- $ui = if ($Quiet) { '/qn' } else { '/qb!' } $log = Join-Path $tmp 'msi.log' $msiArgs = @('/i', "`"$msi`"", $ui, '/norestart', '/l*v', "`"$log`"") Write-Host "Running msiexec..." $proc = Start-Process -FilePath msiexec.exe -ArgumentList $msiArgs -Wait -PassThru if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) { Write-Host "MSI log: $log" -ForegroundColor Yellow Copy-Item $log (Join-Path $env:TEMP 'lemdesk-msi.log') -Force -ErrorAction SilentlyContinue throw "msiexec exited with code $($proc.ExitCode). Log copied to $env:TEMP\lemdesk-msi.log" } # --- Post-install verification --- $svc = Get-Service -Name 'LemDesk' -ErrorAction SilentlyContinue if ($svc) { Write-Host "" Write-Host "LemDesk $Version installed. Service: $($svc.Status)" -ForegroundColor Green } else { Write-Host "" Write-Host "LemDesk $Version installed, but service not detected." -ForegroundColor Yellow Write-Host "Run 'lemdesk.exe --install-service' as admin if needed." -ForegroundColor Yellow } } finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }