Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec couche 2 — air-ssh (SSH maison, moderne-only, patron sans-IO)

Spécification technique — Version 0.1 (proposition). Couche 2 « Frameworks système ». Cadrée par ADR-043.

Cadrage. air-ssh implémente SSH (RFC 4250-4254 + 4256, certificats) maison, memory-safe Rust, zéro-C — décision ADR-043 (pas russh : maturité insuffisante pour un sshd distant critique ; crypto = RustCrypto via air-crypto, jamais réécrite). Client (ssh/scp/sftp) avant serveur (sshd). « Strict kex » anti-Terrapin dès le départ, moderne uniquement. Suit le patron sans-IO 9-composants.

Position et méthode

SSH est un protocole filaire (transport chiffré + auth + multiplexage de canaux) → cœur sans-IO (les 9 composants, fuzzés isolément — Handshaker/Framer = surface hostile)

  • pilote I/O mince sur air-socket (TCP) + air-runtime (async).

Rappel gouvernance (ADR-043). En incubation, Air s’appuie sur l’OpenSSH système ; air-ssh maison est la cible. Le sshd (serveur) exige un audit externe avant toute exposition ; on livre le client d’abord. Cette spec décrit la cible ; l’ordre d’implémentation est client → serveur.

Périmètre de sécurité — moderne uniquement (comme rustls pour TLS)

RETENU :

DomaineRetenuRéf.
Strict kexkex-strict-c/s-v00@openssh.com obligatoire (anti-Terrapin CVE-2023-48795)RFC 4253 durci
Échange de cléscurve25519-sha256, mlkem768x25519-sha256 (hybride PQ, défaut OpenSSH ≥ 9.9 — réutilise ml-kem-768 ADR-078/081), ecdh-sha2-nistp256/384 ; (sntrup761x25519 différé, ADR-081)RFC 5656, ADR-081
Clés d’hôte / signaturesssh-ed25519, ecdsa-sha2-nistp256/384, rsa-sha2-256/512 (vérif.)RFC 8709, 8332
Chiffrementchacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com (AEAD only)RFC 5647
MACAEAD implicite ; sinon hmac-sha2-256/512-etm@openssh.com (encrypt-then-MAC)
Authpublickey (Ed25519/ECDSA/rsa-sha2), keyboard-interactive, certificats OpenSSH ; password (déconseillé)RFC 4252, 4256
Extext-info-c/s (RFC 8308), SFTP (subsystem)RFC 8308

OMIS PAR CONCEPTION (legacy/faible) : SSHv1 ; kex diffie-hellman-group1/14-sha1, tout SHA-1 ; ssh-rsa (SHA-1), ssh-dss ; CBC, RC4, 3DES ; hmac-md5, hmac-sha1 ; compression zlib@openssh avant auth ; agent/X11/port-forwarding = surface minimale, explicite, désactivée par défaut.

