From 55fce76a2d80f4345a4ec67e43f154fac7b6a0f4 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 5 Nov 2026 16:16:06 +0100 Subject: [PATCH] fix(wlan-setup): Windows profile valid against the provisioning schemas Windows Settings rejected the provisioning XML: Global/SubscriberId is base:SubscriberType (\w+, at most 20 characters) in the schemas Windows ships in %SYSTEMROOT%\schemas\provisioning, and "renner-unifi" contains a hyphen. It is now "rennerunifi". Those schemas also allow WPA3ENT and Hotspot2/NAIRealm, which the online schema reference lacks: the profile uses WPA3ENT again (the network is WPA3-Enterprise) and carries NAI realm renner.uno next to the domain. TestWindowsProfileSchema validates the signed document with xmllint against a copy of that schema folder (WLAN_SETUP_PROVISIONING_XSD); without it the test is skipped, the schemas are Microsoft's files and not in the repo. Co-Authored-By: Claude Opus 5.5 --- wlan-setup/profiles.go | 16 +++++----- wlan-setup/server_test.go | 61 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/wlan-setup/profiles.go b/wlan-setup/profiles.go index a78a6dc8..e5bf9da9 100644 --- a/wlan-setup/profiles.go +++ b/wlan-setup/profiles.go @@ -200,10 +200,10 @@ func addText(parent *etree.Element, tag, ns, text string) *etree.Element { } // windowsProfile is the unsigned CarrierProvisioning document: one Hotspot 2.0 WLAN profile -// matched by domain name, EAP-TTLS with inner PAP, and the user's credentials. The -// CarrierControl WLAN schema allows authentication up to WPA2 (Enterprise); Windows adds the -// PMF that the WPA3-Enterprise network requires. Elements follow Microsoft's signed Passpoint -// provisioning samples. +// (domain and NAI realm renner.uno), WPA3-Enterprise, EAP-TTLS with inner PAP, and the user's +// credentials. Valid against the schemas Windows ships in %SYSTEMROOT%\schemas\provisioning +// (TestWindowsProfileSchema); SubscriberId is base:SubscriberType, i.e. \w+ and at most 20 +// characters. func windowsProfile(user, password string) *etree.Document { doc := etree.NewDocument() doc.WriteSettings = etree.WriteSettings{CanonicalEndTags: true, CanonicalText: true, CanonicalAttrVal: true} @@ -213,16 +213,18 @@ func windowsProfile(user, password string) *etree.Document { global := add(root, "Global", "") addText(global, "CarrierId", "", carrierID) - addText(global, "SubscriberId", "", "renner-unifi") + addText(global, "SubscriberId", "", "rennerunifi") wlan := add(add(root, "WLANProfiles", ""), "WLANProfile", nsWLAN) addText(wlan, "name", "", "Renner UniFi") addText(add(add(wlan, "SSIDConfig", ""), "SSID", ""), "name", "", "Renner UniFi") - addText(add(wlan, "Hotspot2", ""), "DomainName", "", "renner.uno") + hotspot2 := add(wlan, "Hotspot2", "") + addText(hotspot2, "DomainName", "", "renner.uno") + addText(add(hotspot2, "NAIRealm", ""), "name", "", "renner.uno") security := add(add(wlan, "MSM", ""), "security", "") auth := add(security, "authEncryption", "") - addText(auth, "authentication", "", "WPA2") + addText(auth, "authentication", "", "WPA3ENT") addText(auth, "encryption", "", "AES") addText(auth, "useOneX", "", "true") diff --git a/wlan-setup/server_test.go b/wlan-setup/server_test.go index 370bc141..2b03b84a 100644 --- a/wlan-setup/server_test.go +++ b/wlan-setup/server_test.go @@ -22,6 +22,7 @@ import ( "net/textproto" "net/url" "os" + "os/exec" "path/filepath" "reflect" "regexp" @@ -644,6 +645,61 @@ func TestCallbackWindows(t *testing.T) { } } +// TestWindowsProfileSchema validates the signed document with xmllint against the schemas +// Windows ships in %SYSTEMROOT%\schemas\provisioning. They are Microsoft's files and not in the +// repo: point WLAN_SETUP_PROVISIONING_XSD at a copy of that folder to run it. +func TestWindowsProfileSchema(t *testing.T) { + dir := os.Getenv("WLAN_SETUP_PROVISIONING_XSD") + if dir == "" { + t.Skip("WLAN_SETUP_PROVISIONING_XSD not set") + } + xmllint, err := exec.LookPath("xmllint") + if err != nil { + t.Skip("xmllint not installed") + } + tc := writeSigningFiles(t) + signer, err := loadSigner(tc.keyFile, tc.certFile) + if err != nil { + t.Fatal(err) + } + body, err := signer.Sign(windowsProfile("Alice.Smith", testKey)) + if err != nil { + t.Fatal(err) + } + + // The schemas import each other by namespace only; a wrapper schema gives libxml2 the + // locations. Dependencies first, so the later imports find their namespaces loaded. + wrapper := etree.NewDocument() + schema := wrapper.CreateElement("xs:schema") + schema.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema") + schema.CreateAttr("targetNamespace", "urn:wlan-setup:schema-test") + for _, s := range []struct{ ns, file string }{ + {"http://www.microsoft.com/networking/CarrierControl/Base/v1", "provisioning_base_v1.xsd"}, + {"http://www.microsoft.com/networking/CarrierControl/Plans/v1", "provisioning_plans_v1.xsd"}, + {"http://www.microsoft.com/networking/CarrierControl/WWAN/v1", "provisioning_wwan_v1.xsd"}, + {"http://www.microsoft.com/networking/CarrierControl/WLAN/v1", "provisioning_wlan_v1.xsd"}, + {dsig.Namespace, "provisioning_sig_v1.xsd"}, + {nsCarrierControl, "provisioning_v1.xsd"}, + } { + imp := schema.CreateElement("xs:import") + imp.CreateAttr("namespace", s.ns) + imp.CreateAttr("schemaLocation", "file://"+filepath.Join(dir, s.file)) + } + tmp := t.TempDir() + wrapperFile := filepath.Join(tmp, "wrapper.xsd") + if err := wrapper.WriteToFile(wrapperFile); err != nil { + t.Fatal(err) + } + docFile := filepath.Join(tmp, "profile.xml") + if err := os.WriteFile(docFile, body, 0o600); err != nil { + t.Fatal(err) + } + out, err := exec.Command(xmllint, "--noout", "--schema", wrapperFile, docFile).CombinedOutput() + if err != nil { + t.Fatalf("xmllint: %v\n%s", err, out) + } +} + func TestWindowsProfileDocument(t *testing.T) { tc := writeSigningFiles(t) signer, err := loadSigner(tc.keyFile, tc.certFile) @@ -676,11 +732,12 @@ func TestWindowsProfileDocument(t *testing.T) { ) for path, want := range map[string]string{ "Global/CarrierId": carrierID, - "Global/SubscriberId": "renner-unifi", + "Global/SubscriberId": "rennerunifi", wlan + "name": "Renner UniFi", wlan + "SSIDConfig/SSID/name": "Renner UniFi", wlan + "Hotspot2/DomainName": "renner.uno", - sec + "authEncryption/authentication": "WPA2", + wlan + "Hotspot2/NAIRealm/name": "renner.uno", + sec + "authEncryption/authentication": "WPA3ENT", sec + "authEncryption/encryption": "AES", sec + "authEncryption/useOneX": "true", sec + "OneX/authMode": "user", -- 2.54.0