Spec couche 2 — air-tls (contingence maison, TLS 1.3 pur Rust)
Spécification technique — Version 0.1 (proposition). Couche 2 « Frameworks système ». Statut : spec de contingence.
Nature de ce document. ADR-042 a tranché que la pile TLS de production d’Air est rustls + aws-lc-rs + rustls-webpki (exception C nommée, étroite). L’ADR garde explicitement ouverte (§Contingence) une porte de sortie : un
air-tlsmaison, protocole pur Rust, sur le patron sans-IO 9-composants, activable si rustls devenait un passif. Le présent document spécifie cette contingence — il ne remplace pas ADR-042, il en instruit l’option maison pour qu’elle soit prête sur étagère. Toute bascule production→maison exigerait un RFC (ADR-015).Il répond à une demande précise : décrire une implémentation 100 % Rust, zéro
unsafe, zéro dépendance à la libc, aucune crate tierce horsstd(voir la réserve « primitives » en §3), qui reprend la couverture fonctionnelle de s2n-tls mais n’en garde que le moderne réputé sûr (TLS 1.3, RFC 8446 ; tout le legacy vulnérable est omis par conception).
Position et méthode
air-tls fournit le protocole de session sécurisée au-dessus d’air-socket
(L1, TCP/Unix) et sous air-network (L2, substrat de connexion ≈ Network.framework)
et les protocoles applicatifs (air-http h2, air-quic→h3). Il n’implémente aucune
primitive cryptographique : celles-ci viennent d’air-crypto (L1). Il implémente
le protocole : record layer, handshake, key schedule, machine à états, validation
X.509 — exactement la frontière tracée par ADR-042 (« le protocole memory-safe, les
primitives déléguées »).
Il suit à l’identique l’anatomie canonique des crates réseau Air
(note réseau §2.2) : cœur sans-IO
(9 composants purs, fuzzables) + pilote I/O mince. Un auditeur retrouve ici les
mêmes cases que dans air-socket::resolver, air-quic, air-ssh.
Périmètre de sécurité — ON NE GARDE QUE LE MODERNE (décision structurante)
Règle gravée. Ce qui a une classe de vulnérabilité connue n’est pas spécifié. La sécurité par omission : le code d’un protocole absent ne peut pas être exploité. C’est le même choix qu’
air-crypto(« RSA & legacy différés, plus sûrs par omission ») et que rustls (« TLS 1.2/1.3 only, zéro legacy »).
RETENU (TLS 1.3, RFC 8446 — AEAD only, forward-secrecy obligatoire) :
| Domaine | Retenu | Référence |
|---|---|---|
| Version protocole | TLS 1.3 uniquement | RFC 8446 |
| Suites (AEAD only) | TLS_AES_128_GCM_SHA256 (MTI), TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256 | RFC 8446 §9.1 |
| Échange de clés | X25519 (défaut) ; X25519MLKEM768 (hybride post-quantique, recommandé 2025+) ; secp256r1/secp384r1 (interop) | RFC 7748, RFC 9180-hybride |
| Signatures (vérif. + sign.) | ed25519, ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, rsa_pss_rsae_sha256/384/512 (vérif. seule, interop WebPKI) | RFC 8032, RFC 8446 §4.2.3 |
| Fonctionnalités | SNI, ALPN, reprise par PSK tickets (FS), KeyUpdate, downgrade sentinel, validation X.509 (RFC 5280) + hostname (RFC 6125), OCSP stapling, CRL | RFC 8446, 5280, 6125 |
| 0-RTT (early data) | optionnel, désactivé par défaut, anti-rejeu documenté | RFC 8446 §2.3, §8 |
OMIS PAR CONCEPTION (jamais spécifié — legacy vulnérable) : SSLv3, TLS 1.0/1.1/1.2 ; RSA key transport (kex statique) ; CBC, RC4, 3DES ; signatures MD5/SHA-1 ; compression TLS (CRIME) ; renégociation (retirée en 1.3) ; groupes DH à corps fini faibles, courbes faibles ; suites NULL/anonymes/export ; tickets sans forward-secrecy.
Conséquence de test : il n’existe aucun chemin de downgrade à tester parce qu’il n’existe aucun code de version antérieure. Le downgrade sentinel de RFC 8446 §4.1.3 est néanmoins implémenté (défense en profondeur : détecter un MITM qui prétendrait qu’on négocie 1.2).
Dépendances — « std seulement » : la réserve honnête des primitives
Contrainte demandée : pur Rust, zéro
unsafe, zéro C, seulestdautorisée. Réserve technique incontournable : on ne réimplémente JAMAIS de crypto (Principe 6, ADR-034, specair-crypto) —stdn’offre aucune primitive. La résolution cohérente avec la doctrine de couches :air-tlsne prend aucune crate tierce, mais consomme les crates Air maison des couches inférieures.
- Crates tierces externes : ZÉRO. Aucune (ni rustls, ni RustCrypto en direct, ni
*-sys).check-c-surfacereste vert pour ce crate — c’est l’intérêt de la contingence maison vs ADR-042 (qui, lui, ouvre l’exception Caws-lc-sys). std: autorisée (buffers,io::{Read,Write},Vec,BTreeMap…). (Le cœur sans-IO viseno_std + allocà terme, cohérent note §2.1 ; le pilote I/O restestd. Non bloquant pour la v0.1.)- Dépendances Air internes (couches inférieures, maison, non « tierces ») :
air-crypto(L1) — toutes les primitives : protection de record =lowlevel::AirAeadExplicitNonce(nonce =static_iv ⊕ seq, RFC 8446 §5.3 — pas la façadeAirAeadà nonce aléatoire ; additif ADR-082),AirEcdh*(X25519),AirSigningKey/AirVerifyingKey(Ed25519),AirHkdf(key schedule),AirHash(transcript SHA-256/384),AirHmac,AirRandom, secretszeroize. C’est la frontière crypto vettée (RustCrypto sous exception nommée, ADR-034) —air-tlsne la franchit jamais.air-socket(L1) — transport TCP/Unix (pilote I/O synchrone).air-runtime(L1) — pilotage asynchrone (buffers possédés, ADR-038).air-base-core(L1) —AirError/AirResult(ADR-019),AirOsStr.
Additifs à demander à
air-crypto(aujourd’hui différés — voir « Travail à reprendre ») pour couvrir l’interop WebPKI réelle : AES-128-GCM, ECDSA P-256/P-384 (vérif+sign), RSA-PSS (vérif seule), ML-KEM-768 (hybride PQ). Sans eux,air-tlsv0.1 négocie X25519 + Ed25519/AES-256-GCM/ChaCha20 uniquement (interop limitée aux pairs modernes). Chaque additif = bumpair-cryptoKAT-gated (ADR-034).
- Zéro
unsafeexposé et interne (le seulunsafedu projet côté crypto vit dans RustCrypto, sousair-crypto). Parsing défensif intégral (Principe 3) :get()jamais l’indexation,&[u8]vs&strstricts,checked_*, aucune boucle de longueur non bornée.
Modèle multi-thread (décision — état protocolaire mono-propriétaire)
La machine à états est conçue multi-thread au sens Rust idiomatique (le modèle de rustls/quinn-proto, aligné ADR-038 thread-per-core sans work-stealing) :
| Type | Send | Sync | Partage | Rôle |
|---|---|---|---|---|
TlsClientConfig / TlsServerConfig | ✅ | ✅ | Arc<…> partagé entre threads/connexions ; immuable après build | Politique (certs, suites, verifier, ALPN, tickets) |
TlsClient / TlsServer (connexion) | ✅ | ❌ | possédée par une seule tâche/thread à la fois ; aucun verrou interne | La machine à états d’une connexion |
TlsStream<T> | ✅ | ❌ | idem, enveloppe I/O bloquante | Adaptateur std::io |
Secrets (ConnectionSecrets, clés) | ✅ | ❌ | jamais partagés ; zeroize au Drop | Matériel secret par connexion |
Rationnel. Une connexion TLS est un flux ordonné : deux threads ne peuvent
pas la piloter simultanément sans corrompre le key schedule. On rend donc la
connexion Send (déplaçable entre threads : passage à un worker) mais !Sync
(un seul propriétaire actif) → zéro contention, zéro Mutex, zéro unsafe. La
scalabilité vient du Config partagé Arc + N connexions indépendantes, une
par cœur (thread-per-core). C’est le patron le plus sûr et le plus rapide.
!Syncest prouvé à la compilation (un secret non-Syncdans la structure) et testé compile-fail, commeair-threadteste!Syncde ses primitives.
Anatomie : les 9 composants appliqués à TLS 1.3
Application stricte de la note réseau §2.2.
Un composant absent par conception est marqué (// NO … : raison).
| # | Composant | Réalisation air-tls | Pur ? |
|---|---|---|---|
| 1 | Framer | RecordFramer — délimite le flux TCP en TLSPlaintext/TLSCiphertext (en-tête 5 o : type, legacy_version, longueur ≤ 2¹⁴+256) | ✅ |
| 2 | Codec | RecordCodec, HandshakeCodec, ExtensionCodec, AlertCodec, X509Codec (DER/ASN.1) — (dé)sérialisation typée | ✅ |
| 3 | StateMachine | HandshakeStateMachine — transitions RFC 8446 §A (client & serveur), légalité état×message | ✅ |
| 4 | Handshaker | KeySchedule (HKDF-Expand-Label, RFC 8446 §7.1), TranscriptHash, échange X25519, auth (signature/vérif) — consomme air-crypto | ✅ + air-crypto |
| 5 | Flow Controller | // NO FLOW CONTROL : le fenêtrage est assuré par TCP (L1) sous le record layer ; TLS n'a pas de crédits. (QUIC/h2 le portent, pas TLS.) | — (absent voulu) |
| 6 | Multiplexer | // NO MULTIPLEXING : une connexion TLS = un flux d'octets. Le multiplexage de streams appartient à h2/QUIC au-dessus. | — (absent voulu) |
| 7 | Timer Manager | TlsTimers — timeout de handshake, durée de vie des tickets ; horloge injectée (Clock trait) → tests déterministes | ✅ (horloge injectée) |
| 8 | Session Context | ConnectionSecrets / SessionContext — secrets de trafic, params négociés, état de reprise ; zeroize au Drop | ✅ |
| 9 | Extension hooks | Extension trait + ExtensionRegistry — SNI, ALPN, key_share, supported_versions, signature_algorithms, psk, early_data, status_request, cookie | ✅ |
Inventaire des objets à créer (le décompte)
~55 objets Rust à produire, dont ~50 concrets + ~5 marqueurs/traits transverses, regroupés en 5 familles. Deux composants (Flow, Mux) sont absents par conception (marqueurs, pas de type).
A. Surface publique ergonomique — 25 objets (reprend les familles s2n)
| # | Objet Rust | Remplace (s2n) | Nature |
|---|---|---|---|
| 1 | TlsClientConfig | s2n_config_* (rôle client) | struct immuable (Arc) |
| 2 | TlsClientConfigBuilder | s2n_config_new + setters | builder |
| 3 | TlsServerConfig | s2n_config_* (rôle serveur) | struct immuable (Arc) |
| 4 | TlsServerConfigBuilder | idem serveur | builder |
| 5 | TlsClient | s2n_connection_* (mode client) | machine à états Send/!Sync |
| 6 | TlsServer | s2n_connection_* (mode serveur) | machine à états Send/!Sync |
| 7 | TlsStream<T> | s2n_send/recv/negotiate/shutdown | adaptateur io::{Read,Write} |
| 8 | ClientHello | s2n_client_hello_* (25 fn) | vue en lecture |
| 9 | CertifiedKey | s2n_cert_chain_and_key_* (12 fn) | chaîne + clé privée |
| 10 | Certificate | s2n_certificate_* | cert DER parsé (emprunté) |
| 11 | PrivateKeyHandle | (clé privée s2n) | poignée opaque zeroizée |
| 12 | RootCertStore | trust store s2n_config | ancres de confiance |
| 13 | CertificateRevocationList | s2n_crl_* (9 fn) | CRL parsée |
| 14 | Psk | s2n_psk_* | identité + secret (client) |
| 15 | OfferedPsk | s2n_offered_psk_* | vue serveur du PSK offert |
| 16 | SessionTicket | s2n_session_* (3 fn) | ticket de reprise (opaque) |
| 17 | EarlyDataConfig | s2n_offered_early_data_* (4 fn) | 0-RTT (gated) |
| 18 | ClientHelloFingerprint | s2n_fingerprint_* (8 fn) | JA3/JA4 |
| 19 | ServerName | SNI | newtype validé (IDNA/ASCII) |
| 20 | AlpnProtocol | ALPN | newtype &[u8] |
| 21 | TlsError | s2n_errno/s2n_strerror | enum (ADR-019) |
| 22 | Alert | alertes TLS | enum niveau+description |
| 23 | TlsVersion | (versions) | enum — Tls13 seul |
| 24 | CipherSuite | suites | enum (3 AEAD) |
| 25 | SignatureScheme | schémas signature | enum (moderne) |
B. Traits publics (idiomes Rust — points d’extension) — 8 traits
| # | Trait | Remplace (s2n) | Rôle |
|---|---|---|---|
| 26 | ServerCertVerifier | callbacks verify config | politique de validation du cert serveur (client) |
| 27 | ClientCertVerifier | mTLS verify | validation du cert client (serveur) |
| 28 | SigningKey | clé privée | signe le CertificateVerify (abstrait le stockage) |
| 29 | AsyncSigner | s2n_async_pkey_* (8 fn) | offload de signature (HSM/process séparé) |
| 30 | SessionStore | cache de session serveur | stockage/lookup des tickets |
| 31 | Ticketer | chiffrement des tickets | scelle/ouvre les NewSessionTicket |
| 32 | PskStore | lookup PSK serveur | résout un OfferedPsk → secret |
| 33 | KeyLog | debug | export SSLKEYLOGFILE (désactivé défaut) |
+ défauts fournis : WebPkiServerVerifier, WebPkiClientVerifier (impl. RFC 5280/6125),
SingleUseTicketer (chiffre les tickets via air-crypto). (comptés en C ci-dessous.)
C. Cœur sans-IO — codecs & état — 13 objets (les 9 composants + défauts)
| # | Objet | Composant | Fichier (§structure) |
|---|---|---|---|
| 34 | RecordFramer | 1. Framer | proto/framer.rs |
| 35 | RecordCodec | 2. Codec (record) | proto/codec/record.rs |
| 36 | HandshakeCodec | 2. Codec (handshake) | proto/codec/handshake.rs |
| 37 | ExtensionCodec | 2. Codec (extensions) | proto/codec/ext.rs |
| 38 | AlertCodec | 2. Codec (alertes) | proto/codec/alert.rs |
| 39 | X509Codec | 2. Codec (X.509/ASN.1) | proto/codec/x509.rs |
| 40 | PathValidator | 2/4 (validation PKIX) | proto/x509/path.rs |
| 41 | HandshakeStateMachine | 3. StateMachine | proto/state.rs |
| 42 | KeySchedule | 4. Handshaker | proto/handshake/schedule.rs |
| 43 | TranscriptHash | 4. Handshaker | proto/handshake/transcript.rs |
| 44 | TlsTimers | 7. Timer Manager | proto/timers.rs |
| 45 | ConnectionSecrets | 8. Session Context | proto/session.rs |
| 46 | ExtensionRegistry | 9. Extension hooks | proto/ext.rs |
D. Fournisseur crypto (abstraction pluggable) — 6 traits (défaut = air-crypto)
| # | Trait | Rôle |
|---|---|---|
| 47 | CryptoProvider | agrège les 5 suivants ; AirCryptoProvider = impl défaut sur air-crypto |
| 48 | AeadRecordCipher | protection de record (AES-GCM/ChaCha20) — nonce dérivé par record (RFC 8446 §5.3) |
| 49 | KeyExchangeGroup | X25519 / hybride ML-KEM — generate, agree |
| 50 | SignatureVerifier | vérifie une signature d’un schéma donné |
| 51 | HashProvider | SHA-256/384 pour transcript + HKDF |
| 52 | SecureRandom | ré-exporte air-crypto::AirRandom (jamais de PRNG userspace) |
E. Types de valeur internes (enums wire) — ~6 objets
ContentType, HandshakeType, ExtensionType, NamedGroup, AlertDescription,
ProtocolVersion (wire 0x0303/0x0304). Petits #[repr(u8/u16)] avec
(dé)sérialisation bornée.
Total ≈ 25 (A) + 8 (B) + 3 défauts + 13 (C) + 6 (D) + 6 (E) = ~61 objets, dont ~40 « lourds » (logique réelle) et ~21 enums/newtypes/marqueurs. Absents voulus : Flow Controller, Multiplexer (marqueurs commentés, pas de type).
Codecs par standard supporté (sérialisation ↔ frames)
Exigence : pour chaque standard, au minimum un codec bytes↔frames. Trait commun, lecteur borné anti-panic (
get()), écriture append-only.
#![allow(unused)]
fn main() {
/// Lecteur d'octets borné — JAMAIS d'indexation paniquante (Principe 3).
pub struct Reader<'a> { buf: &'a [u8], cursor: usize }
impl<'a> Reader<'a> {
pub fn take(&mut self, n: usize) -> Result<&'a [u8], DecodeError>; // get(), borné
pub fn u8(&mut self) -> Result<u8, DecodeError>;
pub fn u16(&mut self) -> Result<u16, DecodeError>; // big-endian
pub fn u24(&mut self) -> Result<u32, DecodeError>; // longueurs TLS
pub fn sub(&mut self, len_bytes: usize) -> Result<Reader<'a>, DecodeError>; // vecteur préfixé
pub fn expect_end(&self) -> Result<(), DecodeError>; // pas d'octets résiduels
}
/// Tout message wire (dé)sérialise via ce trait. `decode` ne panique JAMAIS.
pub trait Codec: Sized {
fn encode(&self, out: &mut Vec<u8>);
fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError>;
/// Round-trip garanti sur entrée valide : `decode(encode(x)) == x` (property test).
}
}
Standard 1 — TLS 1.3 record + handshake (RFC 8446)
Codec record (RecordCodec) — frame de 5 octets :
#![allow(unused)]
fn main() {
pub enum ContentType { Handshake, ApplicationData, Alert, ChangeCipherSpec /*compat middlebox*/ }
pub struct TlsPlaintext<'a> { // avant chiffrement
pub content_type: ContentType,
pub legacy_version: ProtocolVersion, // 0x0303 sur le fil (RFC 8446 §5.1)
pub fragment: &'a [u8], // ≤ 2^14 octets
}
pub struct TlsCiphertext<'a> { // après chiffrement (opaque_type=ApplicationData)
pub encrypted_record: &'a [u8], // ≤ 2^14 + 256
}
// RecordCodec::decode borne la longueur ; > 2^14+256 → Alert(record_overflow).
}
Codec handshake (HandshakeCodec) — msg_type(1) ‖ length(u24) ‖ body :
#![allow(unused)]
fn main() {
pub enum HandshakeMessage {
ClientHello(ClientHelloBody),
ServerHello(ServerHelloBody),
EncryptedExtensions(Vec<ExtensionData>),
Certificate(CertificateBody), // chaîne + extensions par cert
CertificateVerify(SignatureScheme, Vec<u8>),
Finished(Vec<u8>), // HMAC du transcript
NewSessionTicket(NewSessionTicketBody),
KeyUpdate(KeyUpdateRequest),
// OMIS : HelloRequest, CertificateRequest v1.2, ServerKeyExchange… (legacy)
}
}
Codec extensions (ExtensionCodec) — type(u16) ‖ len(u16) ‖ data, liste
close (RETENU) : server_name, supported_versions, supported_groups,
signature_algorithms, key_share, pre_shared_key, psk_key_exchange_modes,
application_layer_protocol_negotiation, early_data, cookie, status_request.
Extension inconnue → ignorée gracieusement (jamais de panic ; note §2.2 #9).
Codec alertes (AlertCodec) — level(1) ‖ description(1) ; seules les
descriptions RFC 8446 §6 sont acceptées.
Standard 2 — X.509 / PKIX (RFC 5280) — X509Codec + PathValidator
#![allow(unused)]
fn main() {
pub struct Certificate<'a> { der: &'a [u8], tbs: TbsView<'a> } // vue empruntée, zéro-copie
impl<'a> Certificate<'a> {
pub fn parse(der: &'a [u8]) -> Result<Self, DecodeError>; // ASN.1 DER borné, jamais de panic
pub fn subject(&self) -> Name<'a>;
pub fn subject_alt_names(&self) -> SanIter<'a>; // dNSName / iPAddress
pub fn validity(&self) -> (AirInstant, AirInstant);
pub fn public_key(&self) -> SubjectPublicKeyInfo<'a>;
pub fn signature_scheme(&self) -> Result<SignatureScheme, TlsError>;
}
/// Construction + validation de chemin (RFC 5280 §6) : c'est la « machine à états »
/// de la validation PKIX (chaînage, dates, basicConstraints, keyUsage, EKU, SAN,
/// révocation CRL/OCSP). Détaillée en tests négatifs (cert expiré, CA non-CA, …).
pub struct PathValidator<'a> { roots: &'a RootCertStore, time: AirInstant, /* … */ }
impl<'a> PathValidator<'a> {
pub fn verify_server_chain(&self, chain: &[Certificate<'_>], name: &ServerName)
-> Result<VerifiedChain, TlsError>;
}
}
Décision ASN.1 : parseur DER maison, borné, sous-ensemble strict (uniquement les structures PKIX nécessaires) — jamais un parseur ASN.1 générique (surface hostile). Exactement l’esprit du parseur DNS d’
air-socket(sous-ensemble RFC 1035, pas de compression suivie sans borne).
Machine à états — conçue multi-thread (mono-propriétaire, sans-IO)
#![allow(unused)]
fn main() {
/// Événements rendus par le cœur au pilote I/O (aucun I/O ici).
pub enum TlsEvent {
WantsWrite, // des octets sont prêts à émettre (poll_transmit)
HandshakeComplete { alpn: Option<AlpnProtocol>, peer: Option<VerifiedChain> },
ReceivedApplicationData, // du plaintext est lisible
ReceivedAlert(Alert),
KeyUpdateRequested,
PeerClosed,
}
impl TlsClient {
/// Crée le cœur (envoie le ClientHello en interne, disponible via poll_transmit).
pub fn new(config: Arc<TlsClientConfig>, server_name: ServerName) -> Result<Self, TlsError>;
/// SANS-IO : ingère des octets réseau bruts → transitions d'état + événements.
/// Ne bloque jamais, ne touche aucun socket. Cœur de la fuzzabilité.
pub fn feed(&mut self, incoming: &[u8]) -> Result<Vec<TlsEvent>, TlsError>;
/// SANS-IO : récupère les octets à écrire sur le réseau (handshake, app data, alerts).
pub fn poll_transmit(&mut self) -> Option<&[u8]>;
/// SANS-IO : plaintext applicatif déchiffré disponible (après handshake).
pub fn read_application_data(&mut self, out: &mut [u8]) -> Result<usize, TlsError>;
/// SANS-IO : chiffre du plaintext applicatif → mis en file de `poll_transmit`.
pub fn write_application_data(&mut self, plaintext: &[u8]) -> Result<usize, TlsError>;
/// SANS-IO : échéance (handshake timeout…). Horloge injectée → test déterministe.
pub fn handle_timeout(&mut self, now: AirInstant) -> Result<Vec<TlsEvent>, TlsError>;
pub fn is_handshaking(&self) -> bool;
pub fn send_key_update(&mut self, request_update: bool) -> Result<(), TlsError>;
pub fn send_close_notify(&mut self);
}
// `TlsServer` : symétrique (new(config) ; feed du ClientHello ; ServerHello/EE/Cert/CV/Finished).
}
États internes (HandshakeStateMachine, RFC 8446 §A) — client :
Start → WaitServerHello → WaitEncryptedExtensions → WaitCertificate → WaitCertificateVerify → WaitFinished → Connected. Toute réception hors-séquence →
Alert(unexpected_message) + TlsError, jamais de panic. La table
état × message est couverte à 100 % (property/model-based, note §2.2 #3).
Pilotage (pilote I/O, io/) — le seul endroit qui touche air-socket :
#![allow(unused)]
fn main() {
impl<T: Read + Write> TlsStream<T> { // T = AirTcpStream (air-socket)
pub fn connect(config: Arc<TlsClientConfig>, name: ServerName, transport: T)
-> Result<Self, TlsError>; // boucle feed/poll_transmit jusqu'à HandshakeComplete
}
impl<T: Read + Write> Read for TlsStream<T> { /* … */ } // idiome std
impl<T: Read + Write> Write for TlsStream<T> { /* … */ }
// Variante async : pilote sur air-runtime (buffers possédés, ADR-038) — même cœur.
}
Surface publique par famille (reprise de s2n, en idiome Rust)
Configuration (≈ s2n_config_*, 75 fn)
#![allow(unused)]
fn main() {
impl TlsClientConfigBuilder {
pub fn new() -> Self;
pub fn with_root_certificates(self, roots: RootCertStore) -> Self;
pub fn with_server_verifier(self, v: Arc<dyn ServerCertVerifier>) -> Self;
pub fn with_client_auth(self, key: Arc<dyn SigningKey>, chain: CertifiedKey) -> Self; // mTLS
pub fn with_alpn_protocols(self, protos: &[AlpnProtocol]) -> Self;
pub fn with_cipher_suites(self, suites: &[CipherSuite]) -> Self; // sous-ensemble des 3
pub fn with_key_exchange_groups(self, groups: &[NamedGroup]) -> Self;
pub fn with_session_resumption(self, store: Arc<dyn SessionStore>) -> Self;
pub fn enable_early_data(self, max: u32) -> Self; // 0-RTT, off par défaut
pub fn with_key_log(self, log: Arc<dyn KeyLog>) -> Self;
pub fn build(self) -> Result<Arc<TlsClientConfig>, TlsError>; // valide en amont (Principe 4)
}
// TlsServerConfigBuilder : with_certificate(CertifiedKey), with_client_cert_verifier,
// with_ticketer, with_alpn_select_callback, with_max_early_data, …
}
Les setters
s2nde legacy/tuning obsolète (versions min/max < 1.3, suites CBC, renégociation, cache d’anciennes sessions…) n’existent pas — omission = sécurité.
Connexion & I/O (≈ s2n_connection_* 91 + I/O 16)
Fusionnées dans TlsClient/TlsServer (sans-IO, ci-dessus) + TlsStream (bloquant).
Introspection post-handshake : .negotiated_alpn(), .protocol_version() (toujours
Tls13), .negotiated_cipher_suite(), .peer_certificates(), .is_resumed().
Introspection ClientHello (≈ s2n_client_hello_*, 25 fn) & Fingerprint (8 fn)
#![allow(unused)]
fn main() {
impl<'a> ClientHello<'a> {
pub fn server_name(&self) -> Option<ServerName>;
pub fn alpn_protocols(&self) -> AlpnIter<'a>;
pub fn cipher_suites(&self) -> &[CipherSuite];
pub fn supported_groups(&self) -> &[NamedGroup];
pub fn signature_schemes(&self) -> &[SignatureScheme];
pub fn raw_extensions(&self) -> ExtensionIter<'a>;
pub fn fingerprint(&self, kind: FingerprintKind) -> ClientHelloFingerprint; // JA3/JA4
}
}
Certificats, trust, révocation (≈ s2n_cert_* / crl_*)
#![allow(unused)]
fn main() {
impl CertifiedKey {
pub fn from_pem(cert_chain_pem: &[u8], key_pem: &[u8]) -> Result<Self, TlsError>;
pub fn from_der(chain: Vec<Vec<u8>>, key: PrivateKeyHandle) -> Result<Self, TlsError>;
}
impl RootCertStore {
pub fn empty() -> Self;
pub fn add_der(&mut self, der: &[u8]) -> Result<(), TlsError>;
pub fn add_pem_bundle(&mut self, pem: &[u8]) -> Result<usize, TlsError>;
}
pub trait ServerCertVerifier: Send + Sync {
fn verify_server_cert(&self, end_entity: &Certificate<'_>, intermediates: &[Certificate<'_>],
server_name: &ServerName, ocsp: Option<&[u8]>, now: AirInstant)
-> Result<VerifiedChain, TlsError>;
fn supported_verify_schemes(&self) -> &[SignatureScheme];
}
}
PSK / reprise / 0-RTT / signature offload
#![allow(unused)]
fn main() {
pub trait SigningKey: Send + Sync {
fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result<Vec<u8>, TlsError>;
fn supported_schemes(&self) -> &[SignatureScheme];
}
pub trait AsyncSigner: Send + Sync { // ≈ s2n_async_pkey_* (offload HSM)
fn begin_sign(&self, scheme: SignatureScheme, message: Vec<u8>) -> SignTicket;
fn poll_sign(&self, ticket: &SignTicket) -> Poll<Result<Vec<u8>, TlsError>>;
}
pub trait SessionStore: Send + Sync { /* get/put tickets, expiration */ }
pub trait Ticketer: Send + Sync { // scelle/ouvre NewSessionTicket via air-crypto AEAD
fn seal(&self, state: &[u8]) -> Result<Vec<u8>, TlsError>;
fn open(&self, ticket: &[u8]) -> Result<Vec<u8>, TlsError>;
}
}
Régime de conformance (les docs de conformance à produire/rejouer)
Aligné sur la note réseau §2.6 (« vecteurs de conformité rejoués, comme les KAT d’
air-crypto») et l’homologation IETF exigée par ADR-042 §Conséquences même pour l’intégration — a fortiori pour la maison.
| Régime | Source de vérité | Ce qu’on prouve |
|---|---|---|
| Traces RFC 8448 | Example Handshake Traces for TLS 1.3 | Le handshake produit octet-pour-octet les records attendus (key schedule, transcript, Finished) — l’étalon-or de correction protocolaire |
| KAT crypto | RFC 7748/8032/8439/5869 (via air-crypto) | Primitives déjà KAT-gated (ADR-034) ; air-tls teste HKDF-Expand-Label (RFC 8446 §7.1) contre 8448 |
| BoGo suite | tests BoringSSL/rustls | Machine à états vs corpus adversarial (des milliers de cas malformés / négatifs) |
| tlsfuzzer | tlsfuzzer | Handshakes malformés → alerte correcte, jamais de panic/plaintext |
| Interop matrix | OpenSSL 3.x, rustls, s2n-tls (serveur ET client) | Négociation réelle sur loopback : les 3 suites × X25519(/hybride) × Ed25519/ECDSA/RSA |
| Fuzz par composant | cargo-fuzz | 1 cible / parseur : RecordFramer, HandshakeCodec, ExtensionCodec, X509Codec — octets arbitraires → zéro panic |
Livrable de conformance : un registre
docs/tls-conformance.md(modèledocs/libc-conformance.md) recensant chaque vecteur, son statut, et le test qui l’exerce. À créer avec l’implémentation.
Stratégie de tests
- Couverture : couche 2, cible > 90 % (CLAUDE.md) — viser 100 % sur le cœur sans-IO (surface hostile), marge tolérée sur le pilote I/O.
- Par composant, en isolation (note §2.2) : Framer/Codec fuzzés (jamais de
lecture hors-borne, jamais d’alloc non bornée) ;
Codecround-tripdecode∘encode = id; StateMachine property + model-based (transitions illégales rejetées, couverture état×message) ; Handshaker vecteurs RFC 8448 + négatifs (downgrade sentinel, signature falsifiée, cert expiré/mauvais SAN). - Session Context :
zeroizedes secrets auDropprouvé (test mémoire) ; zéro fuite inter-session (deux connexions ne partagent aucun secret). - Multi-thread :
TlsClient: Send + !Syncprouvé compile-fail ;TlsConfig: Send + Syncpartagé par N threads (stress). - Pilote I/O : intégration loopback réelle sur
air-socket(comme le fait déjàair-socket), + interop matrix. - Doctests sur la surface publique.
Récapitulatif
| Famille | Objets | Rôle |
|---|---|---|
| A. Surface publique | 25 | Config/Connexion/Stream/ClientHello/Certs/PSK/… (couverture s2n) |
| B. Traits publics (+ défauts) | 8 (+3) | Verifiers, Signers, Stores, Ticketer, KeyLog |
| C. Cœur sans-IO (9 composants) | 13 | Framer, 5 Codecs, X.509, StateMachine, KeySchedule, Timers, Secrets, Extensions |
| D. Fournisseur crypto | 6 traits | CryptoProvider (défaut = air-crypto) |
| E. Enums wire | 6 | ContentType/HandshakeType/… |
| Total | ~61 | dont ~40 « lourds », 2 composants absents voulus (Flow, Mux) |
Décisions de fond
- Contingence, pas production — production = rustls (ADR-042) ; ce document instruit la porte de sortie maison pour qu’elle soit prête (RFC requis pour basculer).
- TLS 1.3 uniquement, moderne seul — tout le legacy vulnérable omis par conception (pas de downgrade à sécuriser : il n’y a pas de code).
- Zéro tierce, zéro C, zéro
unsafe— seulesstd+ les crates Air maison (couches ≤ 1) ; primitives viaair-crypto(la frontière crypto vettée, ADR-034), jamais réimplémentées. - Sans-IO 9-composants — cœur pur fuzzable + pilote I/O mince, identique aux autres crates réseau Air (note réseau §2.2).
- Machine à états mono-propriétaire multi-thread — connexion
Send/!Sync(zéro verrou),ConfigArcSend/Syncpartagé (thread-per-core, ADR-038). - Parsing défensif intégral —
Readerborné,get(),&[u8]/&strstricts, ASN.1 en sous-ensemble strict maison (pas de parseur générique). - Conformance = vecteurs rejoués — RFC 8448, BoGo, tlsfuzzer, interop, fuzz par
composant ; registre
docs/tls-conformance.md.
Travail à reprendre
- Additifs
air-crypto(KAT-gated, ADR-034) requis pour l’interop WebPKI réelle : AES-128-GCM, ECDSA P-256/P-384, RSA-PSS (vérif.), ML-KEM-768 (hybride PQ). Sans eux, interop limitée aux pairs Ed25519/X25519/AES-256/ChaCha20. - Registre
docs/tls-conformance.md(modèlelibc-conformance.md). air-network(substrat L2) :air-tlss’y branche (connexion/listener/ path-awareness) — à spécifier en parallèle (noteapi-reseau-strategie-fr.md).- DTLS : hors périmètre (TLS sur flux uniquement ; DTLS = datagramme, décision ultérieure si besoin QUIC-indépendant).
- ABI C d’
air-tls: différée (commeair-crypto). - Décision de granularité
no_stddu cœur sans-IO (aujourd’huistd; cibleno_std + alloccohérente note §2.1).
Licence du document : MPL 2.0
Statut : Spécification de contingence air-tls maison (couche 2), TLS 1.3 pur
Rust. Instruit l’option gardée ouverte par ADR-042 §Contingence ; ne remplace pas la
décision production rustls + aws-lc-rs. Toute bascule production = RFC (ADR-015).