diff --git a/src/content/docs/networking/eduroam-network-installation.md b/src/content/docs/networking/eduroam-network-installation.md index f9f706d..ba14baa 100644 --- a/src/content/docs/networking/eduroam-network-installation.md +++ b/src/content/docs/networking/eduroam-network-installation.md @@ -10,7 +10,9 @@ Getting eduroam to work on Linux is more painful than it should be. Every "offic ## What doesn't work {{% details title="cat.eduroam.org installer (official)" closed="true" %}} -The Python installer from [cat.eduroam.org](https://cat.eduroam.org/) provides a graphical interface and creates a connection profile. On some recent Linux distributions, the connection may hang during the TLS handshake due to changes in NetworkManager. +The Python installer from [cat.eduroam.org](https://cat.eduroam.org/) provides a graphical interface and creates a connection profile. It reports "Installation successful" without ever attempting a connection, and the connection then hangs indefinitely during the TLS handshake. + +The cause is not NetworkManager: the CA embedded in Saxion's CAT profile is the pre-migration USERTrust / GEANT OV RSA CA 4 chain, while the RADIUS server now chains to HARICA roots. Validation cannot succeed. See [#109](https://github.com/THectic-NL/Zephyrus-Linux/issues/109) for the fingerprints and handshake logs. ![cat.eduroam.org download portal for Saxion](/images/eduroam-cat-portal.avif) {{% /details %}} @@ -36,18 +38,72 @@ script, plus `domain-suffix-match` (the modern replacement for the deprecated The script used to point at the system trust store, which meant any of the roughly 150 public CAs your distribution ships could vouch for a server calling itself -`ise.infra.saxion.net`. It now trusts only the HARICA roots that Saxion's RADIUS server -actually chains to — Hellenic Academic and Research Institutions RootCA 2015 and HARICA -TLS RSA Root CA 2021 — which is what the official CAT installers do. +`ise.infra.saxion.net`. It now trusts four HARICA roots and nothing else: + +| Root | Key | Expires | +|------|-----|---------| +| Hellenic Academic and Research Institutions RootCA 2015 | RSA | 2040 | +| HARICA TLS RSA Root CA 2021 | RSA | 2045 | +| Hellenic Academic and Research Institutions ECC RootCA 2015 | ECC | 2040 | +| HARICA TLS ECC Root CA 2021 | ECC | 2045 | + +The RSA pair is what the server serves today, and both members of it are pinned for a +reason. The server currently chains through the *cross-signed* 2021 root up to the 2015 +root, but HARICA publishes that cross certificate as valid only until **2029-08-31**. +After that date the chain has to terminate at the self-signed 2021 root, which is already +pinned here and already what OpenSSL terminates on today. + +The ECC pair covers a move off RSA. HARICA's repository already lists +`HARICA GEANT TLS ECC 1` (2025) among its intermediates, so that path exists. All four +are HARICA roots, so this stays one CA operator. + +| Date | What happens | +|---|---| +| 2029-08-31 | Cross certificate expires; chain must terminate at the self-signed 2021 root | +| 2040-06-30 | Both 2015 roots expire | +| 2045-02-13 | Both 2021 roots expire | + +Fingerprints last checked against HARICA's repository on **2026-08-31**. + +#### Verify the pinned roots yourself + +Don't take this page's word for it. HARICA publishes the fingerprints of its own roots +at [repo.harica.gr](https://repo.harica.gr/rep_dyn.php). Pick the root from the +dropdown and compare its SHA-1: + +| Entry in HARICA's repository | SHA-1 fingerprint | +|---|---| +| HARICA Root Certification Authority, 2015 | `01:0C:06:95:A6:98:19:14:FF:BF:5F:C6:B0:B6:95:EA:29:E9:12:A6` | +| HARICA TLS RSA Root CA 2021, 2021 | `02:2D:05:82:FA:88:CE:14:0C:06:79:DE:7F:14:10:E9:45:D7:A5:6D` | +| HARICA ECC Root Certification Authority, 2015 | `9F:F1:71:8D:92:D5:9A:F3:7D:74:97:B4:BC:6F:84:68:0B:BA:B6:66` | +| HARICA TLS ECC Root CA 2021, 2021 | `BC:B0:C1:9D:E9:98:92:70:19:38:57:E9:8D:A7:B4:5D:6E:EE:01:48` | -GÉANT moved its Trusted Certificate Service to HARICA, so an earlier version of this -script pinned the pre-migration USERTrust chain and every connection failed with -`unknown CA`. If Saxion changes certificate authority again the same thing will happen; -the script now says so explicitly instead of hanging. +To check what the script actually installed on your machine: + +```bash +awk '/BEGIN CERT/,/END CERT/' ~/.config/saxion-eduroam/saxion-eduroam-ca.pem | + csplit -zs -f /tmp/root- -b '%d.pem' - '/BEGIN CERT/' '{*}' +for f in /tmp/root-*.pem; do + openssl x509 -in "$f" -noout -subject -fingerprint -sha1 +done +``` + +Every fingerprint printed must appear in the table above. If one does not, do not use the +script. Open an issue instead. + +This is the same check we run: nothing is pinned because a handshake offered it, only +because the CA operator publishes it. + +GÉANT moved its Trusted Certificate Service to HARICA, and the official CAT profile +still pins the pre-migration USERTrust chain, which is why the official installer +fails. If Saxion changes CA operator again this script will break too, but it now +prints the chain the server actually served instead of hanging silently. **Requirements:** -- Python 3.10+ +- Python 3.11+ (standard library only, no `pip install`, no `dbus-python`) - NetworkManager 1.8+ (`nmcli`) +- Optional: `zenity` (GNOME) or `kdialog` (KDE) for graphical prompts; falls back to the terminal +- Optional: access to the system journal, used to explain certificate failures ### Connection settings @@ -57,7 +113,7 @@ the script now says so explicitly instead of hanging. | Authentication | Protected EAP (PEAP) | | PEAP version | Automatic | | Inner authentication | MSCHAPv2 | -| CA certificate | Saxion's published chain, written to `~/.config/saxion-eduroam/saxion-eduroam-ca.pem` | +| CA certificate | The HARICA roots the server chains to, written to `~/.config/saxion-eduroam/saxion-eduroam-ca.pem` | | Domain validation | `domain-suffix-match: ise.infra.saxion.net` | | Phase2 domain validation | `phase2-domain-suffix-match: ise.infra.saxion.net` | | Anonymous identity | `anonymous@saxion.nl` | @@ -72,7 +128,7 @@ A Python script automates the full `nmcli` connection setup for Saxion: curl -LO https://zephyrus-linux.stensel.nl/scripts/saxion-eduroam.py # 2. Verify checksum -echo "447a0979166cc801ba7406cc660b0403156532862ac031835291bb9d721f33e1 saxion-eduroam.py" | sha256sum -c +echo "17cd13c629ce480ece1a7896aff7d4061347ea0082b32dfa6b23dac6b34882ad saxion-eduroam.py" | sha256sum -c # 3. Run python3 saxion-eduroam.py @@ -81,7 +137,7 @@ python3 saxion-eduroam.py #### When the certificate stops matching The trusted chain is pinned inside the script, so it breaks the day Saxion -changes certificate authority — which is exactly what happened in +changes certificate authority. That is exactly what happened in [#109](https://github.com/THectic-NL/Zephyrus-Linux/issues/109). If the script reports `unknown CA` or fails to authenticate, `--ignore-certificate` connects without validating and prints the chain the server actually served: @@ -95,13 +151,21 @@ reconnect without the flag. **Do not leave this on.** Without validation, any access point calling itself `eduroam` is trusted. It can terminate the TLS tunnel itself and capture the -MSCHAPv2 exchange, which is crackable offline — that is your Saxion password. +MSCHAPv2 exchange, which is crackable offline. That is your Saxion password. `domain-suffix-match` does not help here: it checks the name on a certificate nobody verified. Use the flag to diagnose, then reconnect properly. -**SHA256:** `447a0979166cc801ba7406cc660b0403156532862ac031835291bb9d721f33e1` +**SHA256:** `17cd13c629ce480ece1a7896aff7d4061347ea0082b32dfa6b23dac6b34882ad` + +The script removes any existing eduroam profile, prompts for your **username** via a GUI dialog (kdialog on KDE, zenity on GNOME) or a terminal fallback, and activates the connection. Your password is never asked by the script; it is requested by your keyring (GNOME Keyring or KWallet) at connection time and stored encrypted, never in plaintext. -The script removes any existing eduroam profile, prompts for your **username** via a GUI dialog (zenity, kdialog, or yad) or terminal fallback, and activates the connection. Your password is never asked by the script; it is requested by your GNOME Keyring at connection time and stored securely, never in plaintext. +Useful flags: + +| Flag | Purpose | +|------|---------| +| `-u`, `--username` | Supply the username instead of being prompted | +| `--silent` | No dialogs; prompt and report on the terminal only | +| `--ignore-certificate` | Skip validation and print the chain the server served. Debugging only, see the warning above | {{< callout type="info" >}} This script is **Saxion-specific** and validates against Saxion's RADIUS server (`ise.infra.saxion.net`). For other institutions, use the official CAT script from [cat.eduroam.org](https://cat.eduroam.org/) as a starting point. @@ -120,7 +184,9 @@ If everything goes well, you should see something like this: ### Manual setup via nmcli {{< callout type="info" >}} -This command stores the password directly in the connection profile. The automated script above uses `password-flags 1` instead, which stores the password securely in GNOME Keyring. Both approaches work; the script's method is more secure. +This command stores the password directly in the connection profile. The automated script above uses `password-flags 1` instead, which hands the password to your keyring. Both work; the script's method is more secure. + +It also references `~/.config/saxion-eduroam/saxion-eduroam-ca.pem`, which only exists once the script has been run. Run the script first, or drop the `802-1x.ca-cert` line and accept that the chain is then unvalidated. {{< /callout >}} ```bash diff --git a/src/content/docs/networking/eduroam-network-installation.nl.md b/src/content/docs/networking/eduroam-network-installation.nl.md index 32433c1..3d66adc 100644 --- a/src/content/docs/networking/eduroam-network-installation.nl.md +++ b/src/content/docs/networking/eduroam-network-installation.nl.md @@ -10,7 +10,9 @@ eduroam werkend krijgen op Linux is pijnlijker dan het zou moeten zijn. Elke "of ## Wat niet werkt {{% details title="cat.eduroam.org installer (officieel)" closed="true" %}} -De Python-installer van [cat.eduroam.org](https://cat.eduroam.org/) biedt een grafische interface en maakt een verbindingsprofiel aan. Op sommige recente Linux-distributies kan de verbinding blijven hangen tijdens de TLS-handshake door wijzigingen in NetworkManager. +De Python-installer van [cat.eduroam.org](https://cat.eduroam.org/) biedt een grafische interface en maakt een verbindingsprofiel aan. Hij meldt "Installation successful" zonder ooit een verbinding te proberen, waarna de verbinding eindeloos blijft hangen tijdens de TLS-handshake. + +De oorzaak is niet NetworkManager: de CA in Saxion's CAT-profiel is de oude USERTrust / GEANT OV RSA CA 4 keten, terwijl de RADIUS-server inmiddels naar HARICA-roots ketent. Validatie kan dan niet slagen. Zie [#109](https://github.com/THectic-NL/Zephyrus-Linux/issues/109) voor de fingerprints en handshake-logs. ![cat.eduroam.org downloadportaal voor Saxion](/images/eduroam-cat-portal.avif) {{% /details %}} @@ -36,18 +38,72 @@ is vastgelegd, plus `domain-suffix-match` (de moderne vervanging voor het veroud Het script wees eerder naar de systeem-truststore. Daarmee kon elk van de ongeveer 150 publieke CA's die je distributie meelevert instaan voor een server die zich -`ise.infra.saxion.net` noemt. Nu worden alleen de HARICA-roots vertrouwd waar Saxion's -RADIUS-server daadwerkelijk naartoe ketent — Hellenic Academic and Research Institutions -RootCA 2015 en HARICA TLS RSA Root CA 2021 — precies wat de officiële CAT-installers doen. +`ise.infra.saxion.net` noemt. Nu worden alleen deze vier HARICA-roots vertrouwd: + +| Root | Sleutel | Verloopt | +|------|---------|----------| +| Hellenic Academic and Research Institutions RootCA 2015 | RSA | 2040 | +| HARICA TLS RSA Root CA 2021 | RSA | 2045 | +| Hellenic Academic and Research Institutions ECC RootCA 2015 | ECC | 2040 | +| HARICA TLS ECC Root CA 2021 | ECC | 2045 | + +Het RSA-paar is wat de server vandaag stuurt, en beide helften daarvan liggen niet voor +niets vast. De server ketent nu via de *cross-signed* 2021-root door naar de 2015-root, +maar HARICA publiceert dat cross-certificaat als geldig tot **2029-08-31**. Daarna moet +de keten eindigen bij de self-signed 2021-root. Die ligt hier al vast, en OpenSSL komt er +vandaag al op uit. + +Het ECC-paar dekt een overstap weg van RSA. HARICA's repository noemt +`HARICA GEANT TLS ECC 1` (2025) al bij de intermediates, dus dat pad bestaat. Alle vier +zijn HARICA-roots, dus het blijft bij één CA-operator. + +| Datum | Wat er gebeurt | +|---|---| +| 2029-08-31 | Cross-certificaat verloopt; keten moet eindigen bij de self-signed 2021-root | +| 2040-06-30 | Beide 2015-roots verlopen | +| 2045-02-13 | Beide 2021-roots verlopen | + +Fingerprints laatst gecontroleerd tegen HARICA's repository op **2026-08-31**. + +#### Controleer de vastgelegde roots zelf + +Geloof deze pagina niet op haar woord. HARICA publiceert de fingerprints van hun eigen +roots op [repo.harica.gr](https://repo.harica.gr/rep_dyn.php). Kies de root in de +dropdown en vergelijk de SHA-1: + +| Entry in HARICA's repository | SHA-1 fingerprint | +|---|---| +| HARICA Root Certification Authority, 2015 | `01:0C:06:95:A6:98:19:14:FF:BF:5F:C6:B0:B6:95:EA:29:E9:12:A6` | +| HARICA TLS RSA Root CA 2021, 2021 | `02:2D:05:82:FA:88:CE:14:0C:06:79:DE:7F:14:10:E9:45:D7:A5:6D` | +| HARICA ECC Root Certification Authority, 2015 | `9F:F1:71:8D:92:D5:9A:F3:7D:74:97:B4:BC:6F:84:68:0B:BA:B6:66` | +| HARICA TLS ECC Root CA 2021, 2021 | `BC:B0:C1:9D:E9:98:92:70:19:38:57:E9:8D:A7:B4:5D:6E:EE:01:48` | -GÉANT heeft zijn Trusted Certificate Service naar HARICA verhuisd. Een eerdere versie van -dit script legde daardoor nog de oude USERTrust-keten vast en elke verbinding faalde met -`unknown CA`. Wisselt Saxion opnieuw van certificaatautoriteit, dan gebeurt hetzelfde; -het script meldt dat nu expliciet in plaats van vast te lopen. +Nakijken wat het script daadwerkelijk op je machine heeft gezet: + +```bash +awk '/BEGIN CERT/,/END CERT/' ~/.config/saxion-eduroam/saxion-eduroam-ca.pem | + csplit -zs -f /tmp/root- -b '%d.pem' - '/BEGIN CERT/' '{*}' +for f in /tmp/root-*.pem; do + openssl x509 -in "$f" -noout -subject -fingerprint -sha1 +done +``` + +Elke fingerprint die eruit komt moet in de tabel hierboven staan. Zo niet: gebruik het +script niet, maar open een issue. + +Dit is dezelfde controle die wij doen: niets wordt vastgelegd omdat een handshake het +aanbood, alleen omdat de CA-operator het publiceert. + +GÉANT heeft zijn Trusted Certificate Service naar HARICA verhuisd, en het officiële +CAT-profiel legt nog steeds de oude USERTrust-keten vast, en daarom faalt de officiële +installer. Wisselt Saxion opnieuw van CA-operator, dan breekt dit script ook, maar het +toont dan de keten die de server werkelijk stuurde in plaats van stil vast te lopen. **Vereisten:** -- Python 3.10+ +- Python 3.11+ (alleen standaardbibliotheek, geen `pip install`, geen `dbus-python`) - NetworkManager 1.8+ (`nmcli`) +- Optioneel: `zenity` (GNOME) of `kdialog` (KDE) voor grafische dialogen; anders de terminal +- Optioneel: toegang tot de systeemjournal, om certificaatfouten te kunnen verklaren ### Verbindingsinstellingen @@ -57,7 +113,7 @@ het script meldt dat nu expliciet in plaats van vast te lopen. | Authenticatie | Protected EAP (PEAP) | | PEAP-versie | Automatisch | | Interne authenticatie | MSCHAPv2 | -| CA-certificaat | De door Saxion gepubliceerde keten, geschreven naar `~/.config/saxion-eduroam/saxion-eduroam-ca.pem` | +| CA-certificaat | De HARICA-roots waar de server naartoe ketent, geschreven naar `~/.config/saxion-eduroam/saxion-eduroam-ca.pem` | | Domeinvalidatie | `domain-suffix-match: ise.infra.saxion.net` | | Fase-2-domeinvalidatie | `phase2-domain-suffix-match: ise.infra.saxion.net` | | Anonieme identiteit | `anonymous@saxion.nl` | @@ -72,7 +128,7 @@ Een Python-script automatiseert de volledige `nmcli`-verbindingsconfiguratie voo curl -LO https://zephyrus-linux.stensel.nl/scripts/saxion-eduroam.py # 2. Controleer de checksum -echo "447a0979166cc801ba7406cc660b0403156532862ac031835291bb9d721f33e1 saxion-eduroam.py" | sha256sum -c +echo "17cd13c629ce480ece1a7896aff7d4061347ea0082b32dfa6b23dac6b34882ad saxion-eduroam.py" | sha256sum -c # 3. Uitvoeren python3 saxion-eduroam.py @@ -81,7 +137,7 @@ python3 saxion-eduroam.py #### Als het certificaat niet meer klopt De vertrouwde keten ligt vast in het script, dus die breekt zodra Saxion van -certificaatautoriteit wisselt — precies wat er in +certificaatautoriteit wisselt. Precies wat er in [#109](https://github.com/THectic-NL/Zephyrus-Linux/issues/109) gebeurde. Meldt het script `unknown CA` of lukt authenticatie niet, dan verbindt `--ignore-certificate` zonder te valideren en toont het welke keten de server @@ -96,14 +152,22 @@ daarna opnieuw zonder de vlag. **Laat dit niet aanstaan.** Zonder validatie wordt elk access point dat zich `eduroam` noemt vertrouwd. Dat kan de TLS-tunnel zelf afsluiten en de -MSCHAPv2-uitwisseling opvangen, die offline te kraken is — dat is je +MSCHAPv2-uitwisseling opvangen, die offline te kraken is. Dat is je Saxion-wachtwoord. `domain-suffix-match` helpt hier niet: die controleert de naam op een certificaat dat niemand geverifieerd heeft. Gebruik de vlag om te diagnosticeren en verbind daarna netjes. -**SHA256:** `447a0979166cc801ba7406cc660b0403156532862ac031835291bb9d721f33e1` +**SHA256:** `17cd13c629ce480ece1a7896aff7d4061347ea0082b32dfa6b23dac6b34882ad` + +Het script verwijdert een eventueel bestaand eduroam-profiel, vraagt je **gebruikersnaam** via een GUI-dialoog (kdialog op KDE, zenity op GNOME) of een terminal-fallback, en activeert de verbinding. Je wachtwoord wordt nooit door het script gevraagd; dat wordt bij het verbinden opgevraagd door je keyring (GNOME Keyring of KWallet) en versleuteld opgeslagen, nooit in platte tekst. -Het script verwijdert een eventueel bestaand eduroam-profiel, vraagt je **gebruikersnaam** via een GUI-dialoog (zenity, kdialog of yad) of terminal-fallback, en activeert de verbinding. Je wachtwoord wordt nooit door het script gevraagd; dat wordt bij het verbinden opgevraagd door je GNOME Keyring en veilig opgeslagen, nooit in platte tekst. +Handige vlaggen: + +| Vlag | Doel | +|------|------| +| `-u`, `--username` | Geef de gebruikersnaam mee in plaats van hem te laten vragen | +| `--silent` | Geen dialogen; vragen en melden alleen op de terminal | +| `--ignore-certificate` | Sla validatie over en toon de keten die de server stuurde. Alleen om te debuggen, zie de waarschuwing hierboven | {{< callout type="info" >}} Dit script is **Saxion-specifiek** en valideert tegen de Saxion RADIUS-server (`ise.infra.saxion.net`). Voor andere instellingen: gebruik het officiële CAT-script van [cat.eduroam.org](https://cat.eduroam.org/) als startpunt. @@ -122,7 +186,9 @@ Als alles goed gaat, zie je zoiets als dit: ### Handmatige setup via nmcli {{< callout type="info" >}} -Dit commando slaat het wachtwoord direct op in het verbindingsprofiel. Het geautomatiseerde script hierboven gebruikt `password-flags 1`, waardoor het wachtwoord veilig in de GNOME Keyring wordt opgeslagen. Beide methoden werken; de aanpak van het script is veiliger. +Dit commando slaat het wachtwoord direct op in het verbindingsprofiel. Het geautomatiseerde script hierboven gebruikt `password-flags 1`, waardoor het wachtwoord aan je keyring wordt overgedragen. Beide methoden werken; de aanpak van het script is veiliger. + +Het verwijst ook naar `~/.config/saxion-eduroam/saxion-eduroam-ca.pem`, dat pas bestaat nadat het script een keer gedraaid heeft. Draai dus eerst het script, of laat de regel `802-1x.ca-cert` weg en accepteer dat de keten dan niet gevalideerd wordt. {{< /callout >}} ```bash diff --git a/src/static/scripts/saxion-eduroam.py b/src/static/scripts/saxion-eduroam.py index ba3d2a8..0ec8677 100644 --- a/src/static/scripts/saxion-eduroam.py +++ b/src/static/scripts/saxion-eduroam.py @@ -26,6 +26,10 @@ SERVER_DOMAIN = "ise.infra.saxion.net" ANONYMOUS_ID = f"anonymous@{REALM}" +# Support policy, not a technical floor: the code itself runs on older Pythons. +# Everything current ships 3.11 or newer. +MIN_PYTHON = (3, 11) + # nmcli's default is 90s of silence, which looks like a hang. Cut it short. CONNECT_TIMEOUT = 45 @@ -37,26 +41,58 @@ ) CA_FILE = os.path.join(CA_DIR, "saxion-eduroam-ca.pem") -# The roots ise.infra.saxion.net chains to. GEANT moved its cert service to -# HARICA; this file used to pin the old USERTrust/"GEANT OV RSA CA 4" chain, -# which the server stopped sending, so every connect died on "unknown CA" -# (#109). Don't guess these -- read them off a real handshake: +# WHAT THIS SCRIPT TRUSTS, AND WHERE IT COMES FROM +# +# Nothing here is invented, and nothing is trusted just because it showed up in +# a handshake. Three sources, each doing one job: +# +# 1. cat.eduroam.org -- the connection parameters. Server name, realm, EAP +# methods. Cross-checked against Saxion's CAT profile; they agree. +# NOT the CA: that profile still pins the pre-migration USERTrust chain, +# which is why the official installer cannot connect (issue #109). +# 2. A live handshake -- which roots are actually in use. Run without a +# pinned CA, wpa_supplicant reports the chain the server sends. That is +# what the server does, not what a profile claims it does. +# 3. repo.harica.gr -- the certificates themselves, from the CA operator +# that issued them. Their published SHA-1 fingerprints were checked +# against these bytes on 2026-08-31. All four matched. +# +# So: CAT says what to connect to, the handshake says which CA is in play, and +# HARICA supplies the certificate. No single source is taken on faith. +# +# Re-checking is the same three steps. To read the live chain: # # nmcli connection modify eduroam 802-1x.ca-cert "" # nmcli connection up eduroam # journalctl -u wpa_supplicant -b | grep CTRL-EVENT-EAP-PEER-CERT # -# HARICA RootCA 2015 A0:40:92:9A:02:CE:53:B4... expires 2040 -# HARICA TLS RSA Root CA 2021 D9:5D:0E:8E:DA:79:52:5B... expires 2045 +# Identify a root by fingerprint, never by name. "HARICA TLS RSA Root CA 2021" +# exists self-signed AND cross-signed by the 2015 root: same CN, different +# certificate, different bytes. Pinning the wrong one silently fails. # -# The first is what the server currently chains to; the second keeps this -# working once GEANT drops the cross-signature. The GEANT TLS RSA 1 -# intermediate is not pinned -- the server sends it, and intermediates rotate -# often enough to break us again. +# RSA -- what the server serves today +# A0:40:92:9A:02:CE:53:B4... HARICA RootCA 2015 expires 2040 +# D9:5D:0E:8E:DA:79:52:5B... HARICA TLS RSA Root CA 2021 expires 2045 +# ECC -- for when Saxion moves off RSA +# 44:B5:45:AA:8A:25:E6:5A... HARICA ECC RootCA 2015 expires 2040 +# 3F:99:CC:47:4A:CF:CE:4D... HARICA TLS ECC Root CA 2021 expires 2045 # -# Yes, pinning breaks when Saxion switches CA. That is the trade: otherwise any -# of ~150 public CAs can impersonate the RADIUS server, and PEAP/MSCHAPv2 hands -# it a hash of the user's password. +# Both roots of each pair are pinned on purpose. The server currently chains +# through the cross-signed 2021 root up to the 2015 root, but HARICA publishes +# that cross certificate as expiring 2029-08-31. After that the chain has to +# terminate at the self-signed 2021 root, which is already pinned here, so that +# transition is a non-event. +# +# The GEANT TLS RSA 1 intermediate is not pinned: the server sends it, and +# intermediates rotate far more often than roots. +# +# All four are HARICA roots, so this trusts one CA operator. A system trust +# store would accept roughly 150. That gap is the whole point: without pinning, +# any of them could vouch for a server calling itself ise.infra.saxion.net, and +# PEAP/MSCHAPv2 hands that server a hash of the user's password. +# +# Pinning does break when Saxion changes CA operator. When it does, the script +# says so and prints the chain it saw, instead of hanging. SAXION_CA_PEM = """\ -----BEGIN CERTIFICATE----- MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix @@ -126,6 +162,38 @@ xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU 63ZTGI0RmLo= -----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- """ # Strict allowlist for valid Saxion usernames (prevents argument injection into nmcli). @@ -154,11 +222,25 @@ def __init__(self, silent: bool = False, username: str = "", self.gui_tool = None if silent else self._detect_gui() def _detect_gui(self) -> str | None: - """Detects available GUI tools (zenity, kdialog, yad).""" + """ + Pick a dialog tool that matches the desktop. + + Order matters: zenity is GTK and kdialog is Qt, and plenty of KDE + installs have zenity pulled in as somebody's dependency. Picking it + first there gives a GTK dialog on a Qt desktop -- wrong fonts, wrong + theme, wrong everything. + """ if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): return None - for tool in ["zenity", "kdialog", "yad"]: + desktop = (os.environ.get("XDG_CURRENT_DESKTOP", "") + + os.environ.get("XDG_SESSION_DESKTOP", "")).upper() + if "KDE" in desktop or "PLASMA" in desktop or os.environ.get("KDE_FULL_SESSION"): + order = ["kdialog", "zenity", "yad"] + else: + order = ["zenity", "kdialog", "yad"] + + for tool in order: if shutil.which(tool): return tool return None @@ -287,6 +369,48 @@ def warn_insecure(self): if not self.silent: self.show_message(warning, True) + def diagnose_cert_failure(self) -> bool: + """ + Say whether the last activation died on certificate validation, and if so + show what the server served. + + Without this a stale pin looks like a hang: NetworkManager keeps retrying + and nothing on screen says the CA is the problem. That is exactly how #109 + went unexplained for as long as it did. + """ + errors = self._journal_lines("CERT-ERROR", "certificate verify failed", + "unknown CA", "Certificate verification failed") + if not errors: + return False + + print("\n" + "=" * 70) + print("The server's certificate did not match the CA pinned in this script.") + print("=" * 70) + for line in errors[-4:]: + print(f" {line}") + self.report_server_chain() + print( + "\nSaxion has most likely changed RADIUS certificate authority. Report the\n" + "chain above so this script can be updated. To connect meanwhile:\n" + f" python3 {os.path.basename(sys.argv[0])} --ignore-certificate\n" + "which skips validation -- read the warning it prints before using it." + ) + return True + + def _journal_lines(self, *needles: str) -> list[str]: + """Recent wpa_supplicant lines matching any of the needles.""" + try: + res = subprocess.run( + ["journalctl", "-u", "wpa_supplicant", "-b", "--no-pager", + "--since", "-5m"], + capture_output=True, text=True, timeout=15, + ) + except (OSError, subprocess.SubprocessError): + return [] + return [ln.split("wpa_supplicant", 1)[-1].strip() + for ln in res.stdout.splitlines() + if any(n in ln for n in needles)] + def report_server_chain(self): """Print the chain the server actually presented, to fix the pin with.""" try: @@ -389,6 +513,17 @@ def install(self): # Real MAC on purpose: Saxion blocks a MAC that looks like it is # scanning, and randomising would let that block be shrugged off. "wifi.cloned-mac-address", "permanent", + # This profile belongs to the person who ran the script, not to every + # account on the machine. Taken from the CAT installer. + "connection.permissions", f"user:{getpass.getuser()}", + # Bound how long NetworkManager sits on a stalled EAP exchange. + "802-1x.auth-timeout", "20", + # Off for now. Left on, NetworkManager starts connecting the moment + # the profile is added -- before the user has typed anything -- and + # that attempt sits in EAP-STARTED until it times out ~30s later, + # asking for the password a second time on the way. Turned back on + # after we connect ourselves below. + "connection.autoconnect", "no", ] if ca_path: cmd += ["802-1x.ca-cert", ca_path] @@ -420,6 +555,20 @@ def install(self): # without the print below the script looks dead while nmcli waits. # --wait bounds nmcli, the subprocess timeout catches it ignoring that. print(f"[INFO] Connecting to {SSID} (up to {CONNECT_TIMEOUT}s)...", flush=True) + + try: + self._activate() + finally: + # Whatever happened above, leave a profile that reconnects on its own + # like any other saved network. + subprocess.run( + ["nmcli", "connection", "modify", CON_NAME, + "connection.autoconnect", "yes"], + capture_output=True, + ) + + def _activate(self): + """Bring the connection up and explain whatever nmcli reports.""" try: res = subprocess.run( ["nmcli", "--wait", str(CONNECT_TIMEOUT), "connection", "up", CON_NAME], @@ -431,10 +580,14 @@ def install(self): print( f"[WARN] eduroam profile saved, but activation did not finish within " f"{CONNECT_TIMEOUT}s.\n" - " This usually means the EAP handshake is failing and NetworkManager\n" - " is retrying. Check what it reported with:\n" - " journalctl -u NetworkManager -u wpa_supplicant -b --since '5 min ago'" + " This usually means the EAP handshake is failing and\n" + " NetworkManager is retrying." ) + if not self.diagnose_cert_failure(): + print( + " Check what it reported with:\n" + " journalctl -u NetworkManager -u wpa_supplicant -b --since '5 min ago'" + ) return output = res.stderr.strip() or res.stdout.strip() @@ -448,7 +601,7 @@ def install(self): print( "[INFO] eduroam profile saved, but the network could not be reached right now.\n" " You are probably not in range of an eduroam access point.\n" - " The profile is stored — connect to eduroam from your network settings when nearby." + " The profile is stored. Connect to eduroam from your network settings when nearby." ) elif ( "Secrets were required" in output @@ -469,12 +622,19 @@ def install(self): else: print( "[WARN] eduroam profile saved, but automatic activation failed.\n" - " You can connect manually via your network settings.\n" f" nmcli: {output}" ) + if not self.diagnose_cert_failure(): + print(" You can connect manually via your network settings.") def main(): + if sys.version_info < MIN_PYTHON: + need = ".".join(str(n) for n in MIN_PYTHON) + have = ".".join(str(n) for n in sys.version_info[:3]) + print(f"This script needs Python {need} or newer; this is {have}.", + file=sys.stderr) + sys.exit(1) parser = argparse.ArgumentParser(description="Saxion eduroam Installer") parser.add_argument("-u", "--username", help="Saxion username")