De licentie zit al in de firmware
Genoeg mensen bezitten een licentie voor de Fisher-Price OS (Windows) zonder ooit de productsleutel te hebben gezien. Hij kwam met de machine, en de sleutel woont in de firmware van die machine, in een kleine ACPI-tabel die MSDM heet.
Dan is de machine niet meer de plek waar het werk gebeurt. Of de eigenaar wil die licentie in een VM die hij nu van jou huurt, of de machine zelf wordt gewist, omgebouwd tot Proxmox-host, en het exemplaar van het OS waarmee hij werd geleverd moet terug als VM erbovenop.
Mechanisch is het één QEMU-optie. Dat is het probleem. QEMU geeft elke tabel aan een gast zonder hem te controleren, de gast activeert met welke sleutel hij ook vindt, en nergens in de stack vraagt iets of de licentie dat allemaal toestaat. Deze post behandelt wat de tabel is, hoe je hem in een VM krijgt zonder een corrupte door te geven, wat de voorwaarden van Microsoft zeggen over verhuizen, en waarom een volumelicentie er nooit in de buurt komt.
Wat de tabel is
Sinds OEM Activation 3.0 drukt een pc-bouwer de sleutel niet meer op een sticker aan de onderkant van de kast, waar iedereen met een telefooncamera hem kan kopiëren, en gaat de sleutel in plaats daarvan in de firmware van de machine. De fabrieksgereedschappen van Microsoft “injects the product keys into the firmware”, en de validatiestap controleert “that the MSDM table exists” en dat de header en de velden “comply with the correct formats”1. Een machine die zo is ingericht, wordt “activated by using the OA3 DPK in the firmware”2. MSDM is een gewone ACPI-tabel, en op Linux lees je hem rechtstreeks uit de firmware:
sudo cat /sys/firmware/acpi/tables/MSDM > msdm.bin
De laptop waarop dit geschreven is, heeft er een. 85 bytes.
De gepubliceerde specificatie van Microsoft definieert de standaard ACPI-header van 36 bytes met de signatuur MSDM, en stopt dan.
Alles na de header is een “Proprietary data structure that contains all the licensing data necessary to enable Windows activation”3.
| Bytes | Bevat | Bekend uit |
|---|---|---|
| 0 tot 35 | de standaard ACPI-header: signatuur MSDM, lengte, checksum, OEM-ID | de specificatie van Microsoft |
| 36 tot 55 | 20 bytes aan velden: versie, gegevenstype, gegevenslengte | echte tabellen, geen gepubliceerde specificatie |
| 56 tot 84 | de productsleutel van 29 tekens, vijf groepen van vijf | echte tabellen, geen gepubliceerde specificatie |
QEMU geeft alles door wat je hem geeft
-acpitable file= van QEMU neemt de “whole ACPI table from the specified files, including all ACPI headers (possible overridden by other options)”4, en de gast ziet dan een MSDM-tabel precies zoals de firmware van een laptop hem zou presenteren.
Dat is gecontroleerd met een bewust nepsleutel.
De bytes kwamen er aan de andere kant identiek uit, van signatuur tot laatste teken.
QEMU weigert niets.
Dit doet hw/acpi/core.c met een kapotte upload5:
| Wat er mis is met het bestand | Wat QEMU doet | Wat de gast ziet |
|---|---|---|
| Headerlengte klopt niet met het bestand | waarschuwt, en overschrijft dan de lengte met de echte grootte | een geldige header |
| Checksum telt niet op tot nul | rekent hem opnieuw uit, op elke tabel, elke keer | een geldige checksum |
| Sleutel afgekapt of verminkt | niets, de gegevens na de header zijn zijn zaak niet | een geldig ogende tabel rond een kapotte sleutel |
De gast kan het verschil niet zien, en jij ook niet totdat iemand een supportticket opent. Het eerste wat iemand ervan hoort, is een klant wiens activering mislukte.
Uploaden klanten deze dus, controleer ze dan voordat QEMU ze ooit ziet:
#!/usr/bin/env python3
"""Refuse anything that is not a well-formed MSDM table before QEMU sees it."""
import re, struct, sys
t = open(sys.argv[1], 'rb').read()
fail = lambda why: sys.exit(f"{sys.argv[1]}: {why}")
if len(t) < 56: fail(f"{len(t)} bytes, too short for an MSDM table")
sig, length = t[:4], struct.unpack_from('<I', t, 4)[0]
if sig != b'MSDM': fail(f"signature is {sig!r}, not MSDM")
if length != len(t): fail(f"header says {length} bytes, file is {len(t)}")
if sum(t) & 0xff: fail("checksum does not sum to zero")
ver, _, dtype, _, dlen = struct.unpack_from('<5I', t, 36)
if (ver, dtype) != (1, 1): fail(f"version {ver}, data type {dtype}: expected 1 and 1")
if dlen != 29 or 56 + dlen != length: fail(f"data length {dlen}: expected 29")
key = t[56:].decode('ascii', 'replace')
if not re.fullmatch(r'([0-9A-Z]{5}-){4}[0-9A-Z]{5}', key): fail("data is not a 5x5 product key")
print(f"OK OEM {t[10:16].decode(errors='replace').strip()!r} key *****-*****-*****-*****-{key[-5:]}")
| Controleert | Houdt het bestand tegen | Een fout betekent |
|---|---|---|
| Signatuur, lengte, checksum | de gedocumenteerde header van Microsoft | het bestand is kapot |
| Versie, gegevenstype, gegevenslengte, vorm van de sleutel | de vorm die echte tabellen hebben | bekijk deze met de hand, niet “dit is vervalst” |
Hij drukt nooit de sleutel af, alleen de laatste groep. Een productsleutel in een logbestand is een productsleutel die iemand anders kan gebruiken.
Gedraaid tegen een goede tabel en drie kapotte kopieën ervan:
OK OEM 'EXMPLE' key *****-*****-*****-*****-EEEEE
bad-sum.bin: checksum does not sum to zero
bad-trunc.bin: header says 85 bytes, file is 70
bad-sig.bin: signature is b'SLIC', not MSDM
Waar hij komt, en wie hem daar mag zetten
Een productsleutel is een geheim.
Hij hoort nergens waar www-data hem kan lezen, en pmxcfs geeft je precies één plek in /etc/pve waar dat niet kan6:
| Waar de tabel zou kunnen staan | Wie hem kan lezen | Op elke node waar de VM naartoe kan migreren |
|---|---|---|
/etc/pve/priv | alleen root | ja |
ergens anders in /etc/pve | leesbaar voor de groep, www-data van de webinterface inbegrepen | ja |
| een map op één node | wat je ook instelt | nee |
Met 85 bytes zit hij nergens in de buurt van de bestandslimiet van 1 MiB in pmxcfs7:
mkdir -p /etc/pve/priv/msdm
check-msdm.py upload.bin && cp upload.bin /etc/pve/priv/msdm/9000.bin
qm set 9000 --args "-acpitable file=/etc/pve/priv/msdm/9000.bin"
qm set --args vervangt de hele regel.
Heeft de VM al SMBIOS- of firmwareopties in args, schrijf ze dan allemaal samen uit.
En omdat args alleen voor root is, is dit een klus voor je provisioning, niet iets wat een klant vanuit de webinterface kan doen.
Zo hoort het ook.
De klant levert het bestand, jouw tooling controleert het en koppelt het.
De tabel verhuist een sleutel, geen recht
Hier vraagt de functie om een helder hoofd. De MSDM-tabel naar een VM verhuizen verhuist de sleutel. Niet de licentie.
De eigen voorwaarden van Microsoft zeggen dat, en de regels die ertoe doen passen in een tabel:
| Geval | Wat Microsoft zegt | Bron |
|---|---|---|
| OEM-licentie, verhuizen | overdraagbaar aan een andere gebruiker “only with the licensed device” | OEM-licentievoorwaarden8 |
| OEM-licentie, hoeveel installaties | “only one instance of the software for use on one device, whether that device is physical or virtual” | OEM-licentievoorwaarden8 |
| OEM-licentie, in gewone woorden | “locked” aan de oorspronkelijke pc “and cannot be transferred to any other PC” | Microsoft small business blog9 |
| De uitzondering | de overdrachtsbepalingen “do not apply” waar de software in Duitsland of een lijst andere landen is gekocht | OEM-licentievoorwaarden8 |
| Het hosten voor klanten | de Services Provider License Agreement is voor “hosted applications to end customers” | SPLA10 |
| Desktopedities als gehoste VM’s | “VMs must be hosted by a Qualified Multitenant Hoster (QMTH)” | Microsoft Learn11 |
De tabel komt altijd van de eigen gelicentieerde machine van de klant, nooit van jouw hosts. Het geval dat recht binnen de tekst valt, is het eenvoudigste: de machine waarmee de licentie werd verkocht, nu met Proxmox erop, met datzelfde exemplaar van de Fisher-Price OS (Windows) verhuisd naar één VM erop. Dat is nog steeds één instantie op één apparaat, en de OEM-voorwaarden staan dat toe “whether that device is physical or virtual”8. Een tweede VM, of een andere machine, en dat is het niet meer.
In dat ene geval zijn de machine van de klant en de hypervisor dezelfde doos, dus er valt niets te uploaden en niets te kopiëren.
De Proxmox-kernel stelt de MSDM-tabel van de firmware al beschikbaar onder /sys/firmware/acpi/tables/, alleen leesbaar voor root, en QEMU draait op Proxmox als root, dus het kan de tabel daar rechtstreeks vandaan nemen:
qm set 100 --args "-acpitable file=/sys/firmware/acpi/tables/MSDM"
Naar de live tabel wijzen in plaats van naar een kopie heeft twee gevolgen.
Het pad wordt gelezen op de node waar de VM ook maar start, dus dit hoort op een losse host en niet in een cluster: migreer de VM en hij krijgt ofwel de tabel van die node, een andere licentie op een andere machine, of start helemaal niet, omdat QEMU een ontbrekend bestand weigert met can't open file … No such file or directory.
En het is één regel, en dus één regel om op een tweede VM te plakken.
Die tweede VM is het geval dat de voorwaarden niet dekken, dus één VM krijgt hem en verder geen.
Bouw de functie dus. Het mechanisme deugt, en er zijn klanten die het mogen gebruiken; een die zijn licentie in Duitsland kocht, kan daar best bij zijn. Maar leg het recht waar het hoort: in je servicevoorwaarden staat de klant ervoor in dat hij een licentie heeft die het draaien in jouw VM dekt, en dat doet hij voordat de uploadknop iets doet. De validator bewijst dat de tabel goed gevormd is. Niets bewijst dat hij de zijne is om te gebruiken.
Volumelicenties komen nooit in de buurt van de tabel
Kan een klant met een volumelicentie dezelfde route gebruiken? Nee. Er is niets voor de tabel om te dragen.
MSDM hoort bij het OEM-kanaal en bij niets anders. De eigen planningsgids van Microsoft zegt dat OEM-activering “is available only for computers that are purchased through OEM channels and have the Windows operating system preinstalled”12. Volumelicenties activeren via een van drie andere modellen, “Multiple Activation Keys (MAK)”, “KMS” en “Active Directory-based activation”12, en in elk daarvan wordt de sleutel in het besturingssysteem geïnstalleerd. Niets leest hem uit de firmware.
| Methode | Waar de sleutel woont | Waar de gast mee praat | In een Proxmox-VM |
|---|---|---|---|
| OEM, OA 3.0 | de MSDM-tabel in de firmware | de activeringsservers van Microsoft | de -acpitable-route hierboven |
| MAK | geïnstalleerd in de gast | Microsoft, één keer, afgeteld van de activeringen van de sleutel | werkt met internettoegang of een telefoon |
| KMS | een generieke sleutel (GVLK) in de gast | de KMS-host van de klant op TCP 1688 | heeft een route ernaartoe nodig, en 25 clients of 5 servers voordat hij iets activeert |
| Active Directory | een GVLK in de gast | de domeincontrollers van de klant, minstens elke 180 dagen | de VM moet lid zijn van hun domein |
| AVMA | een generieke sleutel in de gast | de eigen Datacenter-licentie van de Hyper-V-host | helemaal niet beschikbaar |
Voor een volumeklant valt er op de hypervisor dus niets te bouwen. Hun sleutel gaat in hun image, en jouw deel is het netwerkpad: poort 1688 naar hun KMS-host, of een VPN naar hun domeincontrollers. Het MSDM-slot blijft leeg, en zo hoort het.
Twee dingen zetten mensen op het verkeerde been. De generieke sleutel op volumemedia activeert op zichzelf niets, want “the GVLK doesn’t work unless a valid KMS host key can be found”12, dus een VM gebouwd van de Enterprise-ISO van de klant zonder route naar huis blijft ongeactiveerd staan. En een volumelicentie voor de desktop is geen licentie op zich. Volumeprogramma’s “cover upgrades to Windows client operating systems only”, en “an existing retail or OEM operating system license is needed for each computer”12. De volumelicentie ligt bovenop een basislicentie, heel vaak dezelfde OEM-licentie waar de vorige sectie over ging, en of die in jouw VM mag draaien is nog steeds de hostingvraag in die tabel, geen activeringsvraag.
AVMA verdient een eigen regel, want het is degene die op het antwoord lijkt. Het is de manier van Microsoft waarop een host zijn eigen servergasten activeert, door “the VM activation to the licensed virtualization host” te binden, en het vereist “a Windows Server Datacenter edition with the Hyper-V server host role installed”13. Microsoft is duidelijk over de rest: “AVMA doesn’t work with other server virtualization technologies”13. Op Proxmox activeren servergasten via jouw SPLA-sleutels of via de eigen KMS van de klant.
Niets in de stack controleert het papierwerk
Elke laag hier doet haar werk en niets meer. De firmware bewaart een sleutel. QEMU kopieert de bytes en ruimt de header op. De gast leest de sleutel en activeert ermee. Geen van hen kan zien of de machine eronder degene is waarmee de licentie werd verkocht, en geen van hen was daar ooit voor bedoeld.
De controle komt dus terecht bij wie de hypervisor draait. Een licentie is een overeenkomst tussen degene die hem kocht en Microsoft, en jouw platform is daar geen partij in, maar het is wel het ding dat de sleutel verhuist, en dat maakt de vraag de jouwe of je erom vroeg of niet.
Schrijf het antwoord op voordat de uploadknop iets doet. Eén machine, één VM, de eigen licentie van de klant en zijn woord dat die dit dekt. Het kost niks, en het beschermt jou net zoveel als hen. De software verhuist elke sleutel die hij krijgt. Of dat had gemoeten, is een vraag die alleen een mens kan beantwoorden, dus zorg dat er een dat doet.
Microsoft Learn, OA 3.0 on the factory floor — “injects the product keys into the firmware”;
/Validatecontroleert “that the MSDM table exists”. ↩︎Microsoft Learn, Validate an OEM Activation key — “is activated by using the OA3 DPK in the firmware”. ↩︎
Microsoft, Microsoft Software Licensing Tables (SLIC and MSDM) — tabel 2, offset 36: “Proprietary data structure that contains all the licensing data necessary to enable Windows activation.” ↩︎
QEMU, System Emulation, Invocation —
-smbios-velden per type;-acpitable“For file=, take whole ACPI table from the specified files, including all ACPI headers”;-boot“Currently Seabios for X86 system support it.” ↩︎QEMU v11.0.3,
hw/acpi/core.c— “ACPI table has wrong length” is eenwarn_report, waarna de lengte wordt overschreven en de checksum opnieuw berekend. ↩︎pve-cluster,
src/pmxcfs/pmxcfs.c— paden onderprivworden gemaskeerd tot0777700, al het andere tot0777750. ↩︎pve-cluster,
src/pmxcfs/memdb.h—#define MEMDB_MAX_FILE_SIZE (1024 * 1024) // 1 MiB. ↩︎Microsoft, Windows 11 OEM licence terms — “you may transfer the license to use the software directly to another user, only with the licensed device”; de overdrachtsbepalingen “do not apply if you acquired the software in Germany” of de genoemde landen. Gearchiveerde kopie; de live pagina weigert geautomatiseerde verzoeken. ↩︎ ↩︎ ↩︎ ↩︎
Microsoft, small business blog archive — “the OEM Windows license is ’locked’ to the original PC it comes with and cannot be transferred to any other PC.” ↩︎
Microsoft, Services Provider License Agreement — “for service providers and software development companies licensing eligible Microsoft products to provide software services and hosted applications to end customers.” ↩︎
Microsoft Learn, Windows subscription activation for VDA — “VMs must be hosted by a Qualified Multitenant Hoster (QMTH).” ↩︎
Microsoft Learn, Plan for volume activation — de drie modellen voor volumeactivering; de KMS-drempels van “at least five computers” voor servers en “at least 25 computers” voor clients, op TCP-poort 1688; activering via Active Directory die het domein “at least once every 180 days” nodig heeft; “the GVLK doesn’t work unless a valid KMS host key can be found”; volumelicenties “cover upgrades to Windows client operating systems only”. ↩︎ ↩︎ ↩︎ ↩︎
Microsoft Learn, Automatic Virtual Machine Activation in Windows Server — “AVMA requires a Windows Server Datacenter edition with the Hyper-V server host role installed”; “AVMA doesn’t work with other server virtualization technologies.” ↩︎ ↩︎