docs: add protocol, PKI, and split-tunnel documentation
docs/protocol.md, docs/pki.md, docs/split-tunnel.md — written from the actual implementation (pinned handshake order, ML-KEM-768/FIPS 203, seq||AEAD records with replay window, QUIC/H3 mimicry) including honest v1 limitations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+232
@@ -0,0 +1,232 @@
|
||||
# Aura PKI
|
||||
|
||||
Aura uses a small, self-contained X.509 PKI for **mutual authentication** of the inner
|
||||
handshake. A single self-signed Aura **CA** issues one **server** certificate and one
|
||||
**client** certificate per client. During the handshake the client verifies the server's
|
||||
certificate and the server verifies the client's certificate, both against the CA.
|
||||
|
||||
The PKI is implemented in the `aura-pki` crate (`ca.rs`, `cert.rs`, `store.rs`) and exposed on
|
||||
the command line as `aura pki ...` (`crates/aura-cli/src/pki.rs`,
|
||||
`crates/aura-cli/src/main.rs`).
|
||||
|
||||
> The outer QUIC/TLS layer does **not** use this PKI — it accepts any certificate (see
|
||||
> `protocol.md`, "Mimicry layer"). All certificate trust lives in the inner Aura handshake.
|
||||
|
||||
---
|
||||
|
||||
## Trust model
|
||||
|
||||
```
|
||||
Aura CA (self-signed)
|
||||
CN = <ca_name>, isCA, keyCertSign/crlSign
|
||||
|
|
||||
+------------+------------+
|
||||
| |
|
||||
server leaf client leaf(s)
|
||||
CN = <domain> CN = <client_id>
|
||||
SAN: DNS:<domain> (no SAN)
|
||||
EKU: serverAuth EKU: clientAuth
|
||||
```
|
||||
|
||||
- The **CA** is self-signed with `BasicConstraints: CA`, and key usages
|
||||
`keyCertSign` + `crlSign` + `digitalSignature`. Default lifetime **3650 days**.
|
||||
- A **server leaf** carries `CN = <domain>`, a **`DNS:<domain>` SAN**, and
|
||||
`extendedKeyUsage = serverAuth`. The DNS SAN is what the client matches against its expected
|
||||
`server_name`.
|
||||
- A **client leaf** carries `CN = <client_id>` and `extendedKeyUsage = clientAuth`. The CN is
|
||||
the identity the server learns and records as the session `peer_id`.
|
||||
- Leaf key usages are `digitalSignature` + `keyEncipherment`. Default lifetime **365 days**.
|
||||
- All issued certs (CA and leaves) backdate `not_before` by **5 minutes** to tolerate clock
|
||||
skew.
|
||||
|
||||
### Algorithms
|
||||
|
||||
All keys are **ECDSA P-256 / SHA-256** (rcgen's default `KeyPair::generate`). Private keys are
|
||||
written in **PKCS#8 PEM**. Chain verification (in `cert.rs`) accepts ECDSA P-256/SHA-256
|
||||
(required), and also ECDSA P-384/SHA-384 and Ed25519, so a deployment can switch key types
|
||||
later without code changes.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
The CLI keeps files in plain directories. Conventional names
|
||||
(`crates/aura-cli/src/pki.rs`):
|
||||
|
||||
| File | Constant | Contents |
|
||||
|---------------|------------|-------------------------------------------|
|
||||
| `ca.crt` | `CA_CERT` | CA certificate (PEM) |
|
||||
| `ca.key` | `CA_KEY` | CA private key (PKCS#8 PEM) — **secret** |
|
||||
| `server.crt` | | Server leaf certificate (PEM) |
|
||||
| `server.key` | | Server leaf private key (PEM) — **secret**|
|
||||
| `client.crt` | | Client leaf certificate (PEM) |
|
||||
| `client.key` | | Client leaf private key (PEM) — **secret**|
|
||||
| `revoked.crl` | `CRL_FILE` | Revocation list (one identifier per line) |
|
||||
|
||||
`issue-server` and `issue-client` load the CA from `ca.crt` + `ca.key` in the CA directory and
|
||||
write `server.{crt,key}` / `client.{crt,key}` into the output directory. Paths beginning with
|
||||
`~` are expanded to the home directory (from `$HOME`, or `$USERPROFILE` on Windows).
|
||||
|
||||
These names map directly onto the `[pki]` section of `server.toml` / `client.toml`
|
||||
(`ca_cert`, `cert`, `key`).
|
||||
|
||||
---
|
||||
|
||||
## `aura pki` commands
|
||||
|
||||
```
|
||||
aura pki init --ca-name <CN> --out <DIR>
|
||||
aura pki issue-server --domain <DNS> --out <DIR> [--ca <CA_DIR>]
|
||||
aura pki issue-client --id <CLIENT> --out <DIR> [--ca <CA_DIR>]
|
||||
aura pki revoke --id <ID> [--crl <PATH>]
|
||||
aura pki list [--crl <PATH>]
|
||||
```
|
||||
|
||||
For `issue-server` / `issue-client`, `--ca` defaults to the value of `--out` (so the CA and
|
||||
the issued leaf can live in the same directory). For `revoke` / `list`, `--crl` defaults to
|
||||
`./revoked.crl`.
|
||||
|
||||
### `init` — create a CA
|
||||
|
||||
Generates a fresh self-signed CA and writes `ca.crt` + `ca.key` into `--out` (creating the
|
||||
directory if needed).
|
||||
|
||||
```bash
|
||||
aura pki init --ca-name "Aura Root CA" --out ~/.aura
|
||||
# CA generated:
|
||||
# cert: ~/.aura/ca.crt
|
||||
# key: ~/.aura/ca.key
|
||||
```
|
||||
|
||||
### `issue-server` — issue a server certificate
|
||||
|
||||
Issues a server leaf for a DNS name, signed by the CA, with a `DNS:<domain>` SAN and
|
||||
`serverAuth` EKU.
|
||||
|
||||
```bash
|
||||
aura pki issue-server --domain vpn.example.com --out ~/.aura --ca ~/.aura
|
||||
# server certificate issued for 'vpn.example.com':
|
||||
# cert: ~/.aura/server.crt
|
||||
# key: ~/.aura/server.key
|
||||
```
|
||||
|
||||
> The `--domain` must equal the name the client expects in the handshake. In the shipped
|
||||
> client config that name is taken from `[client] sni`, so the camouflage SNI and the
|
||||
> verified server SAN are the same value.
|
||||
|
||||
### `issue-client` — issue a client certificate
|
||||
|
||||
Issues a client leaf with `CN = <id>` and `clientAuth` EKU. The `<id>` becomes the verified
|
||||
`peer_id` the server sees.
|
||||
|
||||
```bash
|
||||
aura pki issue-client --id laptop --out ~/.aura --ca ~/.aura
|
||||
# client certificate issued for 'laptop':
|
||||
# cert: ~/.aura/client.crt
|
||||
# key: ~/.aura/client.key
|
||||
```
|
||||
|
||||
### `revoke` — add to the revocation list
|
||||
|
||||
Adds an identifier — a **client id / Common Name** or a **certificate serial** (lowercase
|
||||
hex, no separators) — to the CRL file, creating it (and parent directories) if absent.
|
||||
|
||||
```bash
|
||||
aura pki revoke --id laptop --crl ~/.aura/revoked.crl
|
||||
# revoked 'laptop' (CRL: ~/.aura/revoked.crl)
|
||||
```
|
||||
|
||||
### `list` — show revoked identifiers
|
||||
|
||||
Prints the identifiers in the CRL file (empty if the file does not exist).
|
||||
|
||||
```bash
|
||||
aura pki list --crl ~/.aura/revoked.crl
|
||||
# revoked identifiers (CRL: ~/.aura/revoked.crl):
|
||||
# laptop
|
||||
```
|
||||
|
||||
### End-to-end example
|
||||
|
||||
```bash
|
||||
# 1. Create the CA.
|
||||
aura pki init --ca-name "Aura Root CA" --out ~/.aura
|
||||
|
||||
# 2. Issue the server cert for its public DNS name.
|
||||
aura pki issue-server --domain vpn.example.com --out ~/.aura
|
||||
|
||||
# 3. Issue a client cert per device.
|
||||
aura pki issue-client --id laptop --out ~/.aura
|
||||
|
||||
# 4. (later) Revoke a compromised client.
|
||||
aura pki revoke --id laptop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Verification is performed by `AuraCertVerifier` (`crates/aura-pki/src/cert.rs`), built from
|
||||
the CA certificate PEM. It uses **`rustls-webpki`** to validate the peer's leaf against the CA
|
||||
trust anchor. The Aura handshake invokes it on each side (see `protocol.md`).
|
||||
|
||||
**Server certificate** (`verify_server_cert`), run by the client:
|
||||
|
||||
1. webpki chain verification against the CA with key usage **`serverAuth`**, plus validity
|
||||
(time) check.
|
||||
2. The leaf must be valid for the requested `server_name` (DNS SAN match); a mismatch is
|
||||
`NameMismatch`.
|
||||
3. CRL check (see below).
|
||||
|
||||
**Client certificate** (`verify_client_cert`), run by the server:
|
||||
|
||||
1. webpki chain verification against the CA with key usage **`clientAuth`**, plus validity.
|
||||
2. The **client id** is extracted as the first Common Name from the leaf subject (missing CN
|
||||
is `MissingIdentity`).
|
||||
3. CRL check.
|
||||
4. Returns the client id, which the handshake records as the session `peer_id`.
|
||||
|
||||
The leaf certificate is sent **inline** in the handshake (DER, no intermediate chain); the CA
|
||||
is the single trust anchor. Possession of the leaf's private key is proven separately by the
|
||||
handshake signature over the transcript (see `protocol.md`).
|
||||
|
||||
Errors surface as `PkiError`: `CertParse`, `EmptyChain`, `TrustAnchor`, `Verification`,
|
||||
`NameMismatch`, `MissingIdentity`, `Revoked`.
|
||||
|
||||
---
|
||||
|
||||
## Revocation (CRL)
|
||||
|
||||
Aura v1 revocation is deliberately minimal (`crates/aura-pki/src/store.rs`). `CrlStore` is a
|
||||
**set of revoked identifier strings**, where an identifier is either:
|
||||
|
||||
- a certificate **serial number** (lowercase hex, no separators), or
|
||||
- a **client id / Common Name**.
|
||||
|
||||
During verification, if the CRL is non-empty the leaf is rejected (`Revoked`) when **either**
|
||||
its serial **or** its Common Name is present in the set. An empty CRL skips the check
|
||||
entirely.
|
||||
|
||||
The on-disk format is one identifier per line; blank lines and `#` comments are ignored on
|
||||
load. `aura pki revoke` / `aura pki list` manage this file.
|
||||
|
||||
> v1 limitation: this is a flat allow/deny set, not a signed X.509 CRL. There is no CRL
|
||||
> signature, no `nextUpdate`, and no automatic distribution — the file must be provisioned to
|
||||
> the verifying side out of band. The verifier passes `None` for webpki's own revocation
|
||||
> hooks and relies solely on this set.
|
||||
|
||||
---
|
||||
|
||||
## Security notes
|
||||
|
||||
- **Protect the private keys.** `ca.key` is the root of all trust; anyone with it can mint
|
||||
valid server/client certs. `server.key` / `client.key` must stay on their respective hosts.
|
||||
The CLI writes them with default file permissions — restrict them at the OS level.
|
||||
- **The CA is self-signed and unconstrained** (`BasicConstraints: CA` unconstrained). It is
|
||||
the sole trust anchor; there is no intermediate CA tier in v1.
|
||||
- **Server identity is name-bound.** The client only accepts a server leaf whose DNS SAN
|
||||
matches the expected name, so a different valid leaf from the same CA will not be accepted
|
||||
for the wrong host.
|
||||
- **Revocation is best-effort** (see above): plan to distribute the CRL file and keep it in
|
||||
sync on every server that verifies clients.
|
||||
- **Leaf lifetime is 365 days**; plan re-issuance. There is no automated rotation in v1.
|
||||
Reference in New Issue
Block a user