publish prep: README + package hygiene (v0.1.0)

- README.md: honest, agnostic quick-start (server/client), identity
  model, feature table, invariants, scope boundary — written against
  the shipped API, sibling-crate style
- Cargo.toml: readme = README.md; exclude tests/ from the published
  package (matches the N-6 rationale — integration tests reference
  docs/reviews context and are not consumer docs)
- rust-version 1.88 / edition 2021 verified identical to the
  published siblings (alkcall 0.7.1, alktty 0.4.1, alktunnels 0.1.0)

Verification: cargo test (93 passed), cargo test --all-features
(123 passed), clippy -D warnings clean, fmt clean, doc warning-free,
cargo publish --dry-run --allow-dirty passes (16 files, 201.4KiB).
This commit is contained in:
2026-09-12 06:26:47 +00:00
parent f21b62c4b4
commit 37b5f025d0
2 changed files with 153 additions and 1 deletions

View File

@@ -6,9 +6,10 @@ rust-version = "1.88"
license = "MIT OR Apache-2.0"
description = "Shared TLS setup types: server and client rustls configs, cert resolvers, verifiers, and ACME state-machine wiring, transport-agnostic and shareable across transports."
repository = "https://git.alk.dev/alkdev/alktls"
readme = "README.md"
keywords = ["tls", "rustls", "acme", "noq", "network"]
categories = ["network-programming", "cryptography", "asynchronous"]
exclude = [".opencode/", "AGENTS.md", "tasks/", "docs/reviews/", "docs/research/", "docs/plans/", "docs/architecture/", "docs/sdd_process.md"]
exclude = [".opencode/", "AGENTS.md", "tasks/", "docs/reviews/", "docs/research/", "docs/plans/", "docs/architecture/", "docs/sdd_process.md", "tests/"]
[lib]
name = "alktls"

151
README.md Normal file
View File

@@ -0,0 +1,151 @@
# alktls
Shared TLS setup types for `rustls`: server and client config
construction, cert resolvers, verifiers, and ACME state-machine wiring —
transport-agnostic and shareable across transports.
The crate owns **config construction**: given an identity and an ALPN
list, produce a `rustls::ServerConfig` or `rustls::ClientConfig` and
hand it to whichever transport the deployment runs (QUIC, TCP+TLS, or
your own wrapper). It does not dial, accept, dispatch, or resolve peer
identities — those stay in the transport and auth layers.
Not tied to any particular stack: compose it with any transport that
consumes a `rustls` config.
## Quick start
### Server — one identity, N transports
```rust
use std::sync::Arc;
use alktls::{TlsServerConfig, TlsIdentity};
// Any of: X509 { cert, key }, RawKey(Ed25519SecretKey),
// SelfSigned, or Acme { .. } (behind the `acme` feature).
let identity = TlsIdentity::RawKey(Ed25519SecretKey::generate());
let alpn = vec![b"my/protocol".to_vec()];
let tls = Arc::new(TlsServerConfig::new(&identity, &alpn).await?);
// The same config feeds every transport — share it via Arc, never
// re-build per transport (and never spawn a second ACME machine):
let _rustls = tls.rustls_config(); // any custom wrapper
#[cfg(feature = "tcp")]
let _acceptor = tls.for_tcp_tls(); // tokio-rustls TlsAcceptor
#[cfg(feature = "noq")]
let _quic = tls.for_noq()?; // noq (QUIC) ServerConfig
```
On the ACME path the config spawns a background renewal task and
appends `acme-tls/1` to the ALPN list for you (idempotently); the
handle lives in the config, which is why `TlsServerConfig` is not
`Clone` — share via `Arc`.
### Client — pin, CA, or fail closed
```rust
use alktls::{ConnectionCredentials, RemoteIdentity, TlsClientConfig};
// Known peer: pin its fingerprint (the fingerprint IS the trust
// anchor). Pins are format-exact: `ed25519:<hex>` for raw-key
// remotes, `SHA256:<hex>` for X.509 remotes.
let creds = ConnectionCredentials::new()
.with_remote_identity(RemoteIdentity {
fingerprint: "ed25519:64-digit-lowercase-hex".into(),
});
let config = TlsClientConfig::new(&creds, b"my/protocol")?;
let _rustls = config.into_rustls_config(); // for a TCP+TLS dial
// Unknown X.509 endpoint: `remote_identity: None` → CA verification
// against the platform root store (falls back to webpki-roots when
// the platform bundle is missing, e.g. containers).
let public = ConnectionCredentials::new();
let _config = TlsClientConfig::new(&public, b"my/protocol")?;
```
Verifier selection follows the identity: known peer + fingerprint →
pin; unknown + X.509 → CA; unknown + raw key → **fail closed** at the
handshake. Unknown raw-key remotes are never downgraded to CA
verification — a raw-key remote has no CA, so it is always a known
peer.
### Identity model
[`TlsIdentity`](https://docs.rs/alktls/latest/alktls/enum.TlsIdentity.html)
is the one identity type on both paths:
| Identity | Server presents | Client auth presents |
|----------|-----------------|----------------------|
| `X509 { cert, key }` | cert chain from PEM files | cert chain from PEM files |
| `RawKey(Ed25519SecretKey)` | RFC 7250 raw public key (SPKI) | RFC 7250 raw public key |
| `SelfSigned` | generated in-memory dev cert (no SANs, never expires; pair with fingerprint pinning) | nothing (`NoClientCertResolver`) |
| `Acme { .. }` (`acme` feature) | ACME-managed cert, auto-renewed | config error — server-only |
Client-auth presentation follows the local identity: raw key and X.509
present their cert, `SelfSigned`/`None` present nothing.
### Behavior-preservation invariants
Some settings look optional but are load-bearing — the crate sets them
on every path so callers cannot forget them:
- **`max_early_data_size = u32::MAX`** on server configs (0-RTT works
out of the box), `enable_early_data = true` on the client.
- **`aws-lc-rs`** as the crypto provider everywhere.
- **`acme-tls/1` ALPN append** for the ACME path only, done by the
crate.
- **Non-empty root store** — the client CA path merges `webpki-roots`
when the platform store is empty.
- **Fail closed** — verifier selection never silently downgrades; a
wrong pin fails the handshake, not the verification posture.
- **Proof-of-possession by default** — the default server verifier
verifies the client's CertificateVerify signature against the
presented cert (raw-key and X.509 paths), so a copied cert is not
usable by a party that lacks the matching private key. The
no-possession escape hatch ([`AcceptAnyCertVerifier`](https://docs.rs/alktls/latest/alktls/struct.AcceptAnyCertVerifier.html))
exists but is never the default.
## Features
| Feature | Contents |
|---------|----------|
| *(default)* | config construction, identity/credential/fingerprint types, PEM loading, verifiers, resolvers |
| `tcp` | [`for_tcp_tls()`](https://docs.rs/alktls/latest/alktls/struct.TlsServerConfig.html#method.for_tcp_tls) — the `tokio-rustls` acceptor wrapper |
| `noq` | [`for_noq()`](https://docs.rs/alktls/latest/alktls/struct.TlsServerConfig.html#method.for_noq) — QUIC config wrapping via `noq` |
| `acme` | the ACME path — `rustls-acme` state machine, background renewal, `acme-tls/1` |
The default crate is deliberately lean (rustls + cert material + tokio
spawn); the transport-specific dependencies are opt-in.
## Scope boundary
This crate is the **cert/config provider, not the accept loop**. It
does not bind sockets, dial, or dispatch — accept loops and dial seams
live in the consumers. Handshake-time outcomes (a rejected cert, a
mismatched pin) surface through the transport's connector, not through
[`TlsError`](https://docs.rs/alktls/latest/alktls/enum.TlsError.html),
which covers config construction only. ACME state-machine runtime
events are logged in the spawned renewal task.
## Documentation
- [Architecture docs](docs/architecture/README.md) — the authoritative
spec: overview, the server and client API surfaces, and ADRs 001008
(extraction baseline, `TlsError` shape, `noq`, accessors, config-type
ownership, module layout, RFC 7250 negotiation, possession
verification).
- [API docs](https://docs.rs/alktls) — full crate documentation on
docs.rs.
## Verification
```bash
cargo test # default crate
cargo test --all-features # + tcp, noq, acme suites
cargo clippy --all-targets -- -D warnings
cargo fmt --check
```
## License
MIT OR Apache-2.0