Die Lizenz steckt schon in der Firmware
Viele Leute besitzen eine Lizenz für das Fisher-Price OS (Windows), ohne je ihren Produktschlüssel gesehen zu haben. Sie kam mit der Maschine, und der Schlüssel lebt in der Firmware dieser Maschine, in einer kleinen ACPI-Tabelle namens MSDM.
Dann ist die Maschine nicht mehr der Ort, an dem die Arbeit passiert. Entweder will ihr Besitzer diese Lizenz in einer VM, die er jetzt bei dir mietet, oder die Maschine selbst wird gelöscht, zu einem Proxmox-Host gemacht, und die Kopie des Betriebssystems, mit der sie ausgeliefert wurde, soll als VM obendrauf zurück.
Technisch ist es eine QEMU-Option. Genau das ist das Problem. QEMU reicht jede Tabelle ungeprüft an einen Gast weiter, der Gast aktiviert mit dem Schlüssel, den er findet, und nirgends im ganzen Stapel fragt irgendetwas, ob die Lizenz das alles erlaubt. Dieser Beitrag behandelt, was die Tabelle ist, wie man sie in eine VM bringt, ohne eine kaputte weiterzureichen, was Microsofts Bedingungen über den Umzug sagen, und warum eine Volumenlizenz nie in ihre Nähe kommt.
Was die Tabelle ist
Seit OEM Activation 3.0 druckt ein PC-Hersteller den Schlüssel nicht mehr auf einen Aufkleber unten am Gehäuse, wo jeder mit einer Handykamera ihn abschreiben kann, sondern der Schlüssel kommt stattdessen in die Firmware der Maschine. Microsofts Fabrikwerkzeug „injects the product keys into the firmware“, und sein Prüfschritt kontrolliert, „that the MSDM table exists“ und dass Header und Einträge „comply with the correct formats“1. Eine so eingerichtete Maschine ist „activated by using the OA3 DPK in the firmware“2. MSDM ist eine gewöhnliche ACPI-Tabelle, und unter Linux liest man sie direkt aus der Firmware:
sudo cat /sys/firmware/acpi/tables/MSDM > msdm.bin
Der Laptop, auf dem dieser Text geschrieben wurde, trägt eine. 85 Byte.
Microsofts veröffentlichte Spezifikation definiert den üblichen ACPI-Header mit 36 Byte und der Signatur MSDM, und hört dann auf.
Alles nach dem Header ist eine „Proprietary data structure that contains all the licensing data necessary to enable Windows activation“3.
| Byte | Enthält | Bekannt aus |
|---|---|---|
| 0 bis 35 | der übliche ACPI-Header: Signatur MSDM, Länge, Prüfsumme, OEM-ID | Microsofts Spezifikation |
| 36 bis 55 | 20 Byte an Feldern: Version, Datentyp, Datenlänge | echte Tabellen, keine veröffentlichte Spezifikation |
| 56 bis 84 | der Produktschlüssel mit 29 Zeichen, fünf Gruppen zu fünf | echte Tabellen, keine veröffentlichte Spezifikation |
QEMU reicht alles weiter, was du ihm gibst
-acpitable file= in QEMU nimmt die „whole ACPI table from the specified files, including all ACPI headers (possible overridden by other options)“4, und der Gast sieht dann eine MSDM-Tabelle genau so, wie die Firmware eines Laptops sie präsentieren würde.
Das wurde mit einem absichtlich gefälschten Schlüssel geprüft.
Die Byte kamen auf der anderen Seite identisch heraus, von der Signatur bis zum letzten Zeichen.
QEMU lehnt nichts ab.
Das macht hw/acpi/core.c mit einem kaputten Upload5:
| Was an der Datei falsch ist | Was QEMU tut | Was der Gast sieht |
|---|---|---|
| Länge im Header passt nicht zur Datei | warnt, dann überschreibt es die Länge mit der echten Größe | einen gültigen Header |
| Prüfsumme ergibt nicht null | rechnet sie neu, bei jeder Tabelle, jedes Mal | eine gültige Prüfsumme |
| Schlüssel abgeschnitten oder verstümmelt | nichts, die Daten nach dem Header gehen es nichts an | eine gültig aussehende Tabelle um einen kaputten Schlüssel |
Der Gast kann den Unterschied nicht erkennen, und du auch nicht, bis jemand ein Support-Ticket aufmacht. Das Erste, was man davon hört, ist ein Kunde, dessen Aktivierung fehlgeschlagen ist.
Wenn Kunden diese Tabellen also hochladen, prüf sie, bevor QEMU sie je zu sehen bekommt:
#!/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:]}")
| Prüft | Misst die Datei an | Ein Fehlschlag heißt |
|---|---|---|
| Signatur, Länge, Prüfsumme | Microsofts dokumentiertem Header | die Datei ist kaputt |
| Version, Datentyp, Datenlänge, Form des Schlüssels | der Form, die echte Tabellen haben | sieh dir diese von Hand an, nicht „die ist gefälscht“ |
Er gibt nie den Schlüssel aus, nur die letzte Gruppe. Ein Produktschlüssel in einer Logdatei ist ein Produktschlüssel, den jemand anderes benutzen kann.
Gegen eine gute Tabelle und drei kaputte Kopien davon laufen gelassen:
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
Wo sie hinkommt, und wer sie dort hinlegen darf
Ein Produktschlüssel ist ein Geheimnis.
Er gehört nirgends hin, wo www-data ihn lesen kann, und pmxcfs gibt dir in /etc/pve genau einen Ort, an dem das nicht geht6:
| Wo die Tabelle liegen könnte | Wer sie lesen kann | Auf jedem Knoten, auf den die VM migrieren kann |
|---|---|---|
/etc/pve/priv | nur root | ja |
irgendwo sonst in /etc/pve | für die Gruppe lesbar, www-data der Weboberfläche eingeschlossen | ja |
| ein Verzeichnis auf einem Knoten | was immer du einstellst | nein |
Mit 85 Byte ist sie nirgends in der Nähe der Dateigrenze von 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 ersetzt die ganze Zeile.
Trägt die VM schon SMBIOS- oder Firmware-Optionen in args, schreib sie alle zusammen aus.
Und weil args nur root setzen darf, ist das eine Aufgabe für deine Provisionierung, nichts, was ein Kunde aus der Weboberfläche tun kann.
So herum ist es richtig.
Der Kunde liefert die Datei, deine Werkzeuge prüfen sie und hängen sie an.
Die Tabelle bewegt einen Schlüssel, kein Recht
Hier braucht die Funktion einen klaren Kopf. Die MSDM-Tabelle in eine VM zu bringen bewegt den Schlüssel. Nicht die Lizenz.
Microsofts eigene Bedingungen sagen das, und die Zeilen, auf die es ankommt, passen in eine Tabelle:
| Fall | Was Microsoft sagt | Quelle |
|---|---|---|
| OEM-Lizenz, Umzug | an einen anderen Nutzer übertragbar „only with the licensed device“ | OEM-Lizenzbedingungen8 |
| OEM-Lizenz, wie viele Installationen | „only one instance of the software for use on one device, whether that device is physical or virtual“ | OEM-Lizenzbedingungen8 |
| OEM-Lizenz, in einfachen Worten | „locked“ an den ursprünglichen PC „and cannot be transferred to any other PC“ | Microsofts Blog für kleine Unternehmen9 |
| Die Ausnahme | die Übertragungsbestimmungen „do not apply“, wenn die Software in Deutschland oder einem von mehreren anderen Ländern erworben wurde | OEM-Lizenzbedingungen8 |
| Für Kunden hosten | das Services Provider License Agreement ist für „hosted applications to end customers“ | SPLA10 |
| Desktop-Editionen als gehostete VMs | „VMs must be hosted by a Qualified Multitenant Hoster (QMTH)“ | Microsoft Learn11 |
Die Tabelle kommt immer von der eigenen lizenzierten Maschine des Kunden, nie von deinen Hosts. Der Fall, der klar innerhalb des Wortlauts liegt, ist der einfachste: die Maschine, mit der die Lizenz verkauft wurde, jetzt mit Proxmox, und dieselbe Kopie des Fisher-Price OS (Windows) in eine einzige VM darauf umgezogen. Das ist immer noch eine Instanz auf einem Gerät, und die OEM-Bedingungen erlauben das „whether that device is physical or virtual“8. Eine zweite VM, oder eine andere Maschine, und es ist das nicht mehr.
In diesem einen Fall sind die Maschine des Kunden und der Hypervisor dieselbe Kiste, es gibt also nichts hochzuladen und nichts zu kopieren.
Der Proxmox-Kernel legt die MSDM-Tabelle der Firmware schon unter /sys/firmware/acpi/tables/ offen, nur für root lesbar, und QEMU läuft unter Proxmox als root, kann die Tabelle also direkt von dort nehmen:
qm set 100 --args "-acpitable file=/sys/firmware/acpi/tables/MSDM"
Auf die Live-Tabelle zu zeigen statt auf eine Kopie hat zwei Folgen.
Der Pfad wird auf dem Knoten gelesen, auf dem die VM startet, das gehört also auf einen einzelnen Host und nicht in einen Cluster: Migrierst du die VM, bekommt sie entweder die Tabelle dieses Knotens, eine andere Lizenz auf einer anderen Maschine, oder sie startet gar nicht, weil QEMU eine fehlende Datei mit can't open file … No such file or directory ablehnt.
Und es ist eine Zeile, also ist es eine Zeile, die man in eine zweite VM einfügen kann.
Diese zweite VM ist der Fall, den die Bedingungen nicht abdecken, also bekommt eine VM es und keine andere.
Bau die Funktion also. Der Mechanismus ist solide, und es gibt Kunden, die ihn nutzen dürfen; einer, der seine Lizenz in Deutschland gekauft hat, kann durchaus darunter sein. Aber leg die Berechtigung dorthin, wo sie hingehört: in deine Nutzungsbedingungen. Der Kunde sichert zu, dass er eine Lizenz hat, die den Betrieb in deiner VM abdeckt, und er tut das, bevor der Upload-Knopf irgendetwas tut. Der Validator beweist, dass die Tabelle wohlgeformt ist. Nichts beweist, dass er sie benutzen darf.
Volumenlizenzen kommen nie in die Nähe der Tabelle
Kann ein Kunde mit einer Volumenlizenz denselben Weg nehmen? Nein. Es gibt nichts, was die Tabelle tragen könnte.
MSDM gehört zum OEM-Kanal und sonst nirgends hin. Microsofts eigener Planungsleitfaden sagt, OEM-Aktivierung „is available only for computers that are purchased through OEM channels and have the Windows operating system preinstalled“12. Volumenlizenzen aktivieren über eines von drei anderen Modellen, „Multiple Activation Keys (MAK)“, „KMS“ und „Active Directory-based activation“12, und bei jedem davon wird der Schlüssel im Betriebssystem installiert. Nichts liest ihn aus der Firmware.
| Methode | Wo der Schlüssel lebt | Mit wem der Gast spricht | In einer Proxmox-VM |
|---|---|---|---|
| OEM, OA 3.0 | die MSDM-Tabelle in der Firmware | Microsofts Aktivierungsserver | der Weg über -acpitable oben |
| MAK | im Gast installiert | Microsoft, einmal, gezählt gegen die Aktivierungen des Schlüssels | geht mit Internetzugang oder per Telefon |
| KMS | ein generischer Schlüssel (GVLK) im Gast | der KMS-Host des Kunden auf TCP 1688 | braucht eine Route dorthin, und 25 Clients oder 5 Server, bevor er irgendetwas aktiviert |
| Active Directory | ein GVLK im Gast | die Domänencontroller des Kunden, mindestens alle 180 Tage | braucht die VM in seiner Domäne |
| AVMA | ein generischer Schlüssel im Gast | die eigene Datacenter-Lizenz des Hyper-V-Hosts | gar nicht verfügbar |
Für einen Volumenkunden gibt es auf dem Hypervisor also nichts zu bauen. Sein Schlüssel kommt in sein Image, und dein Teil ist der Netzwerkpfad: Port 1688 zu seinem KMS-Host, oder ein VPN zu seinen Domänencontrollern. Der MSDM-Platz bleibt leer, und so soll es sein.
Zwei Dinge erwischen die Leute. Der generische Schlüssel auf Volumenmedien aktiviert für sich allein nichts, denn „the GVLK doesn’t work unless a valid KMS host key can be found“12, eine VM, die aus dem Enterprise-ISO des Kunden ohne Route nach Hause gebaut wurde, bleibt also unaktiviert. Und eine Desktop-Volumenlizenz ist für sich allein keine Lizenz. Volumenprogramme „cover upgrades to Windows client operating systems only“, und „an existing retail or OEM operating system license is needed for each computer“12. Die Volumenlizenz sitzt auf einer Basislizenz, sehr oft derselben OEM-Lizenz, um die es im letzten Abschnitt ging, und ob sie in deiner VM laufen darf, ist immer noch die Hosting-Frage aus dieser Tabelle, keine Aktivierungsfrage.
AVMA verdient eine eigene Zeile, weil es so aussieht, als wäre es die Antwort. Es ist Microsofts Weg, wie ein Host seine eigenen Server-Gäste aktiviert, indem er „the VM activation to the licensed virtualization host“ bindet, und es verlangt „a Windows Server Datacenter edition with the Hyper-V server host role installed“13. Microsoft sind beim Rest deutlich: „AVMA doesn’t work with other server virtualization technologies“13. Unter Proxmox aktivieren Server-Gäste über deine SPLA-Schlüssel oder über den eigenen KMS des Kunden.
Nichts im Stapel prüft die Papiere
Jede Schicht hier tut ihre Arbeit und nichts darüber hinaus. Die Firmware speichert einen Schlüssel. QEMU kopiert die Byte und räumt den Header auf. Der Gast liest den Schlüssel und aktiviert damit. Keine davon kann sagen, ob die Maschine darunter die ist, mit der die Lizenz verkauft wurde, und keine davon sollte das je können.
Die Prüfung landet also bei dem, der den Hypervisor betreibt. Eine Lizenz ist eine Vereinbarung zwischen dem, der sie gekauft hat, und Microsoft, und deine Plattform ist keine Partei davon, aber sie ist das, was den Schlüssel bewegt, und das macht die Frage zu deiner, ob du sie haben wolltest oder nicht.
Schreib die Antwort auf, bevor der Upload-Knopf irgendetwas tut. Eine Maschine, eine VM, die eigene Lizenz des Kunden und sein Wort, dass sie das abdeckt. Kostet nix, und es schützt dich genauso wie ihn. Die Software bewegt jeden Schlüssel, den man ihr gibt. Ob sie das hätte tun sollen, ist eine Frage, die nur ein Mensch beantworten kann, also sorg dafür, dass einer es tut.
Microsoft Learn, OA 3.0 on the factory floor — „injects the product keys into the firmware“;
/Validateprüft, „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) — Tabelle 2, Offset 36: „Proprietary data structure that contains all the licensing data necessary to enable Windows activation.“ ↩︎
QEMU, System Emulation, Invocation —
-smbios-Felder pro Typ;-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“ ist einwarn_report, danach wird die Länge überschrieben und die Prüfsumme neu berechnet. ↩︎pve-cluster,
src/pmxcfs/pmxcfs.c— Pfade unterprivwerden auf0777700maskiert, alles andere auf0777750. ↩︎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“; die Übertragungsbestimmungen „do not apply if you acquired the software in Germany“ oder in den aufgeführten Ländern. Archivierte Kopie; die Live-Seite verweigert automatisierte Anfragen. ↩︎ ↩︎ ↩︎ ↩︎
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 — die drei Modelle der Volumenaktivierung; die KMS-Schwellen von „at least five computers“ für Server und „at least 25 computers“ für Clients, auf TCP-Port 1688; Active Directory-based Activation braucht die Domäne „at least once every 180 days“; „the GVLK doesn’t work unless a valid KMS host key can be found“; Volumenlizenzen „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.“ ↩︎ ↩︎