Dépendances

  • Tierces externes : ZÉRO (contrairement à TLS, pas d’exception C — ADR-043 : air-ssh maison + air-crypto = zéro surface C).
  • Crates Air : air-crypto (L1 — X25519/Ed25519/AEAD/HKDF v1 ; additifs réutilisés ADR-078 : ECDSA, ML-KEM-768 (kex mlkem768x25519) ; additifs ADR-081 : RSA PKCS#1 v1.5 vérif. (rsa-sha2), module lowlevel (ChaCha20 keystream + Poly1305 pour chacha20-poly1305@openssh)), air-socket (L1, TCP), air-runtime (L1, async), air-base-core (L1). air-filesystem (L1) pour known_hosts/clés.
  • Zéro unsafe exposé ; parsing défensif intégral (la surface hostile SSH est pré-authentification — classe regreSSHion éliminée par memory-safety).

Anatomie : les 9 composants appliqués à SSH

#ComposantRéalisation air-sshPur ?
1FramerBppFramer — Binary Packet Protocol (RFC 4253 §6) : packet_length/padding/payload/MAC ; strict kex (numéros de séquence non réinitialisés, anti-Terrapin)
2CodecMessageCodec — messages SSH_MSG_* (KEXINIT, KEX_ECDH_, NEWKEYS, USERAUTH_, CHANNEL_*), types name-list/mpint/string bornés
3StateMachineTransportStateMachineVersion → KexInit → Kex → NewKeys → UserAuth → Connection (RFC 4253/4252)
4HandshakerKeyExchange — kex (X25519/PQ), vérif. clé d’hôte (known_hosts), dérivation de clés, rekey ; consomme air-crypto✅ + air-crypto
5Flow ControllerChannelFlowController — fenêtres de canal (CHANNEL_WINDOW_ADJUST, RFC 4254 §5.2)
6MultiplexerChannelMultiplexer — canaux (session/exec/subsystem/forward), IDs sans collision, isolation
7Timer ManagerSshTimers — rekey (temps/volume), keepalive ; horloge injectée✅ (horloge injectée)
8Session ContextSessionKeys (clés de trafic bidirectionnelles, zeroize) + algos négociés + état des canaux
9Extension hooksext-info (RFC 8308), négociation d’algos, subsystem SFTP

Inventaire des objets — ~30 objets

A. Surface publique (client d’abord) — ~12

#ObjetRôle
1SshClient (+ SshClientConfig/Builder)client (ssh) — connexion + auth
2SshSessionsession établie (ouvre des canaux)
3SshChannelcanal (exec/shell/subsystem) — I/O + fenêtrage
4SshCommandexécution distante (exec) → stdout/stderr/exit
5HostKeyVerifier (trait) + KnownHostsvérif. de la clé d’hôte (TOFU/known_hosts)
6SshCredentialclé publique (Ed25519/ECDSA/cert), agent, mot de passe
7SshIdentityclé privée locale (zeroize) ou délégation agent
8SftpClientsous-système SFTP (RFC draft) — fichiers distants
9ScpTransfercopie de fichiers (sur exec/SFTP)
10SshErrorenum (ADR-019)
11Algorithmenums d’algos négociés (kex/cipher/mac/hostkey)
12SshServer (+ config)serveur sshd — DIFFÉRÉ (audit externe requis, ADR-043)

B. Cœur sans-IO — ~13 (les 9 composants + sous-objets)

BppFramer, MessageCodec, TransportStateMachine, KeyExchange, HostKeyAuth, UserAuthStateMachine, ChannelMultiplexer, ChannelFlowController, SshTimers, SessionKeys/KeySchedule, ExtInfo, SftpCodec, NameListCodec.

C. Enums wire — ~5

MessageType, KexAlgorithm, CipherAlgorithm, MacAlgorithm, ChannelType.

Codecs (bytes ↔ messages)

#![allow(unused)]
fn main() {
/// Lecteur SSH borné (RFC 4251 §5 : string=len32+bytes, mpint, name-list) — anti-panic.
pub struct SshReader<'a> { /* … */ }
impl<'a> SshReader<'a> {
    pub fn string(&mut self) -> Result<&'a [u8], DecodeError>;     // len u32 borné
    pub fn name_list(&mut self) -> Result<NameList<'a>, DecodeError>;
    pub fn mpint(&mut self) -> Result<&'a [u8], DecodeError>;      // grand entier
}
pub enum SshMessage<'a> {
    KexInit(KexInitBody<'a>), KexEcdhInit(&'a [u8]), KexEcdhReply(KexReplyBody<'a>),
    NewKeys, ServiceRequest(&'a [u8]),
    UserauthRequest(UserauthBody<'a>), UserauthSuccess, UserauthFailure(NameList<'a>),
    ChannelOpen(ChannelOpenBody<'a>), ChannelData { channel: u32, data: &'a [u8] },
    ChannelWindowAdjust { channel: u32, bytes: u32 }, ChannelEof(u32), ChannelClose(u32),
    // OMIS : messages des kex/ciphers legacy (non négociables).
}
}

Machine à états — multi-thread (mono-propriétaire, sans-IO)

#![allow(unused)]
fn main() {
impl SshClient {
    pub async fn connect(endpoint: AirEndpoint, config: Arc<SshClientConfig>,
        verifier: Arc<dyn HostKeyVerifier>) -> Result<SshSession, SshError>;
}
impl SshSession {
    pub async fn authenticate(&mut self, identity: SshIdentity) -> Result<(), SshError>;
    pub async fn open_exec(&mut self, command: &str) -> Result<SshChannel, SshError>;
    pub async fn open_shell(&mut self) -> Result<SshChannel, SshError>;
    pub async fn sftp(&mut self) -> Result<SftpClient, SshError>;
}
// Cœur sans-IO : feed(&[u8]) -> events ; poll_transmit() -> &[u8] ; handle_timeout(now).
}

Thread model : SshSession Send/!Sync (une connexion, une tâche ; les canaux se multiplexent dans la session) ; config Arc Send/Sync. Thread-per-core (ADR-038/039). Secrets zeroize.

Sécurité (le cœur de l’exigence ADR-043)

  • Memory-safe ⇒ classe regreSSHion (race handler async-signal-unsafe) éliminée par construction (pas de code C, handlers signaux = doctrine ADR-064 côté runtime).
  • Strict kex obligatoire (anti-Terrapin) — pas de négociation « best-effort ».
  • Moderne uniquement — aucun kex/cipher/MAC faible spécifié (omission = sécurité).
  • Vérif. de clé d’hôte non contournable par défaut (TOFU explicite / known_hosts) ; jamais de « accept-any » silencieux.
  • Forwarding minimal, explicite, off par défaut ; auth avant compression ; surface pré-auth minimale et fuzzée.
  • sshd (serveur) : DIFFÉRÉ, livré après audit externe (ADR-043).

Stratégie de tests

  • Couverture couche 2 > 90 % (viser 100 % cœur sans-IO).
  • Par composant : BppFramer/MessageCodec fuzzés (paquets pré-auth hostiles → zéro panic) ; KeyExchange vecteurs + négatifs (downgrade, Terrapin rejeté par strict kex, mauvaise clé d’hôte) ; ChannelMux/FlowController property.
  • Interop OpenSSH (client air-sshsshd OpenSSH, et vice-versa quand serveur) + conformité RFC 4250-4254/4256, comme les KAT air-crypto.
  • Pilote I/O : loopback TCP réel.

Récapitulatif & décisions

FamilleObjets
A. Surface publique (client d’abord)~12
B. Cœur sans-IO (9 comp.)~13
C. Enums wire~5
Total~30
  1. Maison, memory-safe, zéro-C (ADR-043, pas russh) ; crypto via air-crypto.
  2. Strict kex anti-Terrapin + moderne uniquement (legacy omis par conception).
  3. Client avant serveur ; sshd différé (audit externe requis).
  4. 9 composants exercés (flow control + mux de canaux natifs) ; SFTP en subsystem.
  5. Async 1ʳᵉ classe (ADR-038), session Send/!Sync, secrets zeroize.

Travail à reprendre

  • Additifs air-cryptoinstruits (ADR-081) : RSA PKCS#1 v1.5 vérif. (rsa-sha2), module lowlevel (ChaCha20/Poly1305 pour chacha20-poly1305@openssh), kex PQ via ML-KEM-768 réutilisé (ADR-078). sntrup761 différé (pas de crate pure-Rust vettée).
  • Ordre : client (sshexec/shellsftp/scp) puis serveur sshd (après audit externe).
  • Certificats OpenSSH (RFC-less, format OpenSSH) ; agent (protocole d’agent) ; forwarding (minimal). ABI C différée.

Licence du document : MPL 2.0 Statut : Spécification air-ssh (couche 2) v0.1 — SSH maison memory-safe, moderne-only, strict-kex, patron sans-IO ; crypto via air-crypto (zéro-C, ADR-043). Client avant serveur ; sshd différé (audit externe). Implémentation à suivre.