Files
AuraVPN/docs/pki.md
T
xah30 46513354c0 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>
2026-05-25 18:40:19 +03:00

9.0 KiB

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).

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.

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.

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.

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).

aura pki list --crl ~/.aura/revoked.crl
# revoked identifiers (CRL: ~/.aura/revoked.crl):
#   laptop

End-to-end example

# 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.