
Agentic-first TLS 1.3 and QUIC-TLS in pure Rust, over IronCrypto, with a machine-readable ontology.
Post-quantum by default. Sans-I/O. no_std. No C, no unsafe in the library
(#![forbid(unsafe_code)]), and no third-party dependencies. Every
cryptographic operation is IronCrypto's; IronSocketLayer implements the
protocol and nothing else. (The only unsafe in the repository is test and
measurement instrumentation: an allocation counter and the Cortex-M4 stack
probe. The excluded bench/, fuzz/ and embedded/ crates have their own
dependencies, which never reach the library's.)
$ isl probe cloudflare.com --alpn h2,http/1.1
cloudflare.com:443 state:connected
ech ech:not-offered
revocation revocation:not-checked
version version:tls1.3
cipherSuite suite:tls-aes-128-gcm-sha256
keyExchangeGroup group:x25519mlkem768
peerSignatureScheme sigscheme:ecdsa-secp256r1-sha256
peerKey key:ecdsa-p256
peerSubjectCommonName cloudflare.com
alpn h2
peerNames cloudflare.com, ns.cloudflare.com, *.ns.cloudflare.com, *.secondary.cloudflare.com, secondary.cloudflare.com
properties property:confidentiality, property:forward-secrecy, property:post-quantum-key-exchange, property:server-authenticated
Why a TLS library for agents
An autonomous agent that opens a TLS connection has three problems a human programmer mostly does not:
- It cannot read your docs and reliably act on them. It needs to know which configuration is right for this purpose, as data.
- It cannot tell what it got. "The handshake succeeded" says nothing about whether the session is post-quantum, mutually authenticated, or FIPS-approved.
- It cannot interpret a failure.
SSL_ERROR_SSLplus an error-queue string is not something a planner can branch on.
IronSocketLayer answers each one in the library itself:
| Problem | IronSocketLayer |
|---|---|
| Choosing a configuration | Named profiles and intents: isl recommend intent:agent-to-agent-mtls --post-quantum returns the profile, the rationale, the rejected alternatives with reasons, and the constraints to honour. When nothing meets the requirements it says unavailable and never substitutes a weaker profile. |
| Knowing what you got | Every connection yields a SessionReport: the safe defaults its configuration gave up (relaxations), version, suite, group, schemes, peer chain facts, the names the peer's certificate was issued for (peerNames, the ones to authorize on), the security properties that hold (property:post-quantum-key-exchange, property:mutual-authentication, property:fips-approved-algorithms, ...), FIPS service indicators, and a typed event trail. Stable camelCase JSON. |
| Understanding failure | Every error is a closed ErrorKind with a stable id (error:unknown-ca), the TLS alert it maps to, and retryable / caller_correctable / peer_fault flags, and a recovery() action to branch on (recovery:ask-user, recovery:stop, recovery:retry-with-ech-configs, ...). The ontology holds its meaning and recovery steps under the same id. |
| Discovering the library | An ontology of 104 protocol entries, 33 errors, 6 profiles and 11 intents, exported as JSON, JSON-LD, OWL/Turtle, JSON Schema and Markdown, linked into IronCrypto's ontology by builtOn edges. |
| Tool use | An MCP server (isl mcp) exposing the ontology, the recommender, the error catalog, self-tests, a live tls_probe, and offline inspect_certificate, verify_chain and check_config. |
And it removes one foot-gun entirely: there is no option to disable certificate verification. An agent that needs to talk to a peer without a PKI pins the peer's public key instead, which is stricter and easier to provision.
What is implemented
| Protocol | TLS 1.3 (RFC 8446), client and server: full handshake, RFC 9266 channel binding in one call (channel_binding()), HelloRetryRequest with cookie, mutual authentication, KeyUpdate, post-handshake client authentication (step-up: request_client_auth() before a privileged operation), 0-RTT early data (opt-in, TLS over TCP and QUIC, replay guard and freshness window on the server; rejected data returned, never silently resent), external PSKs (for devices and agent pairs with no PKI; 256-bit minimum, still with a fresh key exchange), session resumption (PSK with (EC)DHE only, stateless AES-256-GCM tickets, single-use on the client, so resumption keeps forward secrecy and post-quantum key exchange), exporters (RFC 8446 §7.5, for RFC 9266 channel binding), ALPN, SNI, close_notify, middlebox compatibility mode |
| QUIC | TLS for QUIC (RFC 9001), QUIC v1 and v2 (RFC 9369): CRYPTO-frame levels, transport parameters, Initial / Handshake / 1-RTT packet protection, header protection, 1-RTT key update |
| Cipher suites | TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256 |
| Key exchange | X25519MLKEM768, SecP256r1MLKEM768, ML-KEM-512, ML-KEM-768, X25519, P-256, P-384, P-521; SecP384r1MLKEM1024 and ML-KEM-1024 on request (not offered by default: their shares exceed 1.5 KB) |
| Signatures | ML-DSA-44, ML-DSA-65, ML-DSA-87, ECDSA P-256/P-384/P-521, Ed25519, RSA-PSS (2048–4096), RSA PKCS#1 v1.5 in certificates only |
| PKI | RFC 5280 path building and validation, name constraints (dNSName, iPAddress), EKU, RFC 6125 name matching with no CN fallback, wildcards refused directly over registry suffixes such as co.uk, SPKI pinning, a certificate builder for ephemeral agent identities, PKCS#8 and PEM loading (including OpenSSL's ML-DSA key format) |
| Revocation | CRLs (RFC 5280 §5), checked along the whole path on both sides from a caller-filled CrlStore, with require_crl to demand coverage, and x509::crl::build so a private CA can revoke an agent. OCSP stapling (RFC 6066, RFC 6960): client policy Off, IfStapled (default) or RequireStaple; staples must be signed by the issuer or its certified delegate and be current; a revoked certificate is always fatal. Servers staple a supplied response, and x509::ocsp::build_response mints one for a private CA. |
| Constrained links | record_size_limit (RFC 8449) in both directions |
| Privacy | Encrypted Client Hello (RFC 9849, HPKE per RFC 9180), client and server, over TCP and QUIC, including HelloRetryRequest and retry_configs. Configured ECH is used or the connection fails; the real name is never sent in the clear as a fallback. isl probe --ech fetches the host's configuration over DNS-over-HTTPS. Without a configuration, clients (both engines) send ECH GREASE by default, so the connections that do use ECH do not stand out. |
| Profiles | default, post-quantum, fips-140-3, cnsa-1, cnsa-2 (ML-KEM-1024, ML-DSA-87, AES-256), dal-a |
| Targets | std; no_std + alloc (builds for thumbv7em-none-eabihf); a separate fixed-capacity TLS 1.3 engine, fixed::Connection, over caller-owned storage with no allocation after initialization (host-tested; see READINESS.md) |
Named in the ontology but not implemented: X448, Ed448 and the other groups and schemes marked so there. TLS 1.2 is excluded by design. IronSocketLayer 0.7 needs IronCrypto 0.3.1 or later, below 0.4, and IronPKI 0.4 (0.4.1 from 0.7.1).
Evidence that it interoperates
Two copies of the same misreading of an RFC interoperate perfectly, so the in-memory tests are backed by handshakes with independent implementations:
- Public servers (
tests/interop.rs): Cloudflare, Google and GitHub, every suite × {X25519, P-256}, a hybrid X25519MLKEM768 handshake with Cloudflare, a real HelloRetryRequest from Google, session resumption with Cloudflare that keeps the hybrid group, and Encrypted Client Hello with Cloudflare (its trace endpoint reportssni=encrypted), including the rejection path: a key Cloudflare does not hold is refused, the retry configurations it returns are authenticated, and the retry succeeds. All with full path validation to the system trust store. - OpenSSL 3.5 (
tests/openssl_interop.rs), in both directions: the IronSocketLayer client againstopenssl s_serverfor 8 key types × 10 groups (keys generated by OpenSSL and loaded through IronSocketLayer's PKCS#8 parser, ML-DSA-65 and ML-DSA-87 included, and the groups include MLKEM1024 and SecP384r1MLKEM1024);openssl s_clientverifying IronSocketLayer-minted chains for every generated key kind × 10 groups;openssl verify -x509_strictaccepting all-ML-DSA-65 and all-ML-DSA-87 chains IronSocketLayer issues; IronSocketLayer verifying OpenSSL client certificates for mutual TLS; and session resumption in both directions (OpenSSL reportsReused, TLSv1.3against the IronSocketLayer server); and OCSP stapling in both directions: IronSocketLayer verifies OpenSSL-generated responses (SHA-1 and SHA-256 CertIDs, good and revoked), and OpenSSL'socspverifier ands_client -statusaccept responses IronSocketLayer mints; and CRLs both ways:openssl verify -crl_checkhonours CRLs IronSocketLayer issues, and IronSocketLayer honours CRLs fromopenssl ca -gencrl; and post-handshake authentication:s_client -enable_phaanswers the IronSocketLayer server's mid-session certificate request; and external PSKs both ways (s_server -psk -nocert,s_client -psk); and 0-RTT both ways (s_server -early_dataanswers a request IronSocketLayer sent only as early data, ands_client -early_datareports its early data accepted). - CNSA 2.0 with OpenSSL 3.5 (
tests/openssl_cnsa2.rs), both ways:profile:cnsa-2with the FIPS gate on, against an OpenSSL restricted to MLKEM1024, TLS_AES_256_GCM_SHA384 and mldsa87. The client verifies an OpenSSL-generated ML-DSA-87 certificate, and OpenSSL verifies an all-ML-DSA-87 chain IronSocketLayer issued. - CNSA 1.0 with OpenSSL 3.5 (
tests/openssl_cnsa1.rs):profile:cnsa-1with the FIPS gate on verifies an RSA-3072 chain OpenSSL signed with sha384WithRSAEncryption, as RFC 9151 section 5.2 requires, and the server signs the handshake with rsa_pss_rsae_sha384. - Published vectors: RFC 9001 Appendix A (Initial secrets, packet key and IV, header protection for AES and ChaCha20), RFC 9180 Appendix A.1.1 (HPKE) and the RFC 8446 HelloRetryRequest constant.
Conformance is also checked by an independent suite, tlsfuzzer: its TLS 1.3
scripts found eight defects, all fixed, and every remaining failure is
classified in docs/VERIFICATION-2026-10-06-tlsfuzzer.md
(scripts/tlsfuzzer.sh reproduces the run).
Robustness is checked separately. Eight libFuzzer targets under
AddressSanitizer cover every peer-facing parser and whole client, server and
QUIC connections, for both engines (fuzz/). Branch coverage is measured, with each refusal it
found untested now pinned by a test that fails when the check is removed
(docs/DO-178C.md).
Both interop suites are #[ignore]d by default because they need the network or
openssl:
$ cargo test -p ironsocketlayer --test interop -- --ignored
$ cargo test -p ironsocketlayer --test openssl_interop -- --ignored --test-threads=1
$ cargo test -p ironsocketlayer --test openssl_cnsa2 -- --ignored
$ cargo test -p ironsocketlayer --test openssl_cnsa1 -- --ignored
Using it
Rust, blocking
use ;
use Duration;
use ;
let config = new;
let limits = new; // handshake deadline, idle limit
let mut tls = connect_with?;
assert!;
tls.write_all?;
Rust, sans-I/O (any runtime, any transport, no_std)
let mut conn = client?;
socket.send; // ClientHello
conn.read_tls?; // server flight
socket.send; // client Finished
println!;
From an intent
use ;
// The profile `isl recommend` names, its ALPN, and its needs as required
// properties: a connection that falls short fails instead of proceeding.
let config = for_intent?;
Agent-to-agent mutual TLS with ephemeral identities
use ;
let key = generate?; // post-quantum identity
let cert = issue?;
let client = new?
.with_identity;
let server = new?
.with_client_auth;
A configuration can require the properties a task depends on; the handshake then fails, before any application data, if one does not hold:
let client = client.require;
With ML-DSA at every link of the chain, the report carries
property:post-quantum-authentication as well as
property:post-quantum-key-exchange. It carries it only then: one ECDSA
signature anywhere in the chain withdraws the claim.
QUIC
use ;
let mut tls = client?;
while let Some = tls.write_handshake
tls.read_handshake?;
while let Some = tls.next_key_change?
Command line and MCP
$ isl recommend intent:harvest-now-decrypt-later --json
$ isl ontology show group:x25519mlkem768
$ isl explain error:unknown-ca
$ isl probe example.com:443 --profile post-quantum --json
$ isl probe crypto.cloudflare.com --ech
$ isl serve --cert chain.pem --key key.pem --port 8443
$ isl inspect cert.pem # names, validity, SPKI pin; no network
$ isl verify chain.pem --roots ca.pem --name api.example
$ echo '{"side":"client","intent":"intent:https-client"}' | isl check-config -
$ isl capabilities
$ isl mcp # { "mcpServers": { "ironsocketlayer": { "command": "isl", "args": ["mcp"] } } }
Security
The 0.1.0 release was audited on 2026-10-06 against historical TLS and X.509 CVE classes, MITRE ATT&CK, NIST FIPS publications and CMMC 2.0 (report). The audit is the author's own, with AI-assisted review, and has not been independently reviewed. Its 21 findings and 12 documented weaknesses are fixed in 0.2.0 and 0.2.1, each with a test that fails without the fix. Upgrade from 0.1.0:
- GHSA-2hmg-p9h5-jccc:
ironsocketlayer0.1.0, name-constraint bypass, path-building and parsing denial of service, and truncation. - GHSA-c4hv-5693-vxvx:
isl-cli0.1.0, the MCPtls_probetool reaching internal networks.
0.2 is a minor version because its fixes change behaviour and add public fields to the configuration and report structs:
- A server refuses an SNI none of its certificates covers, with
unrecognized_name; setServerConfig::sni_fallbackfor the old behaviour. - A server refuses an external-PSK ClientHello its own process sent (the
"Selfie" reflection). A client and server in one process that share a PSK
turn off
ServerConfig::selfie_guard. - A server that requires client certificates never accepts an external PSK in their place.
Report vulnerabilities as SECURITY.md describes. An independent review is the most useful contribution this project could get; docs/REVIEW-GUIDE.md says where to start.
FIPS 140-3 and DO-178C
Neither IronSocketLayer nor IronCrypto is CMVP-validated, and neither holds a DO-178C certification. What they provide:
- FIPS 140-3: the
fips-140-3,cnsa-1anddal-aprofiles refuse to build a connection unless IronCrypto's module is operational in approved mode (ironsocketlayer::policy::enable_fips()), check every negotiated algorithm throughic_fips::check, and record the service indicators in the session report. See docs/FIPS.md. - DO-178C DAL-A: the
dal-aprofile narrows the protocol to one suite, one group, one scheme and mandatory mutual authentication. The code carries 260 tagged low-level requirements traced to high-level requirements and to their verifying tests in docs/TRACEABILITY.md. A test fails if that matrix drifts from the code. docs/DO-178C.md lists what this supports and what a certification applicant must still produce.
Compared with wolfSSL
wolfSSL is mature, small, broadly deployed, holds FIPS 140-3 certificates and sells DO-178C DAL-A packages. IronSocketLayer holds neither, and says so. What it does differently for agentic workloads is set out, with its gaps, in docs/COMPARISON.md.
Current verification results (including whole-suite branch coverage of 97.70%, with every remaining gap reviewed) and open engineering and external evidence are in the verification report and readiness status.
Repository
| Crate | |
|---|---|
crates/ironsocketlayer |
The protocol engine: TLS 1.3, QUIC-TLS, profiles, reports. X.509 is IronPKI, re-exported as x509 |
crates/isl-ontology |
The ontology: static, no_std, zero dependencies; exporters behind std |
crates/isl-cli |
isl: command line and MCP server |
bench/ |
Handshake and throughput comparison with rustls; outside the workspace |
fuzz/ |
libFuzzer targets for every peer-facing parser; outside the workspace |
$ cargo test --workspace
$ cargo clippy --workspace --all-targets
$ cargo build -p ironsocketlayer --no-default-features --target thumbv7em-none-eabihf
IronCrypto is expected at ../IronCrypto (path dependencies with a version
pin, so the workspace also resolves from a registry once published).
License
IronSocketLayer is dual-licensed, as IronCrypto is:
- AGPL-3.0-or-later for open-source use. The network clause has real reach for a TLS library: linking it into a service that terminates or originates TLS makes that service a derivative work.
- Commercial for proprietary, embedded or hosted use. Proprietary use also needs a commercial IronCrypto licence, because all cryptography is IronCrypto's.
Contributions are accepted under the CLA; see CONTRIBUTING.md. Encryption source code is export-controlled (ECCN 5D002); see docs/EXPORT.md.