{"id":35478148,"url":"https://github.com/atgreen/pure-tls","last_synced_at":"2026-02-05T16:07:04.099Z","repository":{"id":331557876,"uuid":"1126765128","full_name":"atgreen/pure-tls","owner":"atgreen","description":"Pure Common Lisp TLS 1.3 implementation","archived":false,"fork":false,"pushed_at":"2026-01-15T13:11:42.000Z","size":765,"stargazers_count":19,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-01-17T03:39:31.092Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/atgreen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-02T14:37:23.000Z","updated_at":"2026-01-15T13:11:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/atgreen/pure-tls","commit_stats":null,"previous_names":["atgreen/pure-tls"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/atgreen/pure-tls","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atgreen%2Fpure-tls","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atgreen%2Fpure-tls/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atgreen%2Fpure-tls/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atgreen%2Fpure-tls/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/atgreen","download_url":"https://codeload.github.com/atgreen/pure-tls/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atgreen%2Fpure-tls/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29125129,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T14:05:12.718Z","status":"ssl_error","status_checked_at":"2026-02-05T14:03:53.078Z","response_time":65,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-03T13:13:59.242Z","updated_at":"2026-02-05T16:07:04.090Z","avatar_url":"https://github.com/atgreen.png","language":"Common Lisp","readme":"# pure-tls\n\nA pure Common Lisp implementation of TLS 1.3 (RFC 8446).\n\n## Quick Start\n\n### Server\n\nHTTPS server with automatic Let's Encrypt certificates:\n\n```lisp\n(asdf:load-system :pure-tls/acme+hunchentoot)\n\n(hunchentoot:start\n  (pure-tls/acme:make-acme-acceptor \"example.com\" \"admin@example.com\"))\n```\n\nThe server obtains a certificate on first start and renews it automatically.\n\n### Client\n\nUse with drakma via cl+ssl compatibility layer (drop-in OpenSSL replacement):\n\n```lisp\n(asdf:load-system :pure-tls/cl+ssl-compat)\n(asdf:register-immutable-system \"cl+ssl\")\n(asdf:load-system :drakma)\n\n(drakma:http-request \"https://example.com/\")\n```\n\n## Features\n\n- **Pure Common Lisp** - No foreign libraries or OpenSSL dependency\n- **TLS 1.3 only** - Modern, secure protocol with simplified handshake\n- **Post-quantum ready** - X25519MLKEM768 hybrid key exchange (FIPS 203)\n- **Encrypted Client Hello (ECH)** - Encrypts SNI to protect privacy (RFC 9639)\n- **Timeouts \u0026 cancellation** - Integrated with [`cl-context`](https://github.com/atgreen/cl-context) for cooperative cancellation\n- **Automatic certificates** - Built-in ACME client for Let's Encrypt\n- **Gray streams** - Seamless integration with existing I/O code\n- **cl+ssl compatible** - Drop-in replacement API available\n- **Native trust store** - Uses Windows CryptoAPI and macOS Security.framework\n\n### Supported Cipher Suites\n\n- `TLS_CHACHA20_POLY1305_SHA256` (0x1303) - Preferred for side-channel resistance\n- `TLS_AES_256_GCM_SHA384` (0x1302)\n- `TLS_AES_128_GCM_SHA256` (0x1301)\n\n### Supported Key Exchange\n\n- **X25519MLKEM768** (hybrid post-quantum) - Combines X25519 with ML-KEM-768 (FIPS 203)\n- X25519 (Curve25519)\n- secp256r1 (P-256)\n- secp384r1 (P-384)\n\n### Supported Signature Algorithms\n\n- **Ed25519** - Edwards curve digital signature (fast, compact)\n- **Ed448** - Edwards curve digital signature (higher security, 224-bit)\n- RSA-PSS (SHA-256, SHA-384, SHA-512)\n- ECDSA with P-256 (SHA-256)\n- ECDSA with P-384 (SHA-384)\n\n### Certificate Revocation\n\n- **CRL Support** - Checks Certificate Revocation Lists (RFC 5280)\n- Parses CRL Distribution Points extension from certificates\n- Built-in HTTP client fetches CRLs (no external dependencies)\n- **CRL signature verification** - Validates CRL authenticity using issuer's public key\n- Honors `HTTP_PROXY` environment variable\n- Thread-safe caching of fetched CRLs\n- Optional revocation checking during certificate verification\n\n```lisp\n;; Enable CRL/OCSP checking during verification\n(pure-tls::verify-certificate-chain chain roots now hostname\n                                    :check-revocation t)\n\n;; Check a single certificate\n(pure-tls::check-certificate-revocation cert)\n;; Returns :valid, :revoked, :unknown, or :error\n```\n\n## Installation\n\nUsing [ocicl](https://github.com/ocicl/ocicl):\n\n```sh\nocicl install pure-tls\n```\n\nOr add to your ASDF system:\n\n```lisp\n:depends-on (#:pure-tls)\n```\n\n## Usage\n\n### HTTPS Client\n\n```lisp\n(let ((socket (usocket:socket-connect \"example.com\" 443\n                                       :element-type '(unsigned-byte 8))))\n  (pure-tls:with-tls-client-stream (tls (usocket:socket-stream socket)\n                                        :hostname \"example.com\")\n    ;; Send HTTP request\n    (write-sequence (flexi-streams:string-to-octets\n                      \"GET / HTTP/1.1\\r\\nHost: example.com\\r\\nConnection: close\\r\\n\\r\\n\"\n                      :external-format :utf-8)\n                    tls)\n    (force-output tls)\n    ;; Read response\n    (loop for byte = (read-byte tls nil nil)\n          while byte\n          do (write-char (code-char byte)))))\n;; Stream automatically closed\n```\n\n### With Certificate Verification\n\n```lisp\n(pure-tls:with-tls-client-stream (tls socket\n                                      :hostname \"example.com\"\n                                      :verify pure-tls:+verify-peer+)\n  (do-something-with tls))\n```\n\n### ALPN Protocol Negotiation\n\n```lisp\n(pure-tls:with-tls-client-stream (tls socket\n                                      :hostname \"example.com\"\n                                      :alpn-protocols '(\"h2\" \"http/1.1\"))\n  (format t \"Selected protocol: ~A~%\" (pure-tls:tls-selected-alpn tls)))\n```\n\n### Timeouts and Cancellation\n\nControl operation timeouts and cancel in-flight operations using [`cl-context`](https://github.com/atgreen/cl-context). Contexts propagate automatically via `*current-context*` — no explicit parameter passing required.\n\n**Basic timeout:**\n\n```lisp\n;; Timeout entire TLS operation (handshake + I/O) after 30 seconds\n(cl-context:with-timeout-context (_ 30)\n  (let ((socket (usocket:socket-connect \"slow-server.com\" 443\n                                         :element-type '(unsigned-byte 8))))\n    (pure-tls:with-tls-client-stream (tls (usocket:socket-stream socket)\n                                          :hostname \"slow-server.com\")\n      ;; Both handshake and reads respect the 30s deadline\n      (read-line tls))))\n;; Raises pure-tls:tls-deadline-exceeded if timeout is exceeded\n```\n\n**User cancellation:**\n\n```lisp\n;; Cooperative cancellation - checked at I/O boundaries\n(multiple-value-bind (cancel-ctx cancel-fn)\n    (cl-context:with-cancel (cl-context:background))\n  (bt2:make-thread\n    (lambda ()\n      (let ((cl-context:*current-context* cancel-ctx))\n        (pure-tls:make-tls-client-stream socket :hostname \"example.com\"))))\n  ;; Later, when user clicks \"Cancel\":\n  (funcall cancel-fn))  ; Interrupts at next blocking operation\n;; Raises pure-tls:tls-context-cancelled at next check point\n```\n\n**Composable deadlines:**\n\n```lisp\n;; Parent deadline automatically propagates to all operations\n(cl-context:with-timeout-context (_ 60)\n  (pure-tls:with-tls-client-stream (tls socket :hostname \"example.com\")\n    (write-http-request tls)\n    (read-http-response tls)))  ; All I/O shares same 60s budget\n```\n\n**cl+ssl compatibility layer:**\n\n```lisp\n;; Works seamlessly with cl+ssl API\n(cl-context:with-timeout-context (_ 30)\n  (cl+ssl:with-global-context ((cl+ssl:make-context))\n    (cl+ssl:make-ssl-client-stream socket :hostname \"example.com\")))\n```\n\n**Benefits:**\n- **Bounded operations** - Timeouts checked at I/O boundaries\n- **Responsive UIs** - Cancel between operations\n- **DoS protection** - Enforce per-connection time limits\n- **Better testing** - Deterministic timeout behavior\n\n**Timeout behavior (cooperative checking):**\n- Checks occur *before* each blocking read, not during\n- Existing blocking reads complete before timeout is detected\n- Effective for slow servers (long waits between messages)\n- Not effective for slow reads (partial data trickling in)\n\n**When timeout checks occur:**\n- Before each TLS record read\n- Between handshake state transitions\n- Before stream read operations\n- Currently NOT implemented for CRL fetching\n\n### TLS Server\n\n```lisp\n(let ((server (usocket:socket-listen \"0.0.0.0\" 8443)))\n  (loop\n    (let ((client (usocket:socket-accept server :element-type '(unsigned-byte 8))))\n      (pure-tls:with-tls-server-stream (tls (usocket:socket-stream client)\n                                            :certificate \"/path/to/cert.pem\"\n                                            :key \"/path/to/key.pem\")\n        (handle-request tls)))))\n```\n\n### Server with Client Certificate Authentication (mTLS)\n\n```lisp\n(pure-tls:make-tls-server-stream stream\n  :certificate \"/path/to/server-cert.pem\"\n  :key \"/path/to/server-key.pem\"\n  :verify pure-tls:+verify-required+)  ; Require client certificate\n```\n\n### Server with SNI Callback (Virtual Hosting)\n\n```lisp\n(defun my-sni-callback (hostname)\n  \"Return certificate and key based on client-requested hostname.\n   Return :reject to send an unrecognized_name alert and abort the handshake.\"\n  (cond\n    ((string= hostname \"site-a.example.com\")\n     (values (pure-tls:load-certificate-chain \"/certs/site-a.pem\")\n             (pure-tls:load-private-key \"/certs/site-a-key.pem\")))\n    ((string= hostname \"site-b.example.com\")\n     (values (pure-tls:load-certificate-chain \"/certs/site-b.pem\")\n             (pure-tls:load-private-key \"/certs/site-b-key.pem\")))\n    ((string= hostname \"blocked.example.com\")\n     :reject)  ; Reject unknown/blocked hostnames with unrecognized_name alert\n    (t nil)))  ; Use default certificate\n\n(pure-tls:make-tls-server-stream stream\n  :certificate \"/path/to/default-cert.pem\"\n  :key \"/path/to/default-key.pem\"\n  :sni-callback #'my-sni-callback)\n```\n\n### Using the cl+ssl Compatibility Layer\n\nThe `pure-tls/cl+ssl-compat` system provides a drop-in replacement for cl+ssl,\nallowing existing code using cl+ssl to work with pure-tls without modification.\n\n```lisp\n(asdf:load-system :pure-tls/cl+ssl-compat)\n\n;; Use familiar cl+ssl API\n(cl+ssl:make-ssl-client-stream stream\n  :hostname \"example.com\"\n  :verify :optional)\n```\n\nThe compatibility layer supports:\n- `cl+ssl:make-ssl-client-stream` / `cl+ssl:make-ssl-server-stream`\n- `cl+ssl:make-context` / `cl+ssl:with-global-context` / `cl+ssl:call-with-global-context`\n- `cl+ssl:stream-fd` (converts file descriptors back to streams)\n- Certificate functions and verification constants\n\n### Replacing cl+ssl in Existing Applications\n\nTo use pure-tls instead of cl+ssl in an application that depends on libraries\nrequiring cl+ssl (such as drakma), use `asdf:register-immutable-system` to\nprevent ASDF from loading the real cl+ssl:\n\n```lisp\n;;; In your .asd file, before the defsystem:\n(eval-when (:compile-toplevel :load-toplevel :execute)\n  ;; Load pure-tls compatibility layer first\n  (asdf:load-system :pure-tls/cl+ssl-compat)\n  ;; Tell ASDF that \"cl+ssl\" is already satisfied - never load the real one\n  (asdf:register-immutable-system \"cl+ssl\"))\n\n(asdf:defsystem \"my-application\"\n  :depends-on (:drakma ...)  ; drakma depends on cl+ssl, but won't load it\n  ...)\n```\n\nThis technique:\n1. Loads the pure-tls compatibility layer, which defines the `CL+SSL` package\n2. Registers \"cl+ssl\" as an immutable system, so ASDF treats it as already loaded\n3. When drakma (or any library) requests `:cl+ssl`, ASDF skips loading it\n\nThis allows you to eliminate OpenSSL as a dependency entirely, making your\napplication fully portable pure Common Lisp for TLS.\n\n## ACME Client (Let's Encrypt)\n\nThe `pure-tls/acme` system provides automatic certificate management using the ACME protocol (RFC 8555), compatible with Let's Encrypt and other ACME-compliant certificate authorities.\n\n### Multi-Domain Certificates\n\n```lisp\n(pure-tls/acme:make-acme-acceptor\n  '(\"example.com\" \"www.example.com\" \"api.example.com\")\n  \"admin@example.com\"\n  :renewal-days 30)\n```\n\n### Certificate Profiles\n\npure-tls supports [Let's Encrypt certificate profiles](https://letsencrypt.org/docs/profiles/), defaulting to `tlsserver` for modern, lean certificates optimized for TLS 1.3:\n\n```lisp\n;; Default: tlsserver profile (recommended for pure-tls)\n(pure-tls/acme:make-acme-acceptor \"example.com\" \"admin@example.com\")\n\n;; Short-lived certificates (~6 days, no revocation info)\n(pure-tls/acme:make-acme-acceptor \"example.com\" \"admin@example.com\"\n  :profile \"shortlived\")\n\n;; Classic 90-day certificates with longer validation windows\n(pure-tls/acme:make-acme-acceptor \"example.com\" \"admin@example.com\"\n  :profile \"classic\")\n```\n\n| Profile | Validity | Auth Reuse | Max Domains | Notes |\n|---------|----------|------------|-------------|-------|\n| `tlsserver` | 90 days | 7 hours | 25 | Default. Smaller certs, removes legacy fields |\n| `shortlived` | ~6 days | 7 hours | 25 | No CRL/OCSP needed. Requires reliable automation |\n| `classic` | 90 days | 30 days | 100 | Let's Encrypt default. Larger certs |\n\nTo change the global default:\n\n```lisp\n(setf pure-tls/acme:*default-profile* \"shortlived\")\n```\n\n### ACME Systems\n\nThe ACME functionality is split into two systems:\n\n- **`pure-tls/acme`** - Core ACME client (no web server dependency)\n- **`pure-tls/acme+hunchentoot`** - Hunchentoot integration with `acme-acceptor`\n\nUse `pure-tls/acme` directly if you're using a different web server.\n\n### Non-Hunchentoot Usage\n\nFor other web servers, use the ACME client directly with the `:certificate-provider` callback:\n\n```lisp\n(asdf:load-system :pure-tls/acme)\n\n;; Create store and client\n(defvar *store* (pure-tls/acme:make-cert-store))\n(defvar *client* (pure-tls/acme:make-acme-client\n                   :directory-url pure-tls/acme:*production-url*\n                   :store *store*))\n\n;; Thread-safe validation state for challenges\n(defvar *validation-lock* (bt:make-lock \"validation\"))\n(defvar *validation-cert* nil)\n(defvar *validation-key* nil)\n\n;; Certificate provider for your TLS server\n(defun my-certificate-provider (hostname alpn-list)\n  (when (member \"acme-tls/1\" alpn-list :test #'string=)\n    (bt:with-lock-held (*validation-lock*)\n      (when (and *validation-cert* *validation-key*)\n        (values (list *validation-cert*) *validation-key* \"acme-tls/1\")))))\n\n;; Use with pure-tls server streams\n(pure-tls:make-tls-server-stream stream\n  :certificate \"/path/to/cert.pem\"\n  :key \"/path/to/key.pem\"\n  :certificate-provider #'my-certificate-provider)\n```\n\n### Certificate Storage\n\nCertificates are stored in platform-appropriate locations:\n\n| Platform | Default Path |\n|----------|-------------|\n| Linux    | `~/.local/state/pure-tls/` |\n| macOS    | `~/Library/Application Support/pure-tls/` |\n| Windows  | `%LOCALAPPDATA%\\pure-tls\\` |\n\nTo use a custom location:\n\n```lisp\n(pure-tls/acme:make-cert-store :base-path #p\"/etc/ssl/acme/\")\n```\n\n### TLS-ALPN-01 Challenge\n\npure-tls/acme uses the TLS-ALPN-01 challenge type, which validates domain ownership by serving a special self-signed certificate on port 443. This is ideal for:\n\n- Servers that already run on port 443 (challenges handled inline)\n- Environments where HTTP port 80 is not available\n- Automatic renewal without service interruption\n\n**Requirements:**\n- Port 443 must be accessible from the internet\n- The domain must resolve to your server's IP address\n\n### Configuration Options\n\n```lisp\n(pure-tls/acme:make-acme-acceptor domains email\n  :port 443              ; HTTPS port (default 443)\n  :production t          ; Use Let's Encrypt production (default T)\n  :profile \"tlsserver\"   ; Certificate profile (default \"tlsserver\")\n  :renewal-days 30       ; Renew when cert expires within N days\n  :store store           ; Custom cert-store (optional)\n  :logger #'my-logger)   ; Custom logging function (optional)\n```\n\n### Debugging\n\nEnable debug logging:\n\n```lisp\n(setf pure-tls/acme:*acme-debug* t)\n```\n\n### Testing with Pebble\n\nFor local development, use [Pebble](https://github.com/letsencrypt/pebble), a small ACME test server:\n\n```bash\n# Start Pebble (requires podman or docker)\ncd test/acme\n./run-pebble.sh start\n\n# Run tests\nsbcl --load quick-pebble-test.lisp\n\n# Stop Pebble\n./run-pebble.sh stop\n```\n\n## API Reference\n\n### Stream Creation\n\n#### `with-tls-client-stream` ((var stream \u0026rest args) \u0026body body)\n\nExecute BODY with VAR bound to a TLS client stream. The stream is automatically closed when BODY exits (normally or via non-local exit).\n\n```lisp\n(pure-tls:with-tls-client-stream (tls socket :hostname \"example.com\")\n  (write-sequence data tls)\n  (force-output tls)\n  (read-sequence buffer tls))\n;; tls is automatically closed here\n```\n\n#### `with-tls-server-stream` ((var stream \u0026rest args) \u0026body body)\n\nExecute BODY with VAR bound to a TLS server stream. The stream is automatically closed when BODY exits.\n\n#### `make-tls-client-stream` (socket \u0026key hostname sni-hostname context verify alpn-protocols ech-configs ech-enabled close-callback external-format buffer-size)\n\nCreate a TLS client stream over a TCP socket.\n\n- `socket` - The underlying TCP stream\n- `hostname` - Server hostname for SNI and certificate verification\n- `sni-hostname` - Override hostname for SNI only (no certificate hostname verification)\n- `context` - TLS context for configuration (optional)\n- `verify` - Certificate verification mode: `+verify-none+`, `+verify-peer+`, or `+verify-required+`\n- `alpn-protocols` - List of ALPN protocol names to offer\n- `ech-configs` - ECH configurations for Encrypted Client Hello (from DNS HTTPS record or manual)\n- `ech-enabled` - Whether to use ECH when configs are available (default T)\n- `close-callback` - Function called when stream is closed\n- `external-format` - If specified, wrap in a flexi-stream for character I/O\n- `buffer-size` - Size of I/O buffers (default 16384)\n\n#### `make-tls-server-stream` (socket \u0026key context certificate key verify alpn-protocols sni-callback close-callback external-format buffer-size)\n\nCreate a TLS server stream over a TCP socket.\n\n- `socket` - The underlying TCP stream\n- `context` - TLS context for configuration (optional)\n- `certificate` - Certificate chain (list of certificates or path to PEM file)\n- `key` - Private key (Ironclad key object or path to PEM file)\n- `verify` - Client certificate verification mode: `+verify-none+`, `+verify-peer+`, or `+verify-required+`\n- `alpn-protocols` - List of ALPN protocol names the server supports\n- `sni-callback` - Function called with client's requested hostname, returns (VALUES cert-chain private-key), NIL to use defaults, or :REJECT to abort with unrecognized_name alert\n- `close-callback` - Function called when stream is closed\n- `external-format` - If specified, wrap in a flexi-stream for character I/O\n- `buffer-size` - Size of I/O buffers (default 16384)\n\n### Stream Accessors\n\n- `(tls-peer-certificate stream)` - Returns the peer's X.509 certificate\n- `(tls-peer-certificate-chain stream)` - Returns the peer's full certificate chain\n- `(tls-selected-alpn stream)` - Returns the negotiated ALPN protocol\n- `(tls-cipher-suite stream)` - Returns the negotiated cipher suite\n- `(tls-version stream)` - Returns the TLS version (always 1.3)\n- `(tls-client-hostname stream)` - Returns the client's SNI hostname (server-side only)\n- `(tls-ech-accepted-p stream)` - Returns T if ECH was used and accepted (client-side only)\n- `(tls-request-key-update stream \u0026key request-peer-update)` - Request a TLS 1.3 key update\n\n### Context Management\n\n#### `make-tls-context` (\u0026key verify-mode certificate-chain private-key alpn-protocols ca-certificates)\n\nCreate a reusable TLS context for configuration.\n\n### Verification Modes\n\n- `+verify-none+` (0) - No certificate verification\n- `+verify-peer+` (1) - Verify peer certificate if provided\n- `+verify-required+` (2) - Require and verify peer certificate\n\n## Certificate Verification\n\n### Windows\n\nOn Windows, pure-tls uses the Windows CryptoAPI to validate certificates\nagainst the system certificate store. This is the authoritative verification\nmethod on Windows - there is no fallback to pure Lisp verification:\n\n- **No CA bundle needed** - Uses Windows trusted root certificates\n- **Enterprise PKI support** - Respects Group Policy certificate deployments\n- **Automatic updates** - Trust store is maintained by Windows Update\n- **Authoritative** - CryptoAPI verdict is final; if it rejects a certificate, the connection fails\n\nTo disable native verification and use pure Lisp verification instead\n(requires providing CA certificates manually):\n\n```lisp\n(setf pure-tls:*use-windows-certificate-store* nil)\n```\n\n### macOS\n\nOn macOS, pure-tls uses the Security.framework to validate certificates\nagainst the system Keychain. This is the authoritative verification\nmethod on macOS - there is no fallback to pure Lisp verification:\n\n- **No CA bundle needed** - Uses macOS Keychain trusted root certificates\n- **Enterprise PKI support** - Respects MDM-deployed certificates\n- **Automatic updates** - Trust store is maintained by macOS updates\n- **Authoritative** - Keychain verdict is final; if it rejects a certificate, the connection fails\n\nTo disable native verification and use pure Lisp verification instead\n(requires providing CA certificates manually):\n\n```lisp\n(setf pure-tls:*use-macos-keychain* nil)\n```\n\n### Linux\n\nOn Linux, pure-tls uses pure Lisp certificate verification\nand automatically searches for CA certificates:\n\n1. `SSL_CERT_FILE` environment variable\n2. `SSL_CERT_DIR` environment variable\n3. Platform-specific locations:\n   - `/etc/ssl/certs/ca-certificates.crt` (Debian/Ubuntu)\n   - `/etc/pki/tls/certs/ca-bundle.crt` (RHEL/CentOS)\n   - Homebrew OpenSSL paths\n\nIf CA certificates are not found automatically:\n\n```sh\nexport SSL_CERT_FILE=/path/to/cacert.pem\n```\n\nOr download the Mozilla CA bundle from https://curl.se/ca/cacert.pem\n\n### Custom CA Certificates\n\nFor corporate environments or testing with custom CAs:\n\n```lisp\n;; Use a specific CA bundle file\n(pure-tls:make-tls-context :ca-file \"/path/to/ca-bundle.crt\")\n\n;; Use a directory of certificates\n(pure-tls:make-tls-context :ca-directory \"/path/to/certs/\")\n\n;; Add corporate CA alongside system certificates\n(pure-tls:make-tls-context :ca-file \"/path/to/corporate-ca.pem\")\n\n;; Use only custom CA (skip system certificates)\n(pure-tls:make-tls-context\n  :ca-file \"/path/to/custom-ca.pem\"\n  :auto-load-system-ca nil)\n```\n\n## Side-Channel Hardening\n\npure-tls implements several measures to mitigate side-channel attacks:\n\n### Constant-Time Operations\n\nAll security-sensitive comparisons (MAC verification, key comparison) use Ironclad's constant-time comparison functions to prevent timing attacks. The implementation avoids early-return patterns that could leak information about secret data.\n\n### Uniform Error Handling\n\nAll decryption failures produce identical error conditions (`tls-mac-error`) regardless of the failure cause, as required by RFC 8446. This prevents padding oracle attacks by ensuring attackers cannot distinguish between different types of decryption failures.\n\n### Secret Zeroization\n\nSensitive cryptographic material can be explicitly cleared from memory using the `zeroize` function or the `with-zeroized-vector` macro:\n\n```lisp\n;; Explicit zeroization\n(let ((key (derive-key ...)))\n  (unwind-protect\n      (use-key key)\n    (pure-tls:zeroize key)))\n\n;; RAII-style zeroization\n(pure-tls:with-zeroized-vector (key (derive-key ...))\n  (use-key key))\n;; key is automatically zeroed here, even if an error occurs\n```\n\nNote: In a garbage-collected runtime, zeroization is best-effort as the GC may have already copied the data. For highest security requirements, consider foreign memory that can be mlock'd.\n\n### TLS 1.3 Record Padding\n\nRecord padding helps mitigate traffic analysis by hiding the true length of application data. Configure padding via `*record-padding-policy*`:\n\n```lisp\n;; Pad all records to 256-byte boundaries\n(setf pure-tls:*record-padding-policy* :block-256)\n\n;; Pad to 1024-byte boundaries\n(setf pure-tls:*record-padding-policy* :block-1024)\n\n;; Fixed-size records (4096 bytes)\n(setf pure-tls:*record-padding-policy* :fixed-4096)\n\n;; Custom padding function\n(setf pure-tls:*record-padding-policy*\n      (lambda (plaintext-length)\n        (* 128 (ceiling plaintext-length 128))))\n\n;; No padding (default)\n(setf pure-tls:*record-padding-policy* nil)\n```\n\n### Side-Channel Considerations\n\n- **ChaCha20-Poly1305 (Recommended)**: This cipher suite uses only ARX (add-rotate-xor) operations, which are inherently constant-time and resistant to cache-timing attacks. It is the preferred cipher suite for pure software implementations.\n- **AES-GCM**: Since Ironclad implements AES in pure Common Lisp using table lookups (rather than hardware AES-NI instructions), the AES-GCM cipher suites may be susceptible to cache-timing attacks. When possible, prefer ChaCha20-Poly1305 for better side-channel resistance.\n\n## Post-Quantum Key Exchange\n\npure-tls supports **X25519MLKEM768**, a hybrid post-quantum key exchange that combines classical X25519 with the ML-KEM-768 lattice-based algorithm (FIPS 203). This provides defense against \"harvest now, decrypt later\" attacks where adversaries collect encrypted traffic today to decrypt with future quantum computers.\n\n### How It Works\n\nX25519MLKEM768 performs two key exchanges in parallel:\n\n1. **X25519** - Classical elliptic curve Diffie-Hellman (128-bit security)\n2. **ML-KEM-768** - Lattice-based key encapsulation (192-bit post-quantum security)\n\nThe shared secrets are concatenated, ensuring security even if one algorithm is broken.\n\n### Automatic Negotiation\n\nPost-quantum key exchange is negotiated automatically when both client and server support it:\n\n```lisp\n;; Client and server negotiate X25519MLKEM768 if both support it\n;; No configuration needed - it's the preferred key exchange\n(pure-tls:make-tls-client-stream stream :hostname \"example.com\")\n```\n\n### Browser Compatibility\n\nMajor browsers support X25519MLKEM768:\n- **Chrome 124+** - Enabled by default\n- **Firefox** - Behind flag\n- **Safari** - Not yet supported\n\n### Testing Post-Quantum with Chrome\n\nA test server is included for Chrome interoperability testing:\n\n```bash\ncd test/chrome-interop\n./generate-localhost-cert.sh  # Generate self-signed cert (once)\nsbcl --load chrome-server.lisp\n\n# Open Chrome to https://localhost:8443/\n# The page shows whether post-quantum key exchange was negotiated\n```\n\n### FIPS 203 Compliance\n\nThe ML-KEM-768 implementation:\n- Passes all 1000 NIST FIPS 203 Known Answer Test (KAT) vectors\n- Uses constant-time modular arithmetic (Barrett reduction)\n- Implements implicit rejection for CCA security\n\nTo run the KAT tests:\n\n```bash\n# Download test vectors\ncurl -sL https://raw.githubusercontent.com/post-quantum-cryptography/KAT/main/MLKEM/kat_MLKEM_768.rsp \\\n     -o test/vectors/kat_MLKEM_768.rsp\n\n# Run tests\nsbcl --eval '(asdf:load-system :pure-tls)' \\\n     --load test/ml-kem-kat.lisp \\\n     --eval '(ml-kem-kat:run-tests)'\n```\n\n### Security Considerations\n\n- **Hybrid design** - Security relies on the stronger of X25519 or ML-KEM-768\n- **Larger key shares** - Client sends 1216 bytes, server sends 1120 bytes (vs 32 bytes for X25519 alone)\n- **Constant-time** - All secret-dependent operations use constant-time arithmetic\n- **Implicit rejection** - Invalid ciphertexts produce pseudorandom output (CCA security)\n\n## Encrypted Client Hello (ECH)\n\npure-tls supports **Encrypted Client Hello (ECH)** per RFC 9639, which encrypts the ClientHello message including the SNI (Server Name Indication) to protect user privacy from network observers.\n\n### How It Works\n\nWithout ECH, the server hostname is sent in plaintext during the TLS handshake, allowing network observers to see which websites you're connecting to. ECH encrypts this information using a public key published in the server's DNS HTTPS record.\n\n1. **Client fetches ECH config** from DNS HTTPS record (caller's responsibility)\n2. **Inner ClientHello** contains the real SNI and is encrypted\n3. **Outer ClientHello** shows only the public \"client-facing server\" name\n4. **Server decrypts** the inner ClientHello and processes the real request\n\n### Client Usage\n\n```lisp\n;; ECH configs are typically obtained from DNS HTTPS records\n;; The caller is responsible for DNS lookup (following rustls/BoringSSL pattern)\n(let ((ech-configs (fetch-ech-configs-from-dns \"example.com\")))  ; Your DNS lookup\n  (pure-tls:make-tls-client-stream socket\n    :hostname \"example.com\"\n    :ech-configs ech-configs))\n\n;; Check if ECH was accepted by the server\n(pure-tls:tls-ech-accepted-p stream)  ; =\u003e T or NIL\n```\n\n### Handling ECH Retry\n\nIf the server rejects ECH (e.g., config is outdated), it may provide new configs:\n\n```lisp\n(handler-case\n    (pure-tls:make-tls-client-stream socket\n      :hostname \"example.com\"\n      :ech-configs old-configs)\n  (pure-tls:tls-ech-retry-error (e)\n    ;; Server provided new configs - retry with them\n    (let ((new-configs (pure-tls:tls-ech-retry-error-configs e)))\n      (pure-tls:make-tls-client-stream new-socket\n        :hostname \"example.com\"\n        :ech-configs new-configs))))\n```\n\n### Disabling ECH\n\nECH is only used when configs are provided. To disable:\n\n```lisp\n;; Simply don't provide ech-configs\n(pure-tls:make-tls-client-stream socket :hostname \"example.com\")\n\n;; Or explicitly disable even if configs are available\n(pure-tls:make-tls-client-stream socket\n  :hostname \"example.com\"\n  :ech-configs configs\n  :ech-enabled nil)\n```\n\n### ECH Config Format\n\nECH configs can be provided as:\n- Raw bytes (ECHConfigList from DNS)\n- Parsed `ech-config` structures\n- List of configs (first compatible one is used)\n\n```lisp\n;; Parse raw ECHConfigList bytes\n(pure-tls:parse-ech-config-list raw-bytes)  ; =\u003e list of ech-config\n```\n\n### Security Considerations\n\n- **Privacy protection** - Network observers cannot see the target hostname\n- **HPKE encryption** - Inner ClientHello is encrypted with X25519 + AES-128-GCM\n- **Retry handling** - Servers can provide updated configs if current ones are stale\n- **Client-side only** - Server-side ECH is not yet implemented\n\n### Browser Compatibility\n\nECH is supported by major browsers:\n- **Chrome 117+** - Enabled by default\n- **Firefox 118+** - Enabled by default\n- **Safari** - Not yet supported\n\n## Debugging with Wireshark\n\npure-tls supports the NSS Key Log format via the `SSLKEYLOGFILE` environment variable. This allows you to decrypt TLS traffic in Wireshark for debugging purposes.\n\n### Setup\n\n1. Set the `SSLKEYLOGFILE` environment variable to a writable file path:\n   ```sh\n   export SSLKEYLOGFILE=/tmp/tls-keys.log\n   ```\n\n2. Start your Lisp application that uses pure-tls\n\n3. In Wireshark:\n   - Go to **Edit \u003e Preferences \u003e Protocols \u003e TLS**\n   - Set **(Pre)-Master-Secret log filename** to the same path (`/tmp/tls-keys.log`)\n   - Capture traffic and Wireshark will automatically decrypt TLS 1.3 sessions\n\n### Logged Secrets\n\nThe following secrets are logged (compatible with Wireshark TLS 1.3 dissector):\n\n- `CLIENT_HANDSHAKE_TRAFFIC_SECRET` - Client handshake traffic key\n- `SERVER_HANDSHAKE_TRAFFIC_SECRET` - Server handshake traffic key\n- `CLIENT_TRAFFIC_SECRET_0` - Client application traffic key\n- `SERVER_TRAFFIC_SECRET_0` - Server application traffic key\n- `EXPORTER_SECRET` - Exporter master secret\n\n## Dependencies\n\n- [ironclad](https://github.com/sharplispers/ironclad) - Cryptographic primitives\n- [trivial-gray-streams](https://github.com/trivial-gray-streams/trivial-gray-streams) - Gray stream support\n- [flexi-streams](https://github.com/edicl/flexi-streams) - Character encoding (optional)\n- [alexandria](https://github.com/keithj/alexandria) - Utilities\n- [trivial-features](https://github.com/trivial-features/trivial-features) - Portable platform detection\n- [cffi](https://github.com/cffi/cffi) - Windows and macOS only, for native trust store bindings\n\n## Session Resumption (PSK)\n\npure-tls supports TLS 1.3 session resumption using Pre-Shared Keys (PSK) derived from NewSessionTicket messages. This allows clients to reconnect to servers more quickly by skipping the certificate exchange.\n\n### How It Works\n\n1. After a successful handshake, the server sends a NewSessionTicket message\n2. The client caches the ticket (keyed by hostname)\n3. On subsequent connections, the client offers the cached PSK\n4. If the server accepts, the handshake completes without certificate exchange\n\n### Client Usage\n\nSession resumption is automatic. The client caches session tickets and offers them on subsequent connections:\n\n```lisp\n;; First connection - full handshake\n(let ((tls (pure-tls:make-tls-client-stream stream :hostname \"example.com\")))\n  ;; ... use connection ...\n  (close tls))\n\n;; Second connection - resumed session (faster)\n(let ((tls (pure-tls:make-tls-client-stream stream :hostname \"example.com\")))\n  ;; ... uses cached PSK if available ...\n  (close tls))\n```\n\n### Managing the Session Cache\n\n```lisp\n;; Clear all cached session tickets\n(pure-tls:session-ticket-cache-clear)\n\n;; Clear ticket for a specific hostname\n(pure-tls:session-ticket-cache-clear \"example.com\")\n```\n\n### Server Configuration\n\nFor servers, session tickets are encrypted with a server-side key. You can set a persistent key for session tickets to survive server restarts:\n\n```lisp\n;; Set a 32-byte key for ticket encryption\n;; (If not set, a random key is generated on first use)\n(setf pure-tls:*server-ticket-key* (pure-tls:random-bytes 32))\n```\n\n### Security Considerations\n\n- Session tickets are encrypted with AES-256-GCM\n- Ticket lifetime is 24 hours by default\n- Only PSK with (EC)DHE key exchange is supported (provides forward secrecy)\n- PSK-only mode (without (EC)DHE) is not supported\n\n## Testing\n\n### Running the Test Suite\n\n```bash\n# Run all tests (unit, network, BoringSSL)\nmake test\n\n# Or from Lisp:\n(asdf:load-system :pure-tls/test)\n(pure-tls/test:run-tests)          ; Offline tests\n(pure-tls/test:run-network-tests)  ; Network tests (requires internet)\n```\n\n### Test Coverage\n\nThe test suite validates:\n\n- **Cryptographic primitives**: HKDF (RFC 5869), AES-GCM, ChaCha20-Poly1305 (RFC 8439)\n- **TLS 1.3 key schedule**: RFC 8448 test vectors for all key derivation steps\n- **Record layer**: Header format, content types, AEAD nonce construction\n- **X.509 certificates**: ASN.1 parsing, hostname verification, OID handling\n- **Bundled bad certificates**: Offline tests using certificates from [badssl.com](https://github.com/chromium/badssl.com) (expired, self-signed, known malware CAs)\n- **X.509 validation**: Certificate validation tests from Google's [x509test](https://github.com/google/x509test) project (RFC 5280 compliance, X.690 DER encoding)\n- **OpenSSL test suite**: Live TLS handshake tests adapted from OpenSSL's ssl-tests (basic handshakes, ALPN, SNI, key update, curves, mTLS)\n- **BoringSSL test suite**: Protocol compliance testing via shim binary (65% pass rate; failures are TLS 1.2 tests which pure-tls does not implement)\n- **Live validation**: TLS 1.3 connections to major sites (Google, Cloudflare, GitHub, etc.)\n\n### BoringSSL Test Suite\n\nThe BoringSSL test runner provides comprehensive protocol compliance testing:\n\n```bash\n# Build the shim binary\nmake boringssl-shim\n\n# Run tests (requires BoringSSL checkout)\nexport BORINGSSL_DIR=/path/to/boringssl\nmake boringssl-tests\n```\n\nThe shim implements the BoringSSL test protocol, allowing pure-tls to be tested against 6500+ test cases covering edge cases, malformed messages, and protocol violations.\n\n### Individual Test Suites\n\n```lisp\n(pure-tls/test:run-crypto-tests)       ; Cryptographic primitives\n(pure-tls/test:run-record-tests)       ; Record layer\n(pure-tls/test:run-handshake-tests)    ; Key schedule, extensions\n(pure-tls/test:run-certificate-tests)  ; X.509 parsing\n(pure-tls/test:run-x509test-tests)     ; X.509 validation (RFC 5280)\n(pure-tls/test:run-network-tests)      ; Network tests (requires internet)\n\n;; OpenSSL-adapted tests\n(fiveam:run! 'pure-tls/test::openssl-tests)\n```\n\n## Limitations\n\n### Not Supported\n\n- **0-RTT early data** - Disabled for security (replay attack concerns)\n- **DTLS** - Datagram TLS (UDP-based) is not implemented\n- **Certificate compression** - RFC 8879 is not implemented\n- **Post-quantum signatures** - ML-DSA (FIPS 204) is not yet supported for certificates\n\n### Limited Support\n\n- **Elliptic curves** - Only X25519, secp256r1 (P-256), and secp384r1 (P-384) are supported. The following are not implemented:\n  - P-521 (secp521r1)\n  - Brainpool curves (brainpoolP256r1, brainpoolP384r1, brainpoolP512r1)\n  - Legacy curves (sect233k1, sect283k1, secp224r1, etc.)\n\n- **Signature algorithms** - RSA-PSS, ECDSA-P256, ECDSA-P384, Ed25519, and Ed448 are supported. Not implemented:\n  - DSA\n  - RSA-PKCS1 (deprecated in TLS 1.3 but still seen in some certificates)\n\n## Acknowledgments\n\nThis project includes test files derived from the [OpenSSL](https://github.com/openssl/openssl) project:\n\n- `test/ssl-tests/` - TLS test configuration files\n- `test/certs/openssl/` - Test certificates\n\nThese files are used under the Apache License 2.0. Copyright (c) OpenSSL Project Authors.\n\nThis project also includes test key material derived from the [BoringSSL](https://boringssl.googlesource.com/boringssl) project:\n\n- `test/certs/boringssl/` - BoringSSL test keys used by the shim and local tests\n\nThese files are used under the BoringSSL license. Copyright (c) BoringSSL Authors.\n\nThis project also includes test certificates from Google's [x509test](https://github.com/google/x509test) project:\n\n- `test/certs/x509test/` - X.509 certificate validation test cases\n\nThese files are used under the Apache License 2.0. Copyright (c) Google Inc.\n\n## License\n\nMIT License\n\nCopyright (c) 2026 Anthony Green \u003cgreen@moxielogic.com\u003e\n\n## See Also\n\n- [RFC 8446](https://tools.ietf.org/html/rfc8446) - TLS 1.3 specification\n- [RFC 9639](https://tools.ietf.org/html/rfc9639) - Encrypted Client Hello (ECH)\n- [RFC 9180](https://tools.ietf.org/html/rfc9180) - Hybrid Public Key Encryption (HPKE)\n- [FIPS 203](https://csrc.nist.gov/pubs/fips/203/final) - ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism)\n- [RFC 8555](https://tools.ietf.org/html/rfc8555) - ACME protocol specification\n- [RFC 8737](https://tools.ietf.org/html/rfc8737) - TLS-ALPN-01 challenge\n- [Let's Encrypt](https://letsencrypt.org/) - Free, automated certificate authority\n- [cl+ssl](https://github.com/cl-plus-ssl/cl-plus-ssl) - OpenSSL-based TLS for Common Lisp\n","funding_links":[],"categories":["Interfaces to other package managers"],"sub_categories":["Hosting platforms"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatgreen%2Fpure-tls","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fatgreen%2Fpure-tls","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatgreen%2Fpure-tls/lists"}