# Store-served Developer Suite / AppClient bootstrap (Windows x64). # https://store.createrun.ai and appclient are replaced by InstallerController before serving. # Run: & ([scriptblock]::Create((irm https://store.createrun.ai/installer/developer-tools/developersuite/install.ps1))) param([switch]$Help) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $store = 'https://store.createrun.ai'.TrimEnd('/') $product = 'appclient' $releaseKey = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUMU+nRYCGV1E+JC+153R1B9WD3YBqJvgEHLlZvJf/N+NIUKLA1qxGxxcNCMbHkCTK1zGn7XQ3nYInXVY24lf6A==' if ($Help) { Write-Output "Install $product from $store (Windows x64). Requires Administrator PowerShell." return } if ($product -notin @('developersuite', 'appclient') -or $store -notmatch '^https://store\.createrun\.(ai|com)$') { throw 'The Store origin or developer tool product in this script is invalid.' } if (-not [Environment]::Is64BitProcess -or $env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { throw 'This developer-tool bootstrap requires Windows x64 and 64-bit PowerShell.' } $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this command in an Administrator PowerShell window.' } try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { throw "TLS 1.2 is required: $_" } function Test-DeveloperToolManifestSignature([byte[]]$ManifestBytes, [string]$SignatureBase64, [string]$PublicKeySpki) { $spki = [Convert]::FromBase64String($PublicKeySpki) if ($spki.Length -ne 91 -or $spki[26] -ne 4 -or [Convert]::ToBase64String($spki[0..26]) -ne 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE') { throw 'The Developer Suite release key is not a P-256 signing key.' } $der = [Convert]::FromBase64String($SignatureBase64) if ($der.Length -lt 8 -or $der.Length -gt 72 -or $der[0] -ne 0x30 -or $der[1] -ne ($der.Length - 2)) { throw 'The Developer Suite manifest signature is not DER ECDSA.' } $signature = New-Object byte[] 64 $position = 2 for ($part = 0; $part -lt 2; $part++) { if ($position + 2 -gt $der.Length -or $der[$position] -ne 2) { throw 'Invalid DER signature integer.' } $length = [int]$der[$position + 1] $position += 2 if ($length -lt 1 -or $length -gt 33 -or $position + $length -gt $der.Length) { throw 'Invalid DER signature integer length.' } $first = [int]$der[$position] if ($length -eq 33) { if ($first -ne 0 -or $der[$position + 1] -lt 0x80) { throw 'Invalid DER signature padding.' } $position++ $length-- } elseif ($first -ge 0x80 -or ($length -gt 1 -and $first -eq 0 -and $der[$position + 1] -lt 0x80)) { throw 'Invalid DER signature integer encoding.' } [Array]::Copy($der, $position, $signature, ($part * 32 + 32 - $length), $length) $position += $length } if ($position -ne $der.Length) { throw 'The DER signature has trailing bytes.' } $keyBlob = New-Object byte[] 72 [BitConverter]::GetBytes([uint32]0x31534345).CopyTo($keyBlob, 0) [BitConverter]::GetBytes([uint32]32).CopyTo($keyBlob, 4) [Array]::Copy($spki, 27, $keyBlob, 8, 64) $key = [Security.Cryptography.CngKey]::Import($keyBlob, [Security.Cryptography.CngKeyBlobFormat]::EccPublicBlob) try { $verifier = New-Object Security.Cryptography.ECDsaCng($key) try { $verifier.HashAlgorithm = [Security.Cryptography.CngAlgorithm]::Sha256 if (-not $verifier.VerifyData($ManifestBytes, $signature)) { throw 'The Developer Suite manifest signature is invalid.' } } finally { $verifier.Dispose() } } finally { $key.Dispose() } } $channel = "$store/installer/developer-tools/developersuite/stable" $version = ([string](Invoke-RestMethod "$channel/latest")).Trim() if ($version -notmatch '^\d+\.\d+\.\d+$') { throw 'The Store returned an invalid Developer Suite version.' } $work = Join-Path $env:TEMP ('createrun-developer-tool-' + [Guid]::NewGuid().ToString('N')) $payload = Join-Path $work 'payload' New-Item -ItemType Directory -Path $payload -Force | Out-Null try { $manifestPath = Join-Path $work 'manifest.json' $signaturePath = Join-Path $work 'manifest.json.sig' Invoke-WebRequest "$channel/releases/$version/manifest.json" -OutFile $manifestPath -UseBasicParsing Invoke-WebRequest "$channel/releases/$version/manifest.json.sig" -OutFile $signaturePath -UseBasicParsing $manifestBytes = [IO.File]::ReadAllBytes($manifestPath) Test-DeveloperToolManifestSignature $manifestBytes ([IO.File]::ReadAllText($signaturePath).Trim()) $releaseKey $manifest = ConvertFrom-Json ([Text.Encoding]::UTF8.GetString($manifestBytes)) $file = "developersuite-$version-win-x64.zip" if ($manifest.schema -ne 'createrun.developer-tool.release@1' -or $manifest.product -ne 'developersuite' -or $manifest.channel -ne 'stable' -or $manifest.platform -ne 'win-x64' -or $manifest.version -ne $version -or $manifest.file -ne $file -or $manifest.sha256 -notmatch '^[a-f0-9]{64}$' -or $manifest.size -le 0 -or $manifest.size -gt 300000000) { throw 'The signed Developer Suite manifest does not match the requested release.' } $archive = Join-Path $work $file Invoke-WebRequest "$store/installer/developer-tools/developersuite/versions/$version/win-x64/$file" -OutFile $archive -UseBasicParsing if ((Get-Item -LiteralPath $archive).Length -ne [long]$manifest.size -or (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $manifest.sha256) { throw 'The Developer Suite archive size or SHA-256 differs from the Store manifest.' } Add-Type -AssemblyName System.IO.Compression Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [IO.Compression.ZipFile]::OpenRead($archive) try { if ($zip.Entries.Count -gt 20000) { throw 'The archive has too many entries.' } $total = [long]0 $root = [IO.Path]::GetFullPath($payload).TrimEnd('\') + '\' foreach ($entry in $zip.Entries) { $total += $entry.Length if ($total -gt 1000000000) { throw 'The archive is too large when extracted.' } if ((($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000) { throw 'The archive contains a symbolic link.' } $target = [IO.Path]::GetFullPath((Join-Path $payload $entry.FullName)) if (-not $target.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) { throw 'The archive contains an unsafe path.' } if ($entry.FullName.EndsWith('/')) { New-Item -ItemType Directory -Path $target -Force | Out-Null; continue } New-Item -ItemType Directory -Path ([IO.Path]::GetDirectoryName($target)) -Force | Out-Null [IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $target, $false) } } finally { $zip.Dispose() } $installer = Join-Path $payload 'DeveloperSuite.Installer.exe' if (-not (Test-Path -LiteralPath $installer -PathType Leaf)) { throw 'The Suite archive lacks DeveloperSuite.Installer.exe.' } $stdout = Join-Path $work 'installer.stdout.log' $stderr = Join-Path $work 'installer.stderr.log' function Invoke-DeveloperToolInstaller([string[]]$InstallerArgs) { $process = Start-Process -FilePath $installer -ArgumentList $InstallerArgs -Wait -PassThru -WindowStyle Hidden ` -RedirectStandardOutput $stdout -RedirectStandardError $stderr Get-Content -LiteralPath $stdout, $stderr -ErrorAction SilentlyContinue | Write-Output if ($process.ExitCode -ne 0) { throw "Developer Suite installer exited $($process.ExitCode)." } } Invoke-DeveloperToolInstaller @('--verify-release-key', $releaseKey) $previousStore = [Environment]::GetEnvironmentVariable('CREATERUN_DEVELOPER_TOOLS_STORE_URL', 'Process') try { [Environment]::SetEnvironmentVariable('CREATERUN_DEVELOPER_TOOLS_STORE_URL', $store, 'Process') if ($product -eq 'appclient') { Invoke-DeveloperToolInstaller @('--cli', 'install-appclient') } else { Invoke-DeveloperToolInstaller @('--cli', 'install') } } finally { [Environment]::SetEnvironmentVariable('CREATERUN_DEVELOPER_TOOLS_STORE_URL', $previousStore, 'Process') } Write-Output "$product installation completed. Run the product's Store health-check guide next." } finally { if (Test-Path -LiteralPath $work) { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue } }