ADR-094 — air-sshd phase 2 : ssh-userauth (authentification publickey Ed25519, conf par-utilisateur binaire, politique de login, events AirCom, durcissement Terrapin)
Statut : Accepté (2026-07-23, décision BDFL). (Proposé le 2026-07-14, ratifié le
2026-07-23.) Met en œuvre la
phase 2 d’ADR-074 (vision air-sshd), au-dessus de la
phase 1 transport (ADR-093, complète et
wire-compat OpenSSH scellée en CI — T.1→T.4b, main e61d952). Reste sur le motif sans-IO
(ADR-091). S’appuie sur air-crypto Ed25519
(ADR-034), air-account
(ADR-041), air-config binaire
(ADR-073), air-process (privsep/drop_to_user),
AirCom (ADR-001, events de session ADR-093 §5).
Catégorie : Architecture (couche 2, service réseau). Toute évolution structurante passe par un RFC (ADR-015).
Contexte
Le transport de la phase 1 s’arrête à SSH_MSG_SERVICE_ACCEPT : un vrai client OpenSSH atteint
ssh-userauth (prouvé en CI, T.4b) puis échoue l’auth faute de méthode. La phase 2 implémente
le protocole d’authentification SSH (RFC 4252) : le client prouve son identité, le serveur
décide, puis la session (phase 3) peut s’ouvrir. C’est aussi là qu’atterrissent trois directives
BDFL déjà gravées (ADR-093 §5/§7) : events AirCom AuthOk/AuthFailed, conf binaire
par-utilisateur en HomeDirectory (l’équivalent authorized_keys), politique de login sur
air-account. Enfin, la phase 2 est le bon moment pour trancher le durcissement Terrapin /
strict-KEX noté en T.4b.
Décision
1. Protocole — RFC 4252 sur le transport chiffré, sans-IO
Extension du cœur air-ssh-proto (pur, #![forbid(unsafe_code)]) : nouveau module userauth
(codec + StateMachine d’authentification), piloté par air-sshd. Messages :
SSH_MSG_USERAUTH_REQUEST (50), _FAILURE (51), _SUCCESS (52), _BANNER (53), et pour
publickey : SSH_MSG_USERAUTH_PK_OK (60). Le parseur de USERAUTH_REQUEST est une surface
hostile (nom d’utilisateur, service, méthode, blob de clé, signature — tous longueur-préfixés
venant du réseau) → décodage borné anti-hostile, from_utf8 (nom d’utilisateur = UTF-8 validé,
borné), fuzz obligatoire.
2. Méthodes — publickey Ed25519 d’abord, moderne uniquement
none: rend la liste des méthodes autorisées (=publickey) sans authentifier (RFC 4252 §5.2) — jamais un succès.publickey(RFC 4252 §7) avecssh-ed25519(RFC 8709) uniquement. Flux en deux temps : (a)has_signature = false→ si la clé est autorisée pour l’utilisateur, le serveur répondPK_OK(sonde) ; (b)has_signature = true→ le serveur vérifie la signature surstring(session_id) ‖ SSH_MSG_USERAUTH_REQUEST‖…(RFC 4252 §7,session_id= leHdu premier KEX, figé en phase 1) viaair-crypto::AirVerifyingKey::verify(constant-time), puisUSERAUTH_SUCCESS.- Refusés :
password(différé — la doctrine binaire/clés d’abord ; à réserver via un incrément dédié si un besoin apparaît), types de clé legacy (ssh-rsa/ssh-dss/ECDSA-NIST).
3. Autorisation — conf binaire par-utilisateur en HomeDirectory (air-config)
L’équivalent d’~/.ssh/authorized_keys, binaire (jamais texte, ADR-073) : un artefact
air-config (schéma capnp dédié sshd_authorized_keys) dans le HomeDirectory de l’utilisateur
(emplacement figé par l’incrément, p.ex. ~/.config/air/sshd/authorized_keys) listant les clés
publiques ssh-ed25519 autorisées (+ options éventuelles : from, command, expiration —
différables). Le serveur, après résolution de l’utilisateur (§4), lit cet artefact via air-config
et décide si la clé présentée est autorisée. Un artefact système (host keys, utilisateurs/
méthodes permis — déjà en phase 1) complète la politique globale. Import/export possibles (ADR-073).
4. Politique de login — air-account + air-process
- Résolution de l’utilisateur via
air-account(/etc/passwd/group→ uid/gid, HomeDirectory, shell) ; utilisateur inexistant / login interdit →USERAUTH_FAILURE(sans révéler pourquoi — anti-énumération, cf. §6). - Privsep : le modèle moniteur↔enfant (SCM_RIGHTS déjà livré) — le composant privilégié
authentifie, la session (phase 3) tourne après
drop_to_user(air-process). La phase 2 prépare l’identité résolue+autorisée ; le largage effectif de privilèges et le PTY/shell sont phase 3 (ssh-connection).
5. Observabilité — events AirCom (directive BDFL)
Le pilote publie sur AirCom (pub-sub §7, schéma air-sshd-schema figé en T.1) : AuthOk{user, method, peer} et AuthFailed{user, method, peer} — un consommateur suit qui s’authentifie et
d’où. SessionOpened/ShellStarted/SessionClosed viendront en phase 3.
6. Durcissements de sécurité (partie intégrante de la phase 2)
- Terrapin / strict-KEX (CVE-2023-48795) : implémenter
kex-strict-s-v00@openssh.com(annoncé dansKEXINITserveur) — si négocié, réinitialiser les numéros de séquence àNEWKEYSet refuser tout message inattendu pendant le KEX. Modifie le transport (phase 1) : traité comme incrément de durcissement en tête de phase 2 (U.0). (C’est la levée de la note T.4b.) - Login grace timeout (via le Timer
air-async), nombre d’essais borné, anti-énumération (échecs indistinguables entre utilisateur inconnu / clé non autorisée / signature invalide ; temps de réponse non dépendant du secret autant que possible).
7. Discipline sans-IO & couche
userauth (cœur, calcul pur : codec, state machine, vérification de signature = calcul via
air-crypto) reste dans air-ssh-proto ; le pilote air-sshd fait l’I/O, la lecture
air-config du HomeDirectory, la résolution air-account, la publication AirCom, le privsep.
Arêtes 2→2 / 2→1 uniquement (jamais 2→0, check-layers).
8. Cible d’acceptation (wire-compat, scellée en CI comme T.4b)
Un vrai client ssh -i <clé_ed25519> de stock s’authentifie avec succès (USERAUTH_SUCCESS,
debug1: Authentication succeeded (publickey)) contre air-sshd, la clé étant dans l’artefact
authorized_keys binaire de l’utilisateur ; et échoue proprement (USERAUTH_FAILURE) avec une
clé non autorisée. Test d’intégration ssh réel automatisé (extension de ssh_interop.rs).
9. Incréments (phase 2)
| Inc. | Contenu | Preuve |
|---|---|---|
| U.0 | Durcissement Terrapin / strict-KEX (kex-strict-s-v00@openssh.com, reset seqnr à NEWKEYS, refus messages hors-KEX). | interop ssh conservée + strict-kex négocié |
| U.1 | Codec + StateMachine userauth (REQUEST/FAILURE/SUCCESS/BANNER/PK_OK, méthode none→liste). Câblage SERVICE_REQUEST→userauth. Fuzz du parseur USERAUTH_REQUEST. | ssh reçoit la liste publickey |
| U.2 | publickey Ed25519 : sonde PK_OK + vérif de signature sur session_id (RFC 4252 §7, air-crypto). | signature valide → SUCCESS (clé codée en dur de test) |
| U.3 | authorized_keys binaire par-utilisateur (air-config, HomeDirectory) + résolution air-account + décision d’autorisation + events AuthOk/AuthFailed. | vrai ssh -i clé → Authentication succeeded ; clé non autorisée → FAILURE |
| U.4 | Login grace / essais bornés / anti-énumération + preuve d’intégration ssh scellée en CI (succès + échec). | test CI vert (succès & refus) |
Chaque incrément : cœur ~100 % (unit + property + fuzz + model-based), pilote > 90 %, barrière
verte, délégué à carbon puis re-vérifié et mergé (barrière + couverture + fuzz + revue adverse
ciblant le parseur USERAUTH_REQUEST et la vérif de signature). Additif air-crypto improbable
(Ed25519 verify déjà public) — sinon STOP-and-report/escalade BDFL comme en phase 1.
Conséquences
air-sshddevient un serveur SSH authentifiant : un utilisateur se connecte réellement (sans shell tant que la phase 3 n’est pas là — la session/PTY/exec =ssh-connection).- La conf binaire par-utilisateur matérialise la doctrine ADR-073 côté HomeDirectory ; jamais
d’
authorized_keystexte. - Observabilité AirCom réalisée pour l’auth ;
air-sshdreste le premier citoyen AirCom. - Terrapin fermé dès la phase 2 (dette T.4b levée).
- Prépare la phase 3 (
ssh-connection: canaux,pty-req/shell/exec,drop_to_user, provenance d’exécution). - Dette de phase 1 à solder en parallèle (hors phase 2 stricto sensu) : re-sceau
couche-1-v2.2groupé (air-socket::into_owned_fd,air-shm, constructeurs seedés #356, AEAD SSH #358) ; F2 (contributory X25519air-crypto).
Alternatives rejetées
passwordd’abord (ou du tout, en v1). Refusé : clés d’abord (plus sûr, aligné doctrine) ; password réservé à un incrément dédié si besoin.- Types de clé legacy (RSA/DSA/ECDSA-NIST). Refusé (profil moderne ADR-074).
authorized_keystexte façon OpenSSH. Refusé :air-configbinaire (ADR-073).- Différer Terrapin/strict-KEX. Rejeté : la mitigation est cheap et touche le transport qu’on vient de bâtir — la faire maintenant évite une reprise ultérieure.
- Auth dans le pilote plutôt que le cœur. La vérification de signature est du calcul pur → elle
reste dans le cœur sans-IO (fuzzable) ; seuls l’I/O,
air-config,air-accountsont au pilote.
Références : RFC 4251 (architecture), RFC 4252 (authentification SSH), RFC 8709
(ssh-ed25519), CVE-2023-48795 (Terrapin / strict-KEX).