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-sshimplémente SSH (RFC 4250-4254 + 4256, certificats) maison, memory-safe Rust, zéro-C — décision ADR-043 (pas russh : maturité insuffisante pour unsshddistant critique ; crypto = RustCrypto viaair-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-sshmaison est la cible. Lesshd(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 :
| Domaine | Retenu | Réf. |
|---|---|---|
| Strict kex | kex-strict-c/s-v00@openssh.com obligatoire (anti-Terrapin CVE-2023-48795) | RFC 4253 durci |
| Échange de clés | curve25519-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 / signatures | ssh-ed25519, ecdsa-sha2-nistp256/384, rsa-sha2-256/512 (vérif.) | RFC 8709, 8332 |
| Chiffrement | chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com (AEAD only) | RFC 5647 |
| MAC | AEAD implicite ; sinon hmac-sha2-256/512-etm@openssh.com (encrypt-then-MAC) | — |
| Auth | publickey (Ed25519/ECDSA/rsa-sha2), keyboard-interactive, certificats OpenSSH ; password (déconseillé) | RFC 4252, 4256 |
| Ext | ext-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-sshmaison +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 (kexmlkem768x25519) ; additifs ADR-081 : RSA PKCS#1 v1.5 vérif. (rsa-sha2), modulelowlevel(ChaCha20 keystream + Poly1305 pourchacha20-poly1305@openssh)),air-socket(L1, TCP),air-runtime(L1, async),air-base-core(L1).air-filesystem(L1) pourknown_hosts/clés. - Zéro
unsafeexposé ; 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
| # | Composant | Réalisation air-ssh | Pur ? |
|---|---|---|---|
| 1 | Framer | BppFramer — Binary Packet Protocol (RFC 4253 §6) : packet_length/padding/payload/MAC ; strict kex (numéros de séquence non réinitialisés, anti-Terrapin) | ✅ |
| 2 | Codec | MessageCodec — messages SSH_MSG_* (KEXINIT, KEX_ECDH_, NEWKEYS, USERAUTH_, CHANNEL_*), types name-list/mpint/string bornés | ✅ |
| 3 | StateMachine | TransportStateMachine — Version → KexInit → Kex → NewKeys → UserAuth → Connection (RFC 4253/4252) | ✅ |
| 4 | Handshaker | KeyExchange — kex (X25519/PQ), vérif. clé d’hôte (known_hosts), dérivation de clés, rekey ; consomme air-crypto | ✅ + air-crypto |
| 5 | Flow Controller | ChannelFlowController — fenêtres de canal (CHANNEL_WINDOW_ADJUST, RFC 4254 §5.2) | ✅ |
| 6 | Multiplexer | ChannelMultiplexer — canaux (session/exec/subsystem/forward), IDs sans collision, isolation | ✅ |
| 7 | Timer Manager | SshTimers — rekey (temps/volume), keepalive ; horloge injectée | ✅ (horloge injectée) |
| 8 | Session Context | SessionKeys (clés de trafic bidirectionnelles, zeroize) + algos négociés + état des canaux | ✅ |
| 9 | Extension hooks | ext-info (RFC 8308), négociation d’algos, subsystem SFTP | ✅ |
Inventaire des objets — ~30 objets
A. Surface publique (client d’abord) — ~12
| # | Objet | Rôle |
|---|---|---|
| 1 | SshClient (+ SshClientConfig/Builder) | client (ssh) — connexion + auth |
| 2 | SshSession | session établie (ouvre des canaux) |
| 3 | SshChannel | canal (exec/shell/subsystem) — I/O + fenêtrage |
| 4 | SshCommand | exécution distante (exec) → stdout/stderr/exit |
| 5 | HostKeyVerifier (trait) + KnownHosts | vérif. de la clé d’hôte (TOFU/known_hosts) |
| 6 | SshCredential | clé publique (Ed25519/ECDSA/cert), agent, mot de passe |
| 7 | SshIdentity | clé privée locale (zeroize) ou délégation agent |
| 8 | SftpClient | sous-système SFTP (RFC draft) — fichiers distants |
| 9 | ScpTransfer | copie de fichiers (sur exec/SFTP) |
| 10 | SshError | enum (ADR-019) |
| 11 | Algorithm | enums d’algos négociés (kex/cipher/mac/hostkey) |
| 12 | SshServer (+ 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-ssh↔sshdOpenSSH, et vice-versa quand serveur) + conformité RFC 4250-4254/4256, comme les KATair-crypto. - Pilote I/O : loopback TCP réel.
Récapitulatif & décisions
| Famille | Objets |
|---|---|
| A. Surface publique (client d’abord) | ~12 |
| B. Cœur sans-IO (9 comp.) | ~13 |
| C. Enums wire | ~5 |
| Total | ~30 |
- Maison, memory-safe, zéro-C (ADR-043, pas russh) ; crypto via
air-crypto. - Strict kex anti-Terrapin + moderne uniquement (legacy omis par conception).
- Client avant serveur ;
sshddifféré (audit externe requis). - 9 composants exercés (flow control + mux de canaux natifs) ; SFTP en subsystem.
- Async 1ʳᵉ classe (ADR-038), session
Send/!Sync, secretszeroize.
Travail à reprendre
- Additifs
air-crypto— instruits (ADR-081) : RSA PKCS#1 v1.5 vérif. (rsa-sha2), modulelowlevel(ChaCha20/Poly1305 pourchacha20-poly1305@openssh), kex PQ via ML-KEM-768 réutilisé (ADR-078).sntrup761différé (pas de crate pure-Rust vettée). - Ordre : client (
ssh→exec/shell→sftp/scp) puis serveursshd(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.