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 <noreply@anthropic.com>
259 lines
12 KiB
Go
259 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/textproto"
|
|
|
|
"github.com/beevik/etree"
|
|
"howett.net/plist"
|
|
)
|
|
|
|
// isrgRootX1 is the trust anchor of the radius-tls chain (DER, base64).
|
|
const isrgRootX1 = "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc="
|
|
|
|
// carrierID identifies the Windows provisioning document; a reinstall replaces the profile.
|
|
const carrierID = "{5423c37a-7530-4ce9-8f87-a2574ea006bc}"
|
|
|
|
// mimeBoundary separates the parts of the Android profile.
|
|
const mimeBoundary = "renner-passpoint-profile"
|
|
|
|
// ppsNode is a node of the PPS MO management tree: a leaf carries a Value, an interior node
|
|
// further nodes.
|
|
type ppsNode struct {
|
|
XMLName xml.Name `xml:"Node"`
|
|
NodeName string `xml:"NodeName"`
|
|
RTProperties *ppsRTProperties `xml:"RTProperties,omitempty"`
|
|
Value *string `xml:"Value,omitempty"`
|
|
Nodes []ppsNode `xml:"Node"`
|
|
}
|
|
|
|
type ppsRTProperties struct {
|
|
DDFName string `xml:"Type>DDFName"`
|
|
}
|
|
|
|
type ppsTree struct {
|
|
XMLName xml.Name `xml:"syncml:dmddf1.2 MgmtTree"`
|
|
VerDTD string `xml:"VerDTD"`
|
|
Nodes []ppsNode `xml:"Node"`
|
|
}
|
|
|
|
func ppsLeaf(name, value string) ppsNode { return ppsNode{NodeName: name, Value: &value} }
|
|
|
|
func ppsInterior(name string, nodes ...ppsNode) ppsNode {
|
|
return ppsNode{NodeName: name, Nodes: nodes}
|
|
}
|
|
|
|
// androidProfile is the application/x-wifi-config body: base64 of a MIME multipart with the
|
|
// Passpoint R1 PPS MO and the trust anchor, each part base64.
|
|
func androidProfile(user, password string) ([]byte, error) {
|
|
subscription := ppsInterior("PerProviderSubscription",
|
|
ppsInterior("i001",
|
|
ppsInterior("HomeSP", ppsLeaf("FriendlyName", "Renner"), ppsLeaf("FQDN", "renner.uno")),
|
|
ppsInterior("Credential",
|
|
ppsLeaf("Realm", "renner.uno"),
|
|
ppsInterior("UsernamePassword",
|
|
ppsLeaf("Username", user),
|
|
ppsLeaf("Password", base64.StdEncoding.EncodeToString([]byte(password))),
|
|
ppsInterior("EAPMethod", ppsLeaf("EAPType", "21"), ppsLeaf("InnerMethod", "PAP")))),
|
|
ppsInterior("Extension", ppsInterior("Android",
|
|
ppsInterior("AAAServerTrustedNames", ppsLeaf("FQDN", "radius.renner.uno"))))))
|
|
subscription.RTProperties = &ppsRTProperties{DDFName: "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0"}
|
|
pps, err := xml.Marshal(ppsTree{VerDTD: "1.2", Nodes: []ppsNode{subscription}})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pps: %w", err)
|
|
}
|
|
|
|
var parts bytes.Buffer
|
|
mw := multipart.NewWriter(&parts)
|
|
if err := mw.SetBoundary(mimeBoundary); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, p := range []struct{ contentType, body string }{
|
|
{"application/x-passpoint-profile", base64.StdEncoding.EncodeToString(pps)},
|
|
{"application/x-x509-ca-cert", isrgRootX1},
|
|
} {
|
|
w, err := mw.CreatePart(textproto.MIMEHeader{
|
|
"Content-Type": {p.contentType},
|
|
"Content-Transfer-Encoding": {"base64"},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := w.Write([]byte(p.body)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := mw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var message bytes.Buffer
|
|
fmt.Fprintf(&message, "Content-Type: %s\r\nContent-Transfer-Encoding: base64\r\n\r\n",
|
|
mime.FormatMediaType("multipart/mixed", map[string]string{"boundary": mw.Boundary()}))
|
|
message.Write(parts.Bytes())
|
|
return []byte(base64.StdEncoding.EncodeToString(message.Bytes())), nil
|
|
}
|
|
|
|
type appleEAP struct {
|
|
UserName string
|
|
UserPassword string
|
|
OuterIdentity string
|
|
AcceptEAPTypes []int
|
|
TTLSInnerAuthentication string
|
|
TLSTrustedServerNames []string
|
|
}
|
|
|
|
type appleWiFi struct {
|
|
PayloadType string
|
|
PayloadVersion int
|
|
PayloadIdentifier string
|
|
PayloadUUID string
|
|
PayloadDisplayName string
|
|
PayloadOrganization string
|
|
IsHotspot bool
|
|
DomainName string
|
|
NAIRealmNames []string
|
|
DisplayedOperatorName string
|
|
ServiceProviderRoamingEnabled bool
|
|
AutoJoin bool
|
|
EncryptionType string
|
|
EAPClientConfiguration appleEAP
|
|
}
|
|
|
|
type appleConfiguration struct {
|
|
PayloadContent []appleWiFi
|
|
PayloadType string
|
|
PayloadVersion int
|
|
PayloadIdentifier string
|
|
PayloadUUID string
|
|
PayloadDisplayName string
|
|
PayloadOrganization string
|
|
}
|
|
|
|
// appleProfile is the .mobileconfig: com.apple.wifi.managed with Passpoint matching. Fixed
|
|
// identifiers and UUIDs, so a reinstall replaces the previous profile.
|
|
func appleProfile(user, password string) ([]byte, error) {
|
|
return plist.MarshalIndent(appleConfiguration{
|
|
PayloadContent: []appleWiFi{{
|
|
PayloadType: "com.apple.wifi.managed",
|
|
PayloadVersion: 1,
|
|
PayloadIdentifier: "uno.renner.wifi.renner-unifi.network",
|
|
PayloadUUID: "824959bf-c55d-4ed2-b569-c33f3759c2b6",
|
|
PayloadDisplayName: "Renner UniFi",
|
|
PayloadOrganization: "Renner",
|
|
IsHotspot: true,
|
|
DomainName: "renner.uno",
|
|
NAIRealmNames: []string{"renner.uno"},
|
|
DisplayedOperatorName: "Renner",
|
|
ServiceProviderRoamingEnabled: false,
|
|
AutoJoin: true,
|
|
EncryptionType: "Any",
|
|
EAPClientConfiguration: appleEAP{
|
|
UserName: user,
|
|
UserPassword: password,
|
|
OuterIdentity: "anonymous",
|
|
AcceptEAPTypes: []int{21},
|
|
TTLSInnerAuthentication: "PAP",
|
|
TLSTrustedServerNames: []string{"radius.renner.uno"},
|
|
},
|
|
}},
|
|
PayloadType: "Configuration",
|
|
PayloadVersion: 1,
|
|
PayloadIdentifier: "uno.renner.wifi.renner-unifi",
|
|
PayloadUUID: "7175a2a3-ddd3-42ea-b4ad-b7e5501892f3",
|
|
PayloadDisplayName: "WLAN Renner UniFi",
|
|
PayloadOrganization: "Renner",
|
|
}, plist.XMLFormat, "\t")
|
|
}
|
|
|
|
// Namespaces of the CarrierProvisioning document.
|
|
const (
|
|
nsCarrierControl = "http://www.microsoft.com/networking/CarrierControl/v1"
|
|
nsWLAN = "http://www.microsoft.com/networking/CarrierControl/WLAN/v1"
|
|
nsOneX = "http://www.microsoft.com/networking/OneX/v1"
|
|
nsEapHostConfig = "http://www.microsoft.com/provisioning/EapHostConfig"
|
|
nsEapCommon = "http://www.microsoft.com/provisioning/EapCommon"
|
|
nsEapTtlsConn = "http://www.microsoft.com/provisioning/EapTtlsConnectionPropertiesV1"
|
|
nsEapHostUserCred = "http://www.microsoft.com/provisioning/EapHostUserCredentials"
|
|
nsEapTtlsUser = "http://www.microsoft.com/provisioning/EapTtlsUserPropertiesV1"
|
|
)
|
|
|
|
// add appends an element named tag to parent. ns, when set, becomes its default namespace.
|
|
func add(parent *etree.Element, tag, ns string) *etree.Element {
|
|
el := parent.CreateElement(tag)
|
|
if ns != "" {
|
|
el.CreateAttr("xmlns", ns)
|
|
}
|
|
return el
|
|
}
|
|
|
|
// addText appends an element named tag with text content to parent.
|
|
func addText(parent *etree.Element, tag, ns, text string) *etree.Element {
|
|
el := add(parent, tag, ns)
|
|
el.SetText(text)
|
|
return el
|
|
}
|
|
|
|
// windowsProfile is the unsigned CarrierProvisioning document: one Hotspot 2.0 WLAN profile
|
|
// (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}
|
|
doc.CreateProcInst("xml", `version="1.0"`)
|
|
doc.CreateText("\n")
|
|
root := add(&doc.Element, "CarrierProvisioning", nsCarrierControl)
|
|
|
|
global := add(root, "Global", "")
|
|
addText(global, "CarrierId", "", carrierID)
|
|
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")
|
|
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", "", "WPA3ENT")
|
|
addText(auth, "encryption", "", "AES")
|
|
addText(auth, "useOneX", "", "true")
|
|
|
|
oneX := add(security, "OneX", nsOneX)
|
|
addText(oneX, "authMode", "", "user")
|
|
eapHost := add(add(oneX, "EAPConfig", ""), "EapHostConfig", nsEapHostConfig)
|
|
method := add(eapHost, "EapMethod", "")
|
|
addText(method, "Type", nsEapCommon, "21")
|
|
addText(method, "VendorId", nsEapCommon, "0")
|
|
addText(method, "VendorType", nsEapCommon, "0")
|
|
addText(method, "AuthorId", nsEapCommon, "311")
|
|
ttls := add(add(eapHost, "Config", ""), "EapTtls", nsEapTtlsConn)
|
|
validation := add(ttls, "ServerValidation", "")
|
|
addText(validation, "ServerNames", "", "radius.renner.uno")
|
|
addText(validation, "TrustedRootCAHash", "", "ca bd 2a 79 a1 07 6a 31 f2 1d 25 36 35 cb 03 9d 43 29 a5 e8")
|
|
addText(validation, "DisablePrompt", "", "true")
|
|
add(add(ttls, "Phase2Authentication", ""), "PAPAuthentication", "")
|
|
identity := add(ttls, "Phase1Identity", "")
|
|
addText(identity, "IdentityPrivacy", "", "true")
|
|
addText(identity, "AnonymousIdentity", "", "anonymous")
|
|
|
|
userCred := add(security, "EapHostUserCredentials", nsEapHostUserCred)
|
|
userCred.CreateAttr("xmlns:eapCommon", nsEapCommon)
|
|
userMethod := add(userCred, "EapMethod", "")
|
|
addText(userMethod, "eapCommon:Type", "", "21")
|
|
addText(userMethod, "eapCommon:AuthorId", "", "311")
|
|
credentials := add(add(userCred, "Credentials", ""), "EapTtls", nsEapTtlsUser)
|
|
addText(credentials, "Username", "", user)
|
|
addText(credentials, "Password", "", password)
|
|
return doc
|
|
}
|