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-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-tls maison, 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 hors std (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) :

DomaineRetenuRéférence
Version protocoleTLS 1.3 uniquementRFC 8446
Suites (AEAD only)TLS_AES_128_GCM_SHA256 (MTI), TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256RFC 8446 §9.1
Échange de clésX25519 (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ésSNI, ALPN, reprise par PSK tickets (FS), KeyUpdate, downgrade sentinel, validation X.509 (RFC 5280) + hostname (RFC 6125), OCSP stapling, CRLRFC 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, seule std autorisée. Réserve technique incontournable : on ne réimplémente JAMAIS de crypto (Principe 6, ADR-034, spec air-crypto) — std n’offre aucune primitive. La résolution cohérente avec la doctrine de couches : air-tls ne 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-surface reste vert pour ce crate — c’est l’intérêt de la contingence maison vs ADR-042 (qui, lui, ouvre l’exception C aws-lc-sys).
  • std : autorisée (buffers, io::{Read,Write}, Vec, BTreeMap…). (Le cœur sans-IO vise no_std + alloc à terme, cohérent note §2.1 ; le pilote I/O reste std. 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çade AirAead à nonce aléatoire ; additif ADR-082), AirEcdh* (X25519), AirSigningKey/AirVerifyingKey (Ed25519), AirHkdf (key schedule), AirHash (transcript SHA-256/384), AirHmac, AirRandom, secrets zeroize. C’est la frontière crypto vettée (RustCrypto sous exception nommée, ADR-034) — air-tls ne 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-tls v0.1 négocie X25519 + Ed25519/AES-256-GCM/ChaCha20 uniquement (interop limitée aux pairs modernes). Chaque additif = bump air-crypto KAT-gated (ADR-034).

  • Zéro unsafe exposé et interne (le seul unsafe du projet côté crypto vit dans RustCrypto, sous air-crypto). Parsing défensif intégral (Principe 3) : get() jamais l’indexation, &[u8] vs &str stricts, 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) :

TypeSendSyncPartageRôle
TlsClientConfig / TlsServerConfigArc<…> partagé entre threads/connexions ; immuable après buildPolitique (certs, suites, verifier, ALPN, tickets)
TlsClient / TlsServer (connexion)possédée par une seule tâche/thread à la fois ; aucun verrou interneLa machine à états d’une connexion
TlsStream<T>idem, enveloppe I/O bloquanteAdaptateur std::io
Secrets (ConnectionSecrets, clés)jamais partagés ; zeroize au DropMaté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.

!Sync est prouvé à la compilation (un secret non-Sync dans la structure) et testé compile-fail, comme air-thread teste !Sync de 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).

#ComposantRéalisation air-tlsPur ?
1FramerRecordFramer — délimite le flux TCP en TLSPlaintext/TLSCiphertext (en-tête 5 o : type, legacy_version, longueur ≤ 2¹⁴+256)
2CodecRecordCodec, HandshakeCodec, ExtensionCodec, AlertCodec, X509Codec (DER/ASN.1) — (dé)sérialisation typée
3StateMachineHandshakeStateMachine — transitions RFC 8446 §A (client & serveur), légalité état×message
4HandshakerKeySchedule (HKDF-Expand-Label, RFC 8446 §7.1), TranscriptHash, échange X25519, auth (signature/vérif) — consomme air-crypto✅ + air-crypto
5Flow 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)
6Multiplexer// NO MULTIPLEXING : une connexion TLS = un flux d'octets. Le multiplexage de streams appartient à h2/QUIC au-dessus.— (absent voulu)
7Timer ManagerTlsTimers — timeout de handshake, durée de vie des tickets ; horloge injectée (Clock trait) → tests déterministes✅ (horloge injectée)
8Session ContextConnectionSecrets / SessionContext — secrets de trafic, params négociés, état de reprise ; zeroize au Drop
9Extension hooksExtension 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 RustRemplace (s2n)Nature
1TlsClientConfigs2n_config_* (rôle client)struct immuable (Arc)
2TlsClientConfigBuilders2n_config_new + settersbuilder
3TlsServerConfigs2n_config_* (rôle serveur)struct immuable (Arc)
4TlsServerConfigBuilderidem serveurbuilder
5TlsClients2n_connection_* (mode client)machine à états Send/!Sync
6TlsServers2n_connection_* (mode serveur)machine à états Send/!Sync
7TlsStream<T>s2n_send/recv/negotiate/shutdownadaptateur io::{Read,Write}
8ClientHellos2n_client_hello_* (25 fn)vue en lecture
9CertifiedKeys2n_cert_chain_and_key_* (12 fn)chaîne + clé privée
10Certificates2n_certificate_*cert DER parsé (emprunté)
11PrivateKeyHandle(clé privée s2n)poignée opaque zeroizée
12RootCertStoretrust store s2n_configancres de confiance
13CertificateRevocationLists2n_crl_* (9 fn)CRL parsée
14Psks2n_psk_*identité + secret (client)
15OfferedPsks2n_offered_psk_*vue serveur du PSK offert
16SessionTickets2n_session_* (3 fn)ticket de reprise (opaque)
17EarlyDataConfigs2n_offered_early_data_* (4 fn)0-RTT (gated)
18ClientHelloFingerprints2n_fingerprint_* (8 fn)JA3/JA4
19ServerNameSNInewtype validé (IDNA/ASCII)
20AlpnProtocolALPNnewtype &[u8]
21TlsErrors2n_errno/s2n_strerrorenum (ADR-019)
22Alertalertes TLSenum niveau+description
23TlsVersion(versions)enum — Tls13 seul
24CipherSuitesuitesenum (3 AEAD)
25SignatureSchemeschémas signatureenum (moderne)

B. Traits publics (idiomes Rust — points d’extension) — 8 traits

#TraitRemplace (s2n)Rôle
26ServerCertVerifiercallbacks verify configpolitique de validation du cert serveur (client)
27ClientCertVerifiermTLS verifyvalidation du cert client (serveur)
28SigningKeyclé privéesigne le CertificateVerify (abstrait le stockage)
29AsyncSigners2n_async_pkey_* (8 fn)offload de signature (HSM/process séparé)
30SessionStorecache de session serveurstockage/lookup des tickets
31Ticketerchiffrement des ticketsscelle/ouvre les NewSessionTicket
32PskStorelookup PSK serveurrésout un OfferedPsk → secret
33KeyLogdebugexport 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)

#ObjetComposantFichier (§structure)
34RecordFramer1. Framerproto/framer.rs
35RecordCodec2. Codec (record)proto/codec/record.rs
36HandshakeCodec2. Codec (handshake)proto/codec/handshake.rs
37ExtensionCodec2. Codec (extensions)proto/codec/ext.rs
38AlertCodec2. Codec (alertes)proto/codec/alert.rs
39X509Codec2. Codec (X.509/ASN.1)proto/codec/x509.rs
40PathValidator2/4 (validation PKIX)proto/x509/path.rs
41HandshakeStateMachine3. StateMachineproto/state.rs
42KeySchedule4. Handshakerproto/handshake/schedule.rs
43TranscriptHash4. Handshakerproto/handshake/transcript.rs
44TlsTimers7. Timer Managerproto/timers.rs
45ConnectionSecrets8. Session Contextproto/session.rs
46ExtensionRegistry9. Extension hooksproto/ext.rs

D. Fournisseur crypto (abstraction pluggable) — 6 traits (défaut = air-crypto)

#TraitRôle
47CryptoProvideragrège les 5 suivants ; AirCryptoProvider = impl défaut sur air-crypto
48AeadRecordCipherprotection de record (AES-GCM/ChaCha20) — nonce dérivé par record (RFC 8446 §5.3)
49KeyExchangeGroupX25519 / hybride ML-KEM — generate, agree
50SignatureVerifiervérifie une signature d’un schéma donné
51HashProviderSHA-256/384 pour transcript + HKDF
52SecureRandomré-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 s2n de 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égrationa fortiori pour la maison.

RégimeSource de véritéCe qu’on prouve
Traces RFC 8448Example Handshake Traces for TLS 1.3Le handshake produit octet-pour-octet les records attendus (key schedule, transcript, Finished) — l’étalon-or de correction protocolaire
KAT cryptoRFC 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 suitetests BoringSSL/rustlsMachine à états vs corpus adversarial (des milliers de cas malformés / négatifs)
tlsfuzzertlsfuzzerHandshakes malformés → alerte correcte, jamais de panic/plaintext
Interop matrixOpenSSL 3.x, rustls, s2n-tls (serveur ET client)Négociation réelle sur loopback : les 3 suites × X25519(/hybride) × Ed25519/ECDSA/RSA
Fuzz par composantcargo-fuzz1 cible / parseur : RecordFramer, HandshakeCodec, ExtensionCodec, X509Codec — octets arbitraires → zéro panic

Livrable de conformance : un registre docs/tls-conformance.md (modèle docs/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) ; Codec round-trip decode∘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 : zeroize des secrets au Drop prouvé (test mémoire) ; zéro fuite inter-session (deux connexions ne partagent aucun secret).
  • Multi-thread : TlsClient: Send + !Sync prouvé compile-fail ; TlsConfig: Send + Sync partagé 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

FamilleObjetsRôle
A. Surface publique25Config/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)13Framer, 5 Codecs, X.509, StateMachine, KeySchedule, Timers, Secrets, Extensions
D. Fournisseur crypto6 traitsCryptoProvider (défaut = air-crypto)
E. Enums wire6ContentType/HandshakeType/…
Total~61dont ~40 « lourds », 2 composants absents voulus (Flow, Mux)

Décisions de fond

  1. 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).
  2. 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).
  3. Zéro tierce, zéro C, zéro unsafe — seules std + les crates Air maison (couches ≤ 1) ; primitives via air-crypto (la frontière crypto vettée, ADR-034), jamais réimplémentées.
  4. Sans-IO 9-composants — cœur pur fuzzable + pilote I/O mince, identique aux autres crates réseau Air (note réseau §2.2).
  5. Machine à états mono-propriétaire multi-thread — connexion Send/!Sync (zéro verrou), Config Arc Send/Sync partagé (thread-per-core, ADR-038).
  6. Parsing défensif intégralReader borné, get(), &[u8]/&str stricts, ASN.1 en sous-ensemble strict maison (pas de parseur générique).
  7. 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èle libc-conformance.md).
  • air-network (substrat L2) : air-tls s’y branche (connexion/listener/ path-awareness) — à spécifier en parallèle (note api-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 (comme air-crypto).
  • Décision de granularité no_std du cœur sans-IO (aujourd’hui std ; cible no_std + alloc cohé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).