From 9059dbc85b74908cd1dee0a4e83130d3d32c04c1 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Mon, 1 Jun 2026 10:12:03 +0000 Subject: [PATCH] kms-website: auto-fetch + auto-install GVLKs (no manual key lookup) Scripts now detect the running edition and fetch the matching GVLK from a published key list instead of requiring the user to copy one from the table. - data/products.yaml: add editionid to every Windows/Server entry, plus build numbers where an EditionID spans releases (LTSC, Server). Azure Edition left unmapped on purpose (collides with Datacenter; KMS may fail there anyway). - /keys.json: Hugo KEYS output format renders products.yaml as JSON (single source of truth). layouts/index.keys.json. - setup-kms.ps1: when no VL key is installed, read registry EditionID (+build/ProductType for server) -> fetch /keys.json -> slmgr /ipk the match -> activate. Only acts when not already licensed (never clobbers retail). - kms-bootstrap.ps1: same for Windows; for Office/Project/Visio, read Click-to-Run ProductReleaseIds -> ospp /inpkey the matching GVLK -> /act. - $env:KMS_KEYS_URL overrides the key-list URL. Co-Authored-By: Claude Opus 4.7 --- data/products.yaml | 46 ++++++++++++++++++++++ hugo.toml | 12 ++++++ layouts/index.keys.json | 2 + static/scripts/kms-bootstrap.ps1 | 66 +++++++++++++++++++++++++++++--- static/scripts/setup-kms.ps1 | 65 +++++++++++++++++++++++-------- 5 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 layouts/index.keys.json diff --git a/data/products.yaml b/data/products.yaml index a6bc9cc..d0bd09e 100644 --- a/data/products.yaml +++ b/data/products.yaml @@ -2,93 +2,123 @@ windows: # Windows 11 / Windows 10 — desktop SKUs (modern) - family: "Windows 11 / 10" edition: "Pro" + editionid: "Professional" gvlk: "W269N-WFGWX-YVC9B-4J6C9-T83GX" notes: "Same GVLK for Win11 and Win10 Pro." current: true - family: "Windows 11 / 10" edition: "Pro N" + editionid: "ProfessionalN" gvlk: "MH37W-N47XK-V7XM9-C7227-GCQG9" notes: "EU 'N' edition (no media pack)." current: true - family: "Windows 11 / 10" edition: "Pro for Workstations" + editionid: "ProfessionalWorkstation" gvlk: "NRG8B-VKK3Q-CXVCJ-9G2XF-6Q84J" current: true - family: "Windows 11 / 10" edition: "Pro for Workstations N" + editionid: "ProfessionalWorkstationN" gvlk: "9FNHH-K3HBT-3W4TD-6383H-6XYWF" current: true - family: "Windows 11 / 10" edition: "Pro Education" + editionid: "ProfessionalEducation" gvlk: "6TP4R-GNPTD-KYYHQ-7B7DP-J447Y" current: true - family: "Windows 11 / 10" edition: "Pro Education N" + editionid: "ProfessionalEducationN" gvlk: "YVWGF-BXNMC-HTQYQ-CPQ99-66QFC" current: true - family: "Windows 11 / 10" edition: "Education" + editionid: "Education" gvlk: "NW6C2-QMPVW-D7KKK-3GKT6-VCFB2" current: true - family: "Windows 11 / 10" edition: "Education N" + editionid: "EducationN" gvlk: "2WH4N-8QGBV-H22JP-CT43Q-MDWWJ" current: true - family: "Windows 11 / 10" edition: "Enterprise" + editionid: "Enterprise" gvlk: "NPPR9-FWDCX-D2C8J-H872K-2YT43" current: true - family: "Windows 11 / 10" edition: "Enterprise N" + editionid: "EnterpriseN" gvlk: "DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4" current: true - family: "Windows 11 / 10" edition: "Enterprise G" + editionid: "EnterpriseG" gvlk: "YYVX9-NTFWV-6MDM3-9PT4T-4M68B" notes: "German government SKU." current: true - family: "Windows 11 / 10" edition: "Enterprise G N" + editionid: "EnterpriseGN" gvlk: "44RPN-FTY23-9VTTB-MP9BX-T84FV" current: true # LTSC editions - family: "Windows LTSC" edition: "Enterprise LTSC 2024 / 2021 / 2019" + editionid: "EnterpriseS" + builds: ["26100", "19044", "17763"] gvlk: "M7XTQ-FN8P6-TTKYV-9D4CC-J462D" notes: "Single GVLK across Win11 LTSC 2024 and Win10 LTSC 2021/2019." current: true - family: "Windows LTSC" edition: "Enterprise N LTSC 2024 / 2021 / 2019" + editionid: "EnterpriseSN" + builds: ["26100", "19044", "17763"] gvlk: "92NFX-8DJQP-P6BBQ-THF9C-7CG2H" current: true - family: "Windows LTSC" edition: "IoT Enterprise LTSC 2024 / 2021" + editionid: "IoTEnterpriseS" + builds: ["26100", "19044"] gvlk: "KBN8V-HFGQ4-MGXVD-347P6-PDQGT" notes: "Same GVLK for x64 and ARM64 IoT." current: true - family: "Windows LTSC" edition: "Enterprise LTSB 2016" + editionid: "EnterpriseS" + builds: ["14393"] gvlk: "DCPHK-NFMTC-H88MJ-PFHPY-QJ4BJ" notes: "Out of mainstream support; security updates ended Oct 2026." - family: "Windows LTSC" edition: "Enterprise N LTSB 2016" + editionid: "EnterpriseSN" + builds: ["14393"] gvlk: "QFFDN-GRT3P-VKWWX-X7T3R-8B639" - family: "Windows LTSC" edition: "Enterprise LTSB 2015" + editionid: "EnterpriseS" + builds: ["10240"] gvlk: "WNMTR-4C88C-JK8YV-HQ7T2-76DF9" notes: "End of support reached." - family: "Windows LTSC" edition: "Enterprise N LTSB 2015" + editionid: "EnterpriseSN" + builds: ["10240"] gvlk: "2F77B-TNFGY-69QQF-B8YKP-D69TJ" windows_server: - family: "Windows Server 2025" edition: "Standard" + editionid: "ServerStandard" + builds: ["26100"] gvlk: "TVRH6-WHNXV-R9WG3-9XRFY-MY832" current: true - family: "Windows Server 2025" edition: "Datacenter" + editionid: "ServerDatacenter" + builds: ["26100"] gvlk: "D764K-2NDRG-47T6Q-P8T8W-YP6DF" current: true - family: "Windows Server 2025" @@ -98,10 +128,14 @@ windows_server: current: true - family: "Windows Server 2022" edition: "Standard" + editionid: "ServerStandard" + builds: ["20348"] gvlk: "VDYBN-27WPP-V4HQT-9VMD4-VMK7H" current: true - family: "Windows Server 2022" edition: "Datacenter" + editionid: "ServerDatacenter" + builds: ["20348"] gvlk: "WX4NM-KYWYW-QJJR4-XV3QB-6VM33" current: true - family: "Windows Server 2022" @@ -111,25 +145,37 @@ windows_server: current: true - family: "Windows Server 2019" edition: "Standard" + editionid: "ServerStandard" + builds: ["17763"] gvlk: "N69G4-B89J2-4G8F4-WWYCC-J464C" current: true - family: "Windows Server 2019" edition: "Datacenter" + editionid: "ServerDatacenter" + builds: ["17763"] gvlk: "WMDGN-G9PQG-XVVXX-R3X43-63DFG" current: true - family: "Windows Server 2019" edition: "Essentials" + editionid: "ServerSolution" + builds: ["17763"] gvlk: "WVDHN-86M7X-466P6-VHXV7-YY726" current: true - family: "Windows Server 2016" edition: "Standard" + editionid: "ServerStandard" + builds: ["14393"] gvlk: "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY" notes: "EOL Jan 2027." - family: "Windows Server 2016" edition: "Datacenter" + editionid: "ServerDatacenter" + builds: ["14393"] gvlk: "CB7KF-BWN84-R7R2Y-793K2-8XDDG" - family: "Windows Server 2016" edition: "Essentials" + editionid: "ServerSolution" + builds: ["14393"] gvlk: "JCKRF-N37P4-C2D82-9YXRT-4M63B" office: # Office LTSC 2024 — current diff --git a/hugo.toml b/hugo.toml index 0919514..92b2525 100644 --- a/hugo.toml +++ b/hugo.toml @@ -14,6 +14,18 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"] kmsPort = 1688 bootstrapURL = "/scripts/kms-bootstrap.ps1" setupURL = "/scripts/setup-kms.ps1" + keysURL = "/keys.json" + +# Publish data/products.yaml as machine-readable JSON at /keys.json so the +# activation scripts can fetch the GVLKs (single source of truth, no hardcoding). +# /keys.json is carved out of Anubis alongside /scripts (see stacks/kms). +[outputs] + home = ["HTML", "KEYS"] + +[outputFormats.KEYS] + mediaType = "application/json" + baseName = "keys" + isPlainText = true [markup] [markup.goldmark.renderer] diff --git a/layouts/index.keys.json b/layouts/index.keys.json new file mode 100644 index 0000000..c844c5d --- /dev/null +++ b/layouts/index.keys.json @@ -0,0 +1,2 @@ +{{- /* Machine-readable GVLK list for the activation scripts. Served at /keys.json. */ -}} +{{- dict "windows" .Site.Data.products.windows "windows_server" .Site.Data.products.windows_server "office" .Site.Data.products.office | jsonify (dict "indent" " ") -}} diff --git a/static/scripts/kms-bootstrap.ps1 b/static/scripts/kms-bootstrap.ps1 index 1aadb98..cb2a945 100644 --- a/static/scripts/kms-bootstrap.ps1 +++ b/static/scripts/kms-bootstrap.ps1 @@ -3,6 +3,8 @@ # Interactive KMS activator. Asks what you want to activate (Windows / # already-installed Office / Project / Visio) and runs only what you confirm. # Points each product at the public KMS host (default: vlmcs.viktorbarzin.me:1688). +# When a Volume License key is missing it auto-detects the edition/product and +# fetches the matching GVLK from the published key list (no manual key lookup). # # Usage: # iwr -UseBasicParsing https://kms.viktorbarzin.me/scripts/kms-bootstrap.ps1 | iex @@ -20,7 +22,8 @@ [CmdletBinding()] param( [string]$KmsHost = $(if ($env:KMS_HOST) { $env:KMS_HOST } else { 'vlmcs.viktorbarzin.me' }), - [int] $KmsPort = $(if ($env:KMS_PORT) { [int]$env:KMS_PORT } else { 1688 }) + [int] $KmsPort = $(if ($env:KMS_PORT) { [int]$env:KMS_PORT } else { 1688 }), + [string]$KeysUrl = $(if ($env:KMS_KEYS_URL) { $env:KMS_KEYS_URL } else { 'https://kms.viktorbarzin.me/keys.json' }) ) $ErrorActionPreference = 'Stop' @@ -83,17 +86,50 @@ function Get-WindowsLicense { [pscustomobject]@{ Licensed = ($p.LicenseStatus -eq 1); DaysLeft = [int]([math]::Round($p.GracePeriodRemaining / 1440)) } } +# Fetch the published GVLK list once (single source of truth, no hardcoding). +$script:KeysCache = $null +function Get-Keys { + if ($script:KeysCache) { return $script:KeysCache } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + try { $script:KeysCache = (Invoke-WebRequest -UseBasicParsing -Uri $KeysUrl -TimeoutSec 20).Content | ConvertFrom-Json } + catch { Warn "Could not fetch the key list from $KeysUrl"; $script:KeysCache = $null } + return $script:KeysCache +} + +# Auto-select the GVLK for THIS machine's edition (registry EditionID, locale- +# independent; narrowed by build for LTSC/Server which share an EditionID). +function Resolve-WindowsGvlk { + $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue + if (-not $cv) { return $null } + $editionId = $cv.EditionID; $build = "$($cv.CurrentBuildNumber)" + $isServer = $false + try { $isServer = ((Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).ProductType -ne 1) } catch {} + $keys = Get-Keys; if (-not $keys) { return $null } + $pool = if ($isServer) { $keys.windows_server } else { $keys.windows } + $m = $pool | Where-Object { $_.editionid -eq $editionId -and ( -not $_.builds -or ($_.builds -contains $build) ) } | Select-Object -First 1 + if ($m) { Write-Host " detected $editionId (build $build) -> $($m.edition)"; return $m.gvlk } + Write-Host " no published GVLK matches EditionID '$editionId' (build $build)" + return $null +} + function Activate-Windows { Step "Windows activation" $slmgr = "$env:WINDIR\System32\slmgr.vbs" & cscript //Nologo $slmgr /skms "$KmsHost`:$KmsPort" | Out-Host if ($LASTEXITCODE -ne 0) { Bad "slmgr /skms failed"; return } $lic = Get-WindowsLicense - if ($null -eq $lic) { Bad "No Volume License Windows SKU - install a GVLK first (slmgr /ipk )."; return } - if ($lic.Licensed) { OK "Windows already licensed ($($lic.DaysLeft) days) - host pinned, skipping /ato"; return } + if ($lic -and $lic.Licensed) { OK "Windows already licensed ($($lic.DaysLeft) days) - host pinned, skipping /ato"; return } + if ($null -eq $lic) { + Step "No Volume License key - fetching the GVLK for this edition" + $gvlk = Resolve-WindowsGvlk + if (-not $gvlk) { Bad "Couldn't auto-select a GVLK. Pick one from https://kms.viktorbarzin.me/#windows and run: slmgr /ipk "; return } + Write-Host " installing GVLK $gvlk" + & cscript //Nologo $slmgr /ipk $gvlk | Out-Host + if ($LASTEXITCODE -ne 0) { Bad "slmgr /ipk failed"; return } + } & cscript //Nologo $slmgr /ato | Out-Host $lic = Get-WindowsLicense - if ($lic.Licensed) { OK "Windows licensed ($($lic.DaysLeft) days)" } + if ($lic -and $lic.Licensed) { OK "Windows licensed ($($lic.DaysLeft) days)" } else { Bad "Windows not licensed - likely not a VL edition (Home/retail/OEM reject KMS). See https://kms.viktorbarzin.me/#faq" } } if ($doWin) { Activate-Windows } @@ -113,6 +149,16 @@ function Find-Ospp { return $null } +# Installed Click-to-Run Volume products, e.g. ProPlus2024Volume, VisioPro2024Volume. +# These IDs match the `product` field in the published key list exactly. +function Get-OfficeReleaseIds { + $c = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -ErrorAction SilentlyContinue + if ($c -and $c.ProductReleaseIds) { + return $c.ProductReleaseIds.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ -match 'Volume$' } + } + return @() +} + function Activate-Ospp([string]$label) { $ospp = Find-Ospp if (-not $ospp) { @@ -126,9 +172,19 @@ function Activate-Ospp([string]$label) { # in ospp output is a fixed literal, not localized). $st = & cscript //Nologo $ospp /dstatus 2>&1 | Out-String if ($st -match '---LICENSED---') { OK "$label already licensed - host set, skipping /act"; return } + # Not licensed: install the matching GVLK for each installed VL product of + # this family (Office = anything that isn't Project/Visio), fetched from the list. + $rels = Get-OfficeReleaseIds | Where-Object { + switch ($label) { 'Project' { $_ -match 'Project' } 'Visio' { $_ -match 'Visio' } default { $_ -notmatch 'Project|Visio' } } + } + $keys = Get-Keys + foreach ($rel in $rels) { + $k = if ($keys) { ($keys.office | Where-Object { $_.product -eq $rel } | Select-Object -First 1).gvlk } else { $null } + if ($k) { Write-Host " $rel -> installing GVLK $k"; & cscript //Nologo $ospp /inpkey:$k | Out-Host } + } & cscript //Nologo $ospp /act | Out-Host $st = & cscript //Nologo $ospp /dstatus 2>&1 | Out-String - if ($st -match '---LICENSED---') { OK "$label licensed" } else { Warn "$label status not LICENSED yet (no VL Office SKU? See https://kms.viktorbarzin.me/#office)" } + if ($st -match '---LICENSED---') { OK "$label licensed" } else { Warn "$label status not LICENSED yet (no VL $label SKU installed? See https://kms.viktorbarzin.me/#office)" } } if ($doOfficeAct) { Activate-Ospp 'Office' } if ($doProjAct) { Activate-Ospp 'Project' } diff --git a/static/scripts/setup-kms.ps1 b/static/scripts/setup-kms.ps1 index a7fd9da..afd34de 100644 --- a/static/scripts/setup-kms.ps1 +++ b/static/scripts/setup-kms.ps1 @@ -1,10 +1,11 @@ # setup-kms.ps1 # -# Minimal KMS-host wiring for an already-installed Volume License Windows. -# Pins the KMS host, then activates only if not already licensed. -# Idempotent - safe to re-run: if Windows is already licensed it reports the -# remaining days and exits without re-contacting the KMS server. -# Does NOT install Office. Does NOT change DNS suffix. Pin only. +# Minimal KMS wiring for Volume License Windows. Pins the KMS host and activates +# only if not already licensed. If no VL key is installed it auto-detects the +# Windows edition and fetches the matching GVLK from the published key list +# (no manual key lookup). Idempotent - safe to re-run: an already-licensed +# machine reports the remaining days and exits without re-contacting KMS. +# Does NOT install Office. Does NOT change DNS suffix. # # Usage: # iwr -UseBasicParsing https://kms.viktorbarzin.me/scripts/setup-kms.ps1 | iex @@ -18,7 +19,8 @@ [CmdletBinding()] param( [string]$KmsHost = $(if ($env:KMS_HOST) { $env:KMS_HOST } else { 'vlmcs.viktorbarzin.me' }), - [int] $KmsPort = $(if ($env:KMS_PORT) { [int]$env:KMS_PORT } else { 1688 }) + [int] $KmsPort = $(if ($env:KMS_PORT) { [int]$env:KMS_PORT } else { 1688 }), + [string]$KeysUrl = $(if ($env:KMS_KEYS_URL) { $env:KMS_KEYS_URL } else { 'https://kms.viktorbarzin.me/keys.json' }) ) $ErrorActionPreference = 'Stop' @@ -44,6 +46,27 @@ function Get-WindowsLicense { [pscustomobject]@{ Licensed = ($p.LicenseStatus -eq 1); DaysLeft = [int]([math]::Round($p.GracePeriodRemaining / 1440)) } } +# Auto-select the GVLK for THIS machine's edition from the published key list, +# so the user never has to look one up. Matches on the registry EditionID +# (locale-independent); for editions that share an EditionID across releases +# (LTSC, Server) it also narrows by the OS build number. +function Resolve-WindowsGvlk { + $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue + if (-not $cv) { return $null } + $editionId = $cv.EditionID + $build = "$($cv.CurrentBuildNumber)" + $isServer = $false + try { $isServer = ((Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).ProductType -ne 1) } catch {} + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + try { $keys = (Invoke-WebRequest -UseBasicParsing -Uri $KeysUrl -TimeoutSec 20).Content | ConvertFrom-Json } + catch { Bad "Could not fetch the key list from $KeysUrl"; return $null } + $pool = if ($isServer) { $keys.windows_server } else { $keys.windows } + $m = $pool | Where-Object { $_.editionid -eq $editionId -and ( -not $_.builds -or ($_.builds -contains $build) ) } | Select-Object -First 1 + if ($m) { Write-Host " detected $editionId (build $build) -> $($m.edition)"; return $m.gvlk } + Write-Host " no published GVLK matches EditionID '$editionId' (build $build)" + return $null +} + $slmgr = "$env:WINDIR\System32\slmgr.vbs" # Pin the KMS host. Idempotent: re-running just re-sets the same value. @@ -53,19 +76,29 @@ if ($LASTEXITCODE -ne 0) { Bad "slmgr /skms failed (exit $LASTEXITCODE): $out"; OK "KMS host pinned" $lic = Get-WindowsLicense -if ($null -eq $lic) { - Bad "No KMS-client (Volume License) Windows SKU detected." - Write-Host " Install a GVLK first: slmgr /ipk (see https://kms.viktorbarzin.me/#windows)" + +# Idempotent: already activated (retail OR KMS) -> don't re-hit the KMS server and +# never clobber a working key. The pinned host keeps Windows auto-renewing. +if ($lic -and $lic.Licensed) { + Write-Host "" + Write-Host "==> Already licensed ($($lic.DaysLeft) days remaining). Nothing to do." -ForegroundColor Green + Write-Host " Host is pinned to $KmsHost; Windows auto-renews every 7 days." return } -# Idempotent: already activated -> don't re-hit the KMS server. The pinned host -# above means Windows keeps auto-renewing every 7 days on its own. -if ($lic.Licensed) { - Write-Host "" - Write-Host "==> Already licensed via KMS ($($lic.DaysLeft) days remaining). Nothing to do." -ForegroundColor Green - Write-Host " Host is pinned to $KmsHost; Windows auto-renews every 7 days." - return +# No Volume License key installed -> fetch + install the GVLK for this edition. +if ($null -eq $lic) { + Step "No Volume License key installed - detecting edition and fetching its GVLK" + $gvlk = Resolve-WindowsGvlk + if (-not $gvlk) { + Bad "Could not auto-select a GVLK for this edition." + Write-Host " Pick one from https://kms.viktorbarzin.me/#windows and run: slmgr /ipk " + return + } + Step "Installing GVLK $gvlk" + $out = & cscript //Nologo $slmgr /ipk $gvlk 2>&1 + if ($LASTEXITCODE -ne 0) { Bad "slmgr /ipk failed (exit $LASTEXITCODE): $out"; return } + OK "GVLK installed" } Step "Activating (slmgr /ato)"