fix(wlan-setup): schema-valid Windows profile, profiles built with serializers #1572

Merged
k.renner merged 4 commits from fix/wlan-setup-windows-schema into main 2026-11-05 14:29:39 +00:00
Member

Windows Settings rejected the Windows profile as invalid provisioning XML.

  • The CarrierControl WLAN schema allows authentication only up to WPA2; WPA3ENT is not in it (WLAN schema). The profile now uses WPA2 (Enterprise); Windows adds the PMF the WPA3-Enterprise network requires.
  • Otherwise aligned with Microsoft's signed Passpoint provisioning samples: Hotspot2 with DomainName only, <?xml version="1.0"?> in front.
  • All profiles are built with serializers instead of strings:
    • Android: PPS MO from encoding/xml structs, MIME multipart via mime/multipart (boundary renner-passpoint-profile; Android's ConfigParser reads part headers into a map).
    • Apple: .mobileconfig via howett.net/plist.
    • Windows: etree document; signature as an etree subtree, digest and SignedInfo canonicalized with goxmldsig's C14N 1.0 canonicalizer. Same form as Microsoft's samples (one transform, rsa-sha1, sha1 digest, chain in KeyInfo).
  • Tests parse every output (multipart reader, encoding/xml, plist, etree) and verify the Windows signature with goxmldsig.

🤖 Generated with Claude Code

Windows Settings rejected the Windows profile as invalid provisioning XML. - The CarrierControl WLAN schema allows `authentication` only up to `WPA2`; `WPA3ENT` is not in it ([WLAN schema](https://learn.microsoft.com/en-us/uwp/schemas/mobilebroadbandschema/wlan/schema-root)). The profile now uses `WPA2` (Enterprise); Windows adds the PMF the WPA3-Enterprise network requires. - Otherwise aligned with Microsoft's signed Passpoint provisioning samples: `Hotspot2` with `DomainName` only, `<?xml version="1.0"?>` in front. - All profiles are built with serializers instead of strings: - Android: PPS MO from `encoding/xml` structs, MIME multipart via `mime/multipart` (boundary `renner-passpoint-profile`; Android's ConfigParser reads part headers into a map). - Apple: `.mobileconfig` via `howett.net/plist`. - Windows: etree document; signature as an etree subtree, digest and SignedInfo canonicalized with goxmldsig's C14N 1.0 canonicalizer. Same form as Microsoft's samples (one transform, rsa-sha1, sha1 digest, chain in KeyInfo). - Tests parse every output (multipart reader, encoding/xml, plist, etree) and verify the Windows signature with goxmldsig. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-11-05 14:07:25 +00:00
fix(wlan-setup): Windows provisioning XML within the CarrierControl schema
ci/woodpecker/pr/wlan-setup-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
b6acbc6e79
Windows Settings rejected the profile as invalid provisioning XML. The
CarrierControl WLAN schema allows authentication only up to WPA2; WPA3ENT is
not in it. The profile now uses WPA2 (Enterprise; Windows adds the PMF the
WPA3-Enterprise network requires) and follows Microsoft's signed Passpoint
samples otherwise: Hotspot2 with DomainName only, XML declaration in front
(outside the root, so not part of the digest).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot changed title from fix(wlan-setup): Windows provisioning XML within the CarrierControl schema to fix(wlan-setup): schema-valid Windows profile, profiles built with serializers 2026-11-05 14:18:18 +00:00
claude-bot added 1 commit 2026-11-05 14:18:18 +00:00
refactor(wlan-setup): build profiles with XML and plist serializers
ci/woodpecker/pr/wlan-setup-test Pipeline was canceled
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
28e864cfc4
All three profiles were assembled from strings. They are now typed
documents:

- Android: PPS MO from encoding/xml structs; MIME multipart with
  mime/multipart (boundary renner-passpoint-profile; Android's ConfigParser
  reads part headers into a map, so their order does not matter).
- Apple: .mobileconfig marshalled with howett.net/plist.
- Windows: CarrierProvisioning built as an etree document; the signature is
  an etree subtree, digest and SignedInfo canonicalized with goxmldsig's
  C14N 1.0 canonicalizer (SignedInfo in place, so it carries the inherited
  xmldsig namespace). Same form as Microsoft's samples: one transform,
  rsa-sha1, sha1 digest, chain in KeyInfo.

Tests parse the outputs (multipart reader, encoding/xml, plist, etree) and
verify the Windows signature with goxmldsig.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot added 2 commits 2026-11-05 14:18:45 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into fix/wlan-setup-windows-schema
ci/woodpecker/pr/wlan-setup-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
9a1dc89326
k.renner requested review from review-bot 2026-11-05 14:24:01 +00:00
review-bot approved these changes 2026-11-05 14:28:30 +00:00
review-bot left a comment
Member

🤖 Automated review — model claude-opus-5-5, commit 9a1dc89326e9944019af2b8cab1c7d69f0cc78ec.

Automated review by the review-bot: it approves only when it found nothing red or yellow, and a human still merges. Request a review from review-bot again for a fresh one.


Summary

This PR fixes the Windows provisioning profile so it validates against the schema: authentication changes from WPA3ENT to WPA2, NAIRealm is dropped from Hotspot2, and an <?xml version="1.0"?> declaration now comes first. It also replaces the hand-built profile strings with real serializers: encoding/xml and mime/multipart for Android, howett.net/plist for Apple, and etree plus goxmldsig C14N for the Windows document and its signature. The change looks correct to me. Head CI (wlan-setup-test) is green, and the tests now parse every profile and check the Windows signature with goxmldsig's validator. The PR is mergeable, and the only other open PR (#1534, YouTrack) doesn't touch wlan-setup/.

⚪ Unverified

  • Android device acceptance. mime/multipart writes part headers in sorted order, so Content-Transfer-Encoding now comes before Content-Type. It also leaves the boundary parameter unquoted (multipart/mixed; boundary=renner-passpoint-profile) and adds CRLFs around boundaries differently from the old literal. The PR says Android's ConfigParser reads part headers into a map, so order shouldn't matter. I did not read AOSP to confirm this, and the tests only parse the output with Go's own reader. Worth one install on a real Android device.
  • Windows behaviour with WPA2 on a WPA3-Enterprise network. The claim that "Windows adds the PMF" when the profile says WPA2 and the network is WPA3-Enterprise describes current Windows behaviour. I can't check that from the repo. Same for dropping NAIRealm, which matches the PR's account of Microsoft's samples. Only a real Windows install will confirm both.
  • SignedInfo canonicalization. The comment at wlan-setup/sign.go ("In place, the canonicalizer adds the namespace SignedInfo inherits from Signature") depends on how goxmldsig's internals behave. Upstream goxmldsig's own signer uses etreeutils.NSDetatch before canonicalizing SignedInfo. Here, TestCallbackWindows and TestWindowsProfileDocument run ctx.Validate (server_test.go:550-557) and pass in CI, so the signature does verify with goxmldsig. Whether Windows' verifier agrees can only be shown on a device.

🟡 Suggestions

  • wlan-setup/sign.go (Sign): the function now changes the caller's *etree.Document by appending Signature to it. Today's only caller (server.go windows case) passes a fresh document, so this works. A line in the doc comment saying doc is modified would stop a future caller from reusing it. The test that signing twice fails partly covers this already.

🟢 Nits

  • wlan-setup/sign.go: const dsigNS = dsig.Namespace is now just an alias. Using dsig.Namespace directly would be slightly clearer.
  • wlan-setup/go.sum gains go.mod-only lines for go-flags, check.v1 and yaml.v1, which come in through howett.net/plist. This is expected; I'm only noting it.

Tests

Test coverage is good. The Android test parses the MIME envelope and parts and walks the PPS MO tree. The Apple test round-trips the plist, compares it with reflect.DeepEqual and checks that the password is XML-escaped. The Windows tests check the XML declaration, element values, namespaces, signature algorithms, the transform count and the certificate chain order, and confirm the digest equals the C14N output of the unsigned document and that re-signing is rejected. The remaining gap is device-level acceptance, which unit tests can't cover.

Recommendation

Looks reasonable — no blocking issues; comments above are optional.

🤖 **Automated review** — model `claude-opus-5-5`, commit `9a1dc89326e9944019af2b8cab1c7d69f0cc78ec`. > Automated review by the `review-bot`: it approves only when it found nothing red or yellow, and a human still merges. Request a review from `review-bot` again for a fresh one. --- ## Summary This PR fixes the Windows provisioning profile so it validates against the schema: `authentication` changes from `WPA3ENT` to `WPA2`, `NAIRealm` is dropped from `Hotspot2`, and an `<?xml version="1.0"?>` declaration now comes first. It also replaces the hand-built profile strings with real serializers: `encoding/xml` and `mime/multipart` for Android, `howett.net/plist` for Apple, and etree plus goxmldsig C14N for the Windows document and its signature. The change looks correct to me. Head CI (`wlan-setup-test`) is green, and the tests now parse every profile and check the Windows signature with goxmldsig's validator. The PR is mergeable, and the only other open PR (#1534, YouTrack) doesn't touch `wlan-setup/`. ## ⚪ Unverified - **Android device acceptance.** `mime/multipart` writes part headers in sorted order, so `Content-Transfer-Encoding` now comes before `Content-Type`. It also leaves the boundary parameter unquoted (`multipart/mixed; boundary=renner-passpoint-profile`) and adds CRLFs around boundaries differently from the old literal. The PR says Android's ConfigParser reads part headers into a map, so order shouldn't matter. I did not read AOSP to confirm this, and the tests only parse the output with Go's own reader. Worth one install on a real Android device. - **Windows behaviour with `WPA2` on a WPA3-Enterprise network.** The claim that "Windows adds the PMF" when the profile says `WPA2` and the network is WPA3-Enterprise describes current Windows behaviour. I can't check that from the repo. Same for dropping `NAIRealm`, which matches the PR's account of Microsoft's samples. Only a real Windows install will confirm both. - **SignedInfo canonicalization.** The comment at `wlan-setup/sign.go` ("In place, the canonicalizer adds the namespace SignedInfo inherits from Signature") depends on how goxmldsig's internals behave. Upstream goxmldsig's own signer uses `etreeutils.NSDetatch` before canonicalizing SignedInfo. Here, `TestCallbackWindows` and `TestWindowsProfileDocument` run `ctx.Validate` (`server_test.go:550-557`) and pass in CI, so the signature does verify with goxmldsig. Whether Windows' verifier agrees can only be shown on a device. ## 🟡 Suggestions - `wlan-setup/sign.go` (`Sign`): the function now changes the caller's `*etree.Document` by appending `Signature` to it. Today's only caller (`server.go` windows case) passes a fresh document, so this works. A line in the doc comment saying `doc` is modified would stop a future caller from reusing it. The test that signing twice fails partly covers this already. ## 🟢 Nits - `wlan-setup/sign.go`: `const dsigNS = dsig.Namespace` is now just an alias. Using `dsig.Namespace` directly would be slightly clearer. - `wlan-setup/go.sum` gains `go.mod`-only lines for `go-flags`, `check.v1` and `yaml.v1`, which come in through `howett.net/plist`. This is expected; I'm only noting it. ## Tests Test coverage is good. The Android test parses the MIME envelope and parts and walks the PPS MO tree. The Apple test round-trips the plist, compares it with `reflect.DeepEqual` and checks that the password is XML-escaped. The Windows tests check the XML declaration, element values, namespaces, signature algorithms, the transform count and the certificate chain order, and confirm the digest equals the C14N output of the unsigned document and that re-signing is rejected. The remaining gap is device-level acceptance, which unit tests can't cover. ## Recommendation Looks reasonable — no blocking issues; comments above are optional. <!-- pr-review:sha=9a1dc89326e9944019af2b8cab1c7d69f0cc78ec -->
k.renner merged commit a5be653044 into main 2026-11-05 14:29:39 +00:00
Sign in to join this conversation.