# ol.clave next Automatic HTTPS certificate management and renewal via ACME, implemented in pure Clojure with minimal dependencies ## ol.clave.acme.account # ol.clave.acme.account ## validate-account ```clojure (validate-account account) ``` Validate and normalize an account map, returning the normalized map or throwing ex-info. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L7-L10) --- ## get-primary-contact ```clojure (get-primary-contact account) ``` Return the primary contact email (without scheme) or nil. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L12-L15) --- ## account-from-edn ```clojure (account-from-edn registration-edn) ``` Parse an EDN string representing account registration metadata. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L17-L20) --- ## serialize ```clojure (serialize account keypair) ``` Serialize an account map and keypair into a pretty-printed EDN artifact. `keypair` is a `java.security.KeyPair`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L22-L27) --- ## deserialize ```clojure (deserialize account-edn) ``` Deserialize an EDN artifact into [account keypair]. Returns a vector of [account keypair] where keypair is a `java.security.KeyPair`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L29-L34) --- ## generate-keypair ```clojure (generate-keypair) (generate-keypair opts) ``` Generate a new ACME account keypair. Options map: | key | description | default | |--------|-----------------------------------------|---------| | :algo | key algorithm (:p256, :p384, :ed25519) | :p256 | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L36-L45) --- ## create ```clojure (create contact tos-agreed) (create contact tos-agreed opts) ``` Construct an ACME account map suitable for directory interactions. `contact` may be a single string or any sequential collection of strings; all values must be `mailto:` URLs per RFC 8555 Section 7.3. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L47-L55) ## ol.clave.acme.challenge # ol.clave.acme.challenge Helpers for working with ACME challenges and authorizations. ## key-authorization ```clojure (key-authorization challenge account-key) ``` Return the key authorization for `challenge` and `account-key`. `challenge` may be a map with `::ol.clave.specs/token` or a raw token string. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L11-L19) --- ## dns01-key-authorization ```clojure (dns01-key-authorization key-authorization) (dns01-key-authorization challenge account-key) ``` Return the DNS-01 key authorization digest. When called with a `challenge` map and `account-key`, computes the key authorization first. | arity | description | |---------------------------|----------------------------------------------| | `[key-authorization]` | digest the provided key authorization string | | `[challenge account-key]` | compute key authorization then digest | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L21-L35) --- ## http01-resource-path ```clojure (http01-resource-path challenge) ``` Return the HTTP-01 resource path for `challenge` or `token`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L37-L43) --- ## dns01-txt-name ```clojure (dns01-txt-name domain-or-authorization) ``` Return the DNS-01 TXT record name for `domain` or `authorization`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L45-L51) --- ## wildcard? ```clojure (wildcard? authorization) ``` Return true when the authorization declares a wildcard identifier. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L53-L56) --- ## identifier ```clojure (identifier authorization) ``` Return the identifier value from an authorization map. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L58-L61) --- ## identifier-domain ```clojure (identifier-domain authorization) ``` Return the identifier domain with any wildcard prefix removed. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L63-L69) --- ## token ```clojure (token challenge) ``` Return the challenge token string. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L71-L74) --- ## find-by-type ```clojure (find-by-type authorization type) ``` Return the first challenge in `authorization` matching `type`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L76-L79) --- ## acme-tls-1-protocol ALPN protocol identifier for TLS-ALPN-01 challenges. Use this value to detect ACME challenge handshakes in your TLS server’s ALPN negotiation callback. See RFC 8737 Section 6.2. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L81-L87) --- ## tlsalpn01-challenge-cert ```clojure (tlsalpn01-challenge-cert identifier key-authorization) (tlsalpn01-challenge-cert authorization challenge account-key) ``` Build a TLS-ALPN-01 challenge certificate. This function has two arities: Low-level arity `[identifier key-authorization]`: - `identifier` - Map with `:type` ("dns" or "ip") and `:value` - `key-authorization` - The computed key authorization string Convenience arity `[authorization challenge account-key]`: - `authorization` - Authorization map with `::acme/identifier` - `challenge` - Challenge map with `::acme/token` - `account-key` - Account keypair for computing key authorization Returns a map with: | key | description | |--------------------|---------------------------------------------| | `:certificate-der` | DER-encoded certificate bytes | | `:certificate-pem` | PEM-encoded certificate string | | `:private-key-der` | DER-encoded private key bytes (PKCS#8) | | `:private-key-pem` | PEM-encoded private key string (PKCS#8) | | `:x509` | Parsed `java.security.cert.X509Certificate` | | `:keypair` | The generated `java.security.KeyPair` | | `:identifier-type` | The identifier type from input | | `:identifier-value`| The identifier value from input | The certificate contains: - Subject and Issuer: CN=ACME challenge - SubjectAltName with the identifier (DNS name or IP address) - Critical acmeValidationV1 extension (OID 1.3.6.1.5.5.7.1.31) containing the SHA-256 digest of the key authorization ```clojure ;; Low-level usage with pre-computed key authorization (tlsalpn01-challenge-cert {:type "dns" :value "example.com"} "token.thumbprint") ;; Convenience usage with authorization and challenge maps (tlsalpn01-challenge-cert authorization challenge account-key) ``` See RFC 8737 for TLS-ALPN-01 challenge specification. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L89-L138) ## ol.clave.acme.commands # ol.clave.acme.commands Plumbing layer ACME command API for interacting with an ACME server. Every command takes an immutable ACME session map as its first argument and returns a tuple where the first element is the updated session (with refreshed nonces, account metadata, etc.). This keeps side effects explicit for callers. Use this namespace when you need precise control over ACME interactions: Session management: - [`new-session`](#new-session), [`create-session`](#create-session), [`load-directory`](#load-directory), [`set-polling`](#set-polling) Account operations: - [`new-account`](#new-account), [`get-account`](#get-account), [`update-account-contact`](#update-account-contact) - [`deactivate-account`](#deactivate-account), [`rollover-account-key`](#rollover-account-key) - [`compute-eab-binding`](#compute-eab-binding) Order lifecycle: - [`new-order`](#new-order), [`get-order`](#get-order), [`poll-order`](#poll-order), [`finalize-order`](#finalize-order) Authorization and challenges: - [`get-authorization`](#get-authorization), [`poll-authorization`](#poll-authorization), [`deactivate-authorization`](#deactivate-authorization) - [`respond-challenge`](#respond-challenge) Certificate operations: - [`get-certificate`](#get-certificate), [`revoke-certificate`](#revoke-certificate) Renewal information (ARI per RFC 9773): - [`get-renewal-info`](#get-renewal-info) Terms of Service: - [`check-terms-of-service`](#check-terms-of-service) Pair these commands with `ol.clave.scope` to enforce timeouts and cancellation across long-running workflows such as account setup or certificate issuance. ## new-session ```clojure (new-session directory-url) (new-session directory-url opts) ``` Create an ACME session value without issuing network requests. Parameters: - `directory-url` — ACME directory URL as a string. - `opts` — optional map configuring the session. Recognised keys are summarised below. | Key | Type | Description | |----------------|-------------------------|--------------------------------------------------------------------------| | `:http-client` | map | Passed to `ol.clave.impl.http/http-client` to build the transport layer. | | `:account-key` | `java.security.KeyPair` | Injects an existing key pair into the session. | | `:account-kid` | string | Stores a known account URL for authenticated calls. | | `:scope` | scope token | Overrides the default cancellation/timeout scope. | Returns `[session nil]`, where `session` is a qualified map of `::ol.clave.specs/*` keys suitable for subsequent commands. Example: ```clojure (require '[ol.clave.commands :as commands]) (let [[session _] (commands/new-session "https://acme.example/dir" {:http-client {}})] (::ol.clave.specs/directory-url session)) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L39-L67) --- ## load-directory ```clojure (load-directory lease session) (load-directory lease session opts) ``` Fetch the ACME directory document and attach it to `session`. Uses a global cache with 12-hour TTL to avoid repeated fetches for long-running servers managing multiple domains. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — session created by [`new-session`](#new-session) or [`create-session`](#create-session). - `opts` — optional map with overrides. Options: | key | description | |-----------|------------------------------------------- | | `:force` | Bypass cache, fetch fresh from CA. | | `:ttl-ms` | Custom cache TTL in milliseconds. | Returns `[updated-session directory]`, where `directory` is the qualified map described by `::ol.clave.specs/directory` and `updated-session` has the directory attached. When loaded from cache, the session will not have a nonce from this call; the first JWS operation will fetch one via HEAD to newNonce. Example: ```clojure (require '[ol.clave.commands :as commands] '[ol.clave.lease :as lease]) (let [bg (lease/background) [session _] (commands/new-session "https://acme.example/dir" {:http-client {}})] (commands/load-directory bg session)) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L69-L106) --- ## set-polling ```clojure (set-polling session opts) ``` Update default polling parameters in the session. Parameters: - `session` — ACME session map. - `opts` — map with optional polling configuration keys. Options: | key | description | |----------------|----------------------------------------------| | `:interval-ms` | Default poll interval fallback (ms). | | `:timeout-ms` | Default overall poll timeout (ms). | Returns the updated session with new polling defaults. Example: ```clojure (-> session (commands/set-polling {:interval-ms 2000 :timeout-ms 120000})) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L108-L130) --- ## create-session ```clojure (create-session lease directory-url) (create-session lease directory-url opts) ``` Build a session and eagerly download the ACME directory. Parameters: - `lease` — lease for cancellation/timeout control. - `directory-url` — ACME directory URL. - `opts` — same options map accepted by [`new-session`](#new-session), optionally extended with `:force` to bypass the directory cache, and `:ttl-ms` for custom cache TTL. Returns `[session directory]` with the directory hydrated and incorporated into `session`. Example: ```clojure (require '[ol.clave.commands :as commands] '[ol.clave.lease :as lease]) (commands/create-session (lease/background) "https://acme.example/dir" {:http-client {}}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L132-L154) --- ## compute-eab-binding ```clojure (compute-eab-binding eab-opts account-key endpoint) ``` Produce an External Account Binding structure for account creation. Parameters: - `eab-opts` — map with `:kid` and `:mac-key`, or `nil` to skip the binding. - `account-key` — existing account key pair (`java.security.KeyPair`). - `endpoint` — directory key identifying the `newAccount` URL. Returns the binding map described by RFC 8555 §7.3.4 or `nil` when `eab-opts` is missing. Invalid base64 encoding raises `::ol.clave.errors/invalid-eab`. Example: ```clojure (require '[ol.clave.commands :as commands]) (commands/compute-eab-binding {:kid "kid-123" :mac-key "q83l..."} account-key "https://acme.example/new-account") ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L156-L177) --- ## new-account ```clojure (new-account lease session account) (new-account lease session account opts) ``` Register a new ACME account at the directory’s `newAccount` endpoint. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated or unauthenticated ACME session map. - `account` — account data using `::ol.clave.specs/*` keys. - `opts` — optional map supporting the keys listed below. | Key | Description | | --- | --- | | `:external-account` | `{:kid string :mac-key bytes-or-base64}` enabling External Account Binding. | Returns `[updated-session normalized-account]`. The session gains the account KID and the response account is merged onto the supplied `account` map. Example: ```clojure (require '[ol.clave.commands :as commands] '[ol.clave.account :as account] '[ol.clave.lease :as lease]) (let [[acct key] (account/deserialize (slurp "test/fixtures/test-account.edn"))] (commands/new-account (lease/background) session acct)) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L179-L207) --- ## find-account-by-key ```clojure (find-account-by-key lease session) (find-account-by-key lease session opts) ``` Look up an existing ACME account by its public key. Uses the newAccount endpoint with `onlyReturnExisting: true` to find an account without creating one. This is useful for key recovery scenarios where you have the account key but lost the account URL. Parameters: - `lease` - A lease for cooperative cancellation. - `session` - Session with account key set (via `:account-key` option). Returns `[updated-session account-kid]` where `account-kid` is the account URL string. The session is updated with the account KID. Throws `::ol.clave.errors/account-not-found` if no account exists for the key. Throws `::ol.clave.errors/invalid-account-key` if session has no account key. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L209-L228) --- ## get-account ```clojure (get-account lease session account) (get-account lease session account opts) ``` Retrieve the current state of an ACME account via POST-as-GET. Parameters: - `lease` - A lease for cooperative cancellation. - `session` — session containing `::ol.clave.specs/account-kid` and `::ol.clave.specs/account-key`. - `account` — baseline account map that will be merged with the server response. Returns `[updated-session account-map]` where `account-map` is the merged account including the authoritative contact info and account status. Example: ```clojure (require '[ol.clave.commands :as commands]) (commands/get-account lease session account) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L230-L252) --- ## update-account-contact ```clojure (update-account-contact lease session account contacts) (update-account-contact lease session account contacts opts) ``` Replace the contact URIs registered for an ACME account. Parameters: - `lease` - A lease for cooperative cancellation. - `session` — authenticated session. - `account` — current account map. - `contacts` — vector of `mailto:` URIs to set on the account. Returns `[updated-session updated-account]` with contacts normalised to a vector of strings sourced from the server response. Example: ```clojure (require '[ol.clave.commands :as commands]) (commands/update-account-contact lease session account ["mailto:admin@example.com"]) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L254-L275) --- ## deactivate-account ```clojure (deactivate-account lease session account) (deactivate-account lease session account opts) ``` Deactivate an ACME account by issuing a status change request. Parameters: - `lease` - A lease for cooperative cancellation. - `session` — authenticated session. - `account` — account map with identifying information. Returns `[updated-session deactivated-account]`. Subsequent account operations will fail with `::ol.clave.errors/unauthorized-account`. Example: ```clojure (require '[ol.clave.commands :as commands]) (commands/deactivate-account lease session account) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L277-L297) --- ## rollover-account-key ```clojure (rollover-account-key lease session account new-account-key) (rollover-account-key lease session account new-account-key opts) ``` Replace the account key pair using the directory `keyChange` endpoint. Parameters: - `lease` - A lease for cooperative cancellation. - `session` — authenticated session containing the current account key and KID. - `account` — account data used to verify the new key. - `new-account-key` — `java.security.KeyPair` to install. Returns `[updated-session verified-account]` with the session updated to store `new-account-key`. Verification failures raise `::ol.clave.errors/account-key-rollover-verification-failed`. Example: ```clojure (require '[ol.clave.commands :as commands] '[ol.clave.account :as account]) (let [new-key (account/generate-keypair)] (commands/rollover-account-key lease session account new-key)) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L299-L323) --- ## new-order ```clojure (new-order lease session order) (new-order lease session order opts) ``` Create a new ACME order for the supplied identifiers. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session with account key and KID. - `order` — map containing `::ol.clave.specs/identifiers` and optional `::ol.clave.specs/notBefore` / `::ol.clave.specs/notAfter`. - `opts` — optional map with overrides. Options: | key | description | |------------|-------------| | `:profile` | Optional profile name as a string when the directory advertises `:profiles`. | Returns `[updated-session order]` where `order` is the normalized order map including `::ol.clave.specs/order-location`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L325-L346) --- ## get-order ```clojure (get-order lease session order-url) (get-order lease session order-url opts) ``` Retrieve the current state of an order via POST-as-GET. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `order-url` — order URL string, or an order map that includes `::ol.clave.specs/order-location`. - `opts` — optional map with overrides. Returns `[updated-session order]` with the latest order data. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L348-L362) --- ## poll-order ```clojure (poll-order lease session order-url) ``` Poll an order URL until it reaches a terminal status. Timeout is the lesser of session’s poll-timeout and the lease’s deadline. Configure polling defaults via [`set-polling`](#set-polling). Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `order-url` — order URL string. Returns `[updated-session order]` on success or throws on invalid/timeout. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L364-L377) --- ## finalize-order ```clojure (finalize-order lease session order csr) (finalize-order lease session order csr opts) ``` Finalize an order by submitting a CSR. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `order` — normalized order map with `::ol.clave.specs/status` and `::ol.clave.specs/finalize`. - `csr` — map containing `:csr-b64url` from [`ol.clave.certificate.impl.csr/create-csr`](api/ol-clave-certificate-impl-csr.adoc#create-csr). - `opts` — optional map with overrides. Returns `[updated-session order]` with the updated order state. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L379-L394) --- ## get-certificate ```clojure (get-certificate lease session certificate-url) (get-certificate lease session certificate-url opts) ``` Download a PEM certificate chain from the certificate URL via POST-as-GET. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session carrying HTTP configuration. - `certificate-url` — certificate URL from an order. - `opts` — optional map with overrides. Returns `[updated-session result]` where `result` includes `:chains` and `:preferred` entries with parsed PEM data. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L396-L410) --- ## get-authorization ```clojure (get-authorization lease session authorization-url) (get-authorization lease session authorization-url opts) ``` Fetch an authorization resource via POST-as-GET. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `authorization-url` — authorization URL string, or an authorization map containing `::ol.clave.specs/authorization-location`. - `opts` — optional map with overrides. Returns `[updated-session authorization]`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L412-L426) --- ## poll-authorization ```clojure (poll-authorization lease session authorization-url) ``` Poll an authorization URL until it reaches a terminal status. Timeout is the lesser of session' poll-timeout and lease’s deadline. Configure polling defaults via [`set-polling`](#set-polling). Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `authorization-url` — authorization URL string. Returns `[updated-session authorization]` on success or throws when invalid, unusable, or timed out. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L428-L442) --- ## deactivate-authorization ```clojure (deactivate-authorization lease session authorization-url) (deactivate-authorization lease session authorization-url opts) ``` Deactivate an authorization by sending a status update. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `authorization-url` — authorization URL string, or authorization map. - `opts` — optional map with overrides. Returns `[updated-session authorization]`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L444-L457) --- ## new-authorization ```clojure (new-authorization lease session identifier) (new-authorization lease session identifier opts) ``` Create a pre-authorization for an identifier via the newAuthz endpoint. Pre-authorization (RFC 8555 Section 7.4.1) allows clients to obtain authorization proactively, outside the context of a specific order. This is useful for hosting providers who want to authorize domains before virtual servers are created. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `identifier` — map with `:type` and `:value` keys. - `opts` — optional map with overrides. Pre-authorization cannot be used with wildcard identifiers. Not all ACME servers support this endpoint. Returns `[updated-session authorization]`. Throws: - `::ol.clave.errors/pre-authorization-unsupported` if server does not advertise newAuthz endpoint. - `::ol.clave.errors/wildcard-identifier-not-allowed` if identifier is a wildcard. - `::ol.clave.errors/pre-authorization-failed` if server rejects the request. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L459-L487) --- ## respond-challenge ```clojure (respond-challenge lease session challenge) (respond-challenge lease session challenge opts) ``` Notify the ACME server that a challenge response is ready. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session. - `challenge` — challenge map containing `::ol.clave.specs/url`. - `opts` — optional map with overrides. Options: | key | description | |------------|--------------------------------------| | `:payload` | Override the default `{}` payload. | Returns `[updated-session challenge]`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L489-L508) --- ## revoke-certificate ```clojure (revoke-certificate lease session certificate) (revoke-certificate lease session certificate opts) ``` Revoke a certificate via the directory’s revokeCert endpoint. Parameters: - `lease` — lease for cancellation/timeout control. - `session` — authenticated session or session with directory loaded. - `certificate` — `java.security.cert.X509Certificate` or DER bytes. - `opts` — optional map with overrides. Options: | key | description | |----------------|----------------------------------------------------------| | `:reason` | RFC 5280 reason code integer (0-6, 8-10). | | `:signing-key` | `java.security.KeyPair` for certificate-key authorization. | When `:signing-key` is provided, uses certificate-key authorization with JWK-embedded JWS. Otherwise uses account-key authorization requiring an authenticated session. Returns `[updated-session nil]` on success. Example: ```clojure (commands/revoke-certificate lease session cert {:reason 1}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L510-L539) --- ## get-renewal-info ```clojure (get-renewal-info lease session cert-or-id) (get-renewal-info lease session cert-or-id opts) ``` Fetch ACME Renewal Information (ARI) for a certificate per RFC 9773. Parameters: - `lease` — lease for cancellation/timeout control. - `session` - ACME session with directory loaded. - `cert-or-id` - X509Certificate or precomputed renewal identifier string. - `opts` - optional map with overrides. When `cert-or-id` is a certificate, the renewal identifier is derived from the Authority Key Identifier extension and serial number. Returns `[updated-session renewal-info]` where `renewal-info` contains `:suggested-window` (map with `:start` and `:end` instants), `:retry-after-ms`, and optional `:explanation-url`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L541-L559) --- ## check-terms-of-service ```clojure (check-terms-of-service lease session) (check-terms-of-service lease session opts) ``` Check for Terms of Service changes by comparing directory meta values. Parameters: - `lease` — lease for cancellation/timeout control. - `session` - ACME session with directory already loaded. - `opts` - optional map with overrides. Refreshes the directory from the server and compares the `termsOfService` field in the meta section with the previously loaded value. Returns `[updated-session tos-change]` where `tos-change` contains: - `:changed?` - true if termsOfService URL changed - `:previous` - previous termsOfService URL or nil - `:current` - current termsOfService URL or nil [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L561-L579) ## ol.clave.acme.impl.account # ol.clave.acme.impl.account ## validate-account ```clojure (validate-account account) ``` See [`ol.clave.acme.account/validate-account`](api/ol-clave-acme-account.adoc#validate-account) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L43-L64) --- ## get-primary-contact ```clojure (get-primary-contact account) ``` See [`ol.clave.acme.account/get-primary-contact`](api/ol-clave-acme-account.adoc#get-primary-contact) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L66-L71) --- ## account-from-edn ```clojure (account-from-edn registration-edn) ``` See [`ol.clave.acme.account/account-from-edn`](api/ol-clave-acme-account.adoc#account-from-edn) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L73-L84) --- ## serialize ```clojure (serialize account keypair) ``` See [`ol.clave.acme.account/serialize`](api/ol-clave-acme-account.adoc#serialize) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L93-L105) --- ## deserialize ```clojure (deserialize account-edn) ``` See [`ol.clave.acme.account/deserialize`](api/ol-clave-acme-account.adoc#deserialize) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L107-L124) --- ## generate-keypair ```clojure (generate-keypair) (generate-keypair {:keys [algo] :or {algo :p256}}) ``` See [`ol.clave.acme.account/generate-keypair`](api/ol-clave-acme-account.adoc#generate-keypair) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L126-L131) --- ## create ```clojure (create contact tos-agreed) (create contact tos-agreed _) ``` See [`ol.clave.acme.account/create`](api/ol-clave-acme-account.adoc#create) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L133-L157) ## ol.clave.acme.impl.ari # ol.clave.acme.impl.ari ARI identifier derivation helpers per RFC 9773. Extracts the Authority Key Identifier keyIdentifier and serial number from an X509Certificate and builds the unique renewal identifier string. ## authority-key-identifier ```clojure (authority-key-identifier cert) ``` Extract the keyIdentifier bytes from the AKI extension of a certificate. Parameters: - `cert` - X509Certificate to extract AKI from. Returns the keyIdentifier bytes or throws `::errors/renewal-info-invalid` if the AKI extension is missing or does not contain a keyIdentifier. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L23-L57) --- ## serial-der-bytes ```clojure (serial-der-bytes cert) ``` Return the DER-encoded serial number bytes of a certificate. Per RFC 9773, this is the two’s complement encoding of the serial number with a leading zero byte if the high bit is set (to preserve positive sign). Parameters: - `cert` - X509Certificate to extract serial from. Returns the DER-encoded serial number bytes (without tag and length). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L59-L73) --- ## renewal-id ```clojure (renewal-id cert) ``` Derive the ARI renewal identifier from a certificate. The identifier is: base64url(AKI keyIdentifier) || '.' || base64url(serial DER) with all trailing padding ('=') stripped per RFC 9773. Parameters: - `cert` - X509Certificate to derive identifier from. Returns the renewal identifier string. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L75-L90) --- ## normalize-renewal-info ```clojure (normalize-renewal-info body retry-after-ms) ``` Normalize a RenewalInfo response from the server. Parameters: - `body` - parsed JSON response body as a map (keyword or string keys). - `retry-after-ms` - Retry-After value in milliseconds. Returns a normalized map with `:suggested-window`, optional `:explanation-url`, and `:retry-after-ms`. Throws `::errors/renewal-info-invalid` if the response is malformed or the window is invalid. The suggested window must have end strictly after start per RFC 9773. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L107-L144) ## ol.clave.acme.impl.authorization # ol.clave.acme.impl.authorization ## normalize-authorization ```clojure (normalize-authorization authorization account-key location) ``` Normalize an authorization response into qualified keys and computed data. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L12-L27) --- ## authorization-valid? ```clojure (authorization-valid? authorization) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L29-L31) --- ## authorization-invalid? ```clojure (authorization-invalid? authorization) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L33-L35) --- ## authorization-unusable? ```clojure (authorization-unusable? authorization) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L37-L39) --- ## authorization-problem ```clojure (authorization-problem authorization) ``` Return the most relevant problem map from an authorization. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L41-L45) --- ## wildcard-identifier? ```clojure (wildcard-identifier? identifier) ``` Return true when `identifier` is a wildcard (value starts with *.). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L47-L52) ## ol.clave.acme.impl.challenge # ol.clave.acme.impl.challenge ## key-authorization ```clojure (key-authorization token account-keypair) ``` See [`ol.clave.acme.challenge/key-authorization`](api/ol-clave-acme-challenge.adoc#key-authorization) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L14-L19) --- ## dns01-key-authorization ```clojure (dns01-key-authorization key-authorization) ``` See [`ol.clave.acme.challenge/dns01-key-authorization`](api/ol-clave-acme-challenge.adoc#dns01-key-authorization) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L21-L27) --- ## http01-resource-path ```clojure (http01-resource-path token) ``` See [`ol.clave.acme.challenge/http01-resource-path`](api/ol-clave-acme-challenge.adoc#http01-resource-path) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L29-L32) --- ## dns01-txt-name ```clojure (dns01-txt-name domain) ``` See [`ol.clave.acme.challenge/dns01-txt-name`](api/ol-clave-acme-challenge.adoc#dns01-txt-name) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L34-L38) --- ## normalize-challenge ```clojure (normalize-challenge challenge) (normalize-challenge challenge account-key) ``` Qualify a challenge map and attach computed key authorization when possible. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L40-L50) ## ol.clave.acme.impl.commands # ol.clave.acme.impl.commands ## default-poll-interval-ms [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L28-L28) --- ## default-poll-timeout-ms [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L29-L29) --- ## new-session ```clojure (new-session directory-url {:keys [http-client account-key account-kid]}) ``` See [`ol.clave.acme.commands/new-session`](api/ol-clave-acme-commands.adoc#new-session) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L31-L43) --- ## set-polling ```clojure (set-polling session {:keys [interval-ms timeout-ms]}) ``` See [`ol.clave.acme.commands/set-polling`](api/ol-clave-acme-commands.adoc#set-polling) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L45-L50) --- ## load-directory ```clojure (load-directory lease session) (load-directory lease {::acme/keys [directory-url] :as session} opts) ``` See [`ol.clave.acme.commands/load-directory`](api/ol-clave-acme-commands.adoc#load-directory) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L52-L80) --- ## create-session ```clojure (create-session lease directory-url opts) ``` See [`ol.clave.acme.commands/create-session`](api/ol-clave-acme-commands.adoc#create-session) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L82-L87) --- ## compute-eab-binding ```clojure (compute-eab-binding eab-opts account-key endpoint) ``` See [`ol.clave.acme.commands/compute-eab-binding`](api/ol-clave-acme-commands.adoc#compute-eab-binding) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L89-L104) --- ## new-account ```clojure (new-account lease session account) (new-account lease session account opts) ``` See [`ol.clave.acme.commands/new-account`](api/ol-clave-acme-commands.adoc#new-account) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L106-L145) --- ## find-account-by-key ```clojure (find-account-by-key lease session) (find-account-by-key lease session _opts) ``` See [`ol.clave.acme.commands/find-account-by-key`](api/ol-clave-acme-commands.adoc#find-account-by-key) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L147-L181) --- ## get-account ```clojure (get-account lease session account) (get-account lease session account _opts) ``` See [`ol.clave.acme.commands/get-account`](api/ol-clave-acme-commands.adoc#get-account) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L242-L252) --- ## update-account-contact ```clojure (update-account-contact lease session account contacts) (update-account-contact lease session account contacts _opts) ``` See [`ol.clave.acme.commands/update-account-contact`](api/ol-clave-acme-commands.adoc#update-account-contact) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L254-L270) --- ## deactivate-account ```clojure (deactivate-account lease session account) (deactivate-account lease session account _opts) ``` See [`ol.clave.acme.commands/deactivate-account`](api/ol-clave-acme-commands.adoc#deactivate-account) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L272-L281) --- ## rollover-account-key ```clojure (rollover-account-key lease session account new-account-key) (rollover-account-key lease session account new-account-key _opts) ``` See [`ol.clave.acme.commands/rollover-account-key`](api/ol-clave-acme-commands.adoc#rollover-account-key) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L283-L316) --- ## new-order ```clojure (new-order lease session order) (new-order lease session order opts) ``` See [`ol.clave.acme.commands/new-order`](api/ol-clave-acme-commands.adoc#new-order) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L318-L351) --- ## get-order ```clojure (get-order lease session order-or-url) (get-order lease session order-or-url _opts) ``` See [`ol.clave.acme.commands/get-order`](api/ol-clave-acme-commands.adoc#get-order) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L371-L383) --- ## poll-order ```clojure (poll-order lease session order-url) ``` See [`ol.clave.acme.commands/poll-order`](api/ol-clave-acme-commands.adoc#poll-order) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L385-L432) --- ## finalize-order ```clojure (finalize-order lease session order csr) (finalize-order lease session order csr _opts) ``` See [`ol.clave.acme.commands/finalize-order`](api/ol-clave-acme-commands.adoc#finalize-order) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L434-L479) --- ## get-authorization ```clojure (get-authorization lease session authorization-or-url) (get-authorization lease session authorization-or-url _opts) ``` See [`ol.clave.acme.commands/get-authorization`](api/ol-clave-acme-commands.adoc#get-authorization) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L499-L509) --- ## poll-authorization ```clojure (poll-authorization lease session authorization-url) ``` See [`ol.clave.acme.commands/poll-authorization`](api/ol-clave-acme-commands.adoc#poll-authorization) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L511-L566) --- ## deactivate-authorization ```clojure (deactivate-authorization lease session authorization-or-url) (deactivate-authorization lease session authorization-or-url _opts) ``` See [`ol.clave.acme.commands/deactivate-authorization`](api/ol-clave-acme-commands.adoc#deactivate-authorization) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L568-L591) --- ## new-authorization ```clojure (new-authorization lease session identifier) (new-authorization lease session identifier _opts) ``` See [`ol.clave.acme.commands/new-authorization`](api/ol-clave-acme-commands.adoc#new-authorization) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L593-L636) --- ## respond-challenge ```clojure (respond-challenge lease session challenge) (respond-challenge lease session challenge opts) ``` See [`ol.clave.acme.commands/respond-challenge`](api/ol-clave-acme-commands.adoc#respond-challenge) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L638-L659) --- ## get-certificate ```clojure (get-certificate lease session certificate-url) (get-certificate lease session certificate-url _opts) ``` See [`ol.clave.acme.commands/get-certificate`](api/ol-clave-acme-commands.adoc#get-certificate) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L661-L689) --- ## revoke-certificate ```clojure (revoke-certificate lease session certificate) (revoke-certificate lease session certificate opts) ``` See [`ol.clave.acme.commands/revoke-certificate`](api/ol-clave-acme-commands.adoc#revoke-certificate) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L691-L738) --- ## get-renewal-info ```clojure (get-renewal-info lease session cert-or-id) (get-renewal-info lease session cert-or-id _opts) ``` See [`ol.clave.acme.commands/get-renewal-info`](api/ol-clave-acme-commands.adoc#get-renewal-info) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L755-L792) --- ## check-terms-of-service ```clojure (check-terms-of-service lease session) (check-terms-of-service lease session _opts) ``` See [`ol.clave.acme.commands/check-terms-of-service`](api/ol-clave-acme-commands.adoc#check-terms-of-service) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L794-L803) ## ol.clave.acme.impl.directory-cache # ol.clave.acme.impl.directory-cache Global directory cache with TTL for ACME directory responses. ACME directories rarely change, so caching them avoids unnecessary network requests for long-running servers managing many domains. The cache is keyed by directory URL with a 12-hour default TTL. Stale entries remain until replaced (no background cleanup). ## default-ttl-ms Default TTL of 12 hours in milliseconds. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L10-L12) --- ## cache-get ```clojure (cache-get url) (cache-get url ttl-ms) ``` Returns cached directory for url if present and fresh, else nil. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L25-L31) --- ## cache-put! ```clojure (cache-put! url directory) ``` Stores directory in cache with current timestamp. Returns directory. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L33-L38) --- ## cache-clear! ```clojure (cache-clear!) ``` Clears entire directory cache. Useful for testing. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L40-L43) --- ## cache-evict! ```clojure (cache-evict! url) ``` Removes single entry from cache. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L45-L48) ## ol.clave.acme.impl.http.interceptors # ol.clave.acme.impl.http.interceptors ## ->uri ```clojure (->uri uri) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L21-L31) --- ## accept-header Request: adds `:accept` header. Only supported value is `:json`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L55-L67) --- ## uri-with-query ```clojure (uri-with-query uri new-query) ``` We can’t use the URI constructor because it encodes all arguments for us. See https://stackoverflow.com/a/77971448/6264 [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L85-L99) --- ## query-params Request: encodes `:query-params` map and appends to `:uri`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L101-L110) --- ## form-params Request: encodes `:form-params` map and adds `:body`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L123-L133) --- ## decompress-body Response: decompresses body based on "content-encoding" header. Valid values: `gzip` and `deflate`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L177-L184) --- ## decode-body Response: based on the value of `:as` in request, decodes as `:string`, `:stream` or `:bytes`. Defaults to `:string`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L191-L203) --- ## construct-uri Request: construct uri from map [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L205-L212) --- ## request-method Request: normalize :method option [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L214-L221) --- ## unexceptional-statuses [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L223-L224) --- ## throw-on-exceptional-status-code Response: throw on exceptional status codes [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L226-L235) --- ## parse-json-body [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L237-L251) --- ## default-interceptors Default interceptor chain. Interceptors are called in order for request and in reverse order for response. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L253-L263) ## ol.clave.acme.impl.order # ol.clave.acme.impl.order ## create-identifier ```clojure (create-identifier identifier) (create-identifier identifier-type identifier-value) ``` See [`ol.clave.acme.order/create-identifier`](api/ol-clave-acme-order.adoc#create-identifier) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L21-L37) --- ## create ```clojure (create identifiers) (create identifiers opts) ``` See [`ol.clave.acme.order/create`](api/ol-clave-acme-order.adoc#create) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L39-L62) --- ## build-order-payload ```clojure (build-order-payload order) ``` Build an ACME newOrder payload from a qualified order map. Parameters: - `order` - Order map with `::acme/identifiers` and optional `::acme/notBefore`, `::acme/notAfter`, `::acme/replaces`. The `replaces` field (RFC 9773) links a renewal order to its predecessor certificate using the ARI unique identifier format. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L74-L101) --- ## normalize-order ```clojure (normalize-order order location) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L103-L113) --- ## ensure-identifiers-consistent ```clojure (ensure-identifiers-consistent expected order) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L115-L122) --- ## order-ready? ```clojure (order-ready? order) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L124-L126) --- ## order-terminal? ```clojure (order-terminal? order) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L128-L130) ## ol.clave.acme.impl.revocation # ol.clave.acme.impl.revocation Pure helpers for certificate revocation payload construction and validation. This namespace handles: - Extracting DER bytes from X509Certificate or raw bytes - Constructing revocation payloads with base64url-encoded certificates - Validating RFC 5280 reason codes for ACME revocation ## valid-reason? ```clojure (valid-reason? reason) ``` Return true if `reason` is a valid RFC 5280 revocation reason code for ACME. Valid codes are 0-6 and 8-10. Code 7 is unused in RFC 5280. Returns false for non-integer values. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/revocation.clj#L21-L28) --- ## certificate->der ```clojure (certificate->der certificate) ``` Extract DER-encoded bytes from a certificate. Accepts either: - `java.security.cert.X509Certificate` - extracts via `.getEncoded()` - `byte[]` - returns as-is Returns the DER-encoded certificate bytes. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/revocation.clj#L30-L49) --- ## payload ```clojure (payload certificate) (payload certificate opts) ``` Construct a revocation request payload. Parameters: - `certificate` - `X509Certificate` or DER bytes - `opts` - optional map with `:reason` (RFC 5280 reason code) Returns a map with: - `:certificate` - base64url-encoded DER - `:reason` - reason code (when provided in opts) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/revocation.clj#L51-L67) ## ol.clave.acme.impl.tos # ol.clave.acme.impl.tos Terms of Service change detection helpers. Compares prior and current directory meta values to detect ToS changes. ## compare-terms ```clojure (compare-terms previous current) ``` Compare previous and current directory meta maps for ToS changes. Parameters: - `previous` - previous directory meta map with `::specs/termsOfService`. - `current` - current directory meta map with `::specs/termsOfService`. Returns a map with: - `:changed?` - true if termsOfService values differ - `:previous` - previous termsOfService URL or nil - `:current` - current termsOfService URL or nil [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/tos.clj#L10-L26) ## ol.clave.acme.order # ol.clave.acme.order Helpers for building and inspecting ACME orders. ## create-identifier ```clojure (create-identifier identifier) (create-identifier identifier-type identifier-value) ``` Construct an identifier map from `identifier` or `type` and `value`. `type` may be a string or keyword such as `dns` or `:dns`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L9-L16) --- ## create ```clojure (create identifiers) (create identifiers opts) ``` Construct an order map with the supplied identifiers and options. Options: | key | description | |---------------|-----------------------------------------------| | `:not-before` | Optional notBefore instant or RFC3339 string. | | `:not-after` | Optional notAfter instant or RFC3339 string. | | `:profile` | Optional profile name as a string. | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L18-L32) --- ## identifiers ```clojure (identifiers order) ``` Return the identifiers on an order map. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L34-L37) --- ## authorizations ```clojure (authorizations order) ``` Return the authorization URLs from an order map. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L39-L42) --- ## url ```clojure (url order) ``` Return the order URL from an order map. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L44-L47) --- ## certificate-url ```clojure (certificate-url order) ``` Return the certificate URL from an order map. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L49-L52) ## ol.clave.acme.solver.http # ol.clave.acme.solver.http HTTP-01 challenge solver with Ring middleware. This namespace provides a complete HTTP-01 challenge solution for clojure ring servers. * [`solver`](#solver) creates a solver map for use with `ol.clave/obtain-certificate` * [`wrap-acme-challenge`](#wrap-acme-challenge) is Ring middleware that serves challenge responses Usage: ```clojure (require '[ol.clave.acme.solver.http :as http-solver]) (def my-solver (http-solver/solver)) ;; Add middleware to your Ring app (def app (-> your-handler (http-solver/wrap-acme-challenge my-solver))) ;; Use solver with obtain-certificate (clave/obtain-certificate lease session identifiers cert-key {:http-01 my-solver} {}) ``` ## solver ```clojure (solver) ``` Create an HTTP-01 solver that registers challenges in a registry. Creates its own registry atom internally. The [`wrap-acme-challenge`](#wrap-acme-challenge) middleware reads from this registry to serve challenge responses. Returns a solver map with `:present`, `:cleanup`, and `:registry`. Example: ```clojure (def my-solver (solver)) ;; Pass solver to middleware (def app (-> handler (wrap-acme-challenge my-solver))) ;; Use with obtain-certificate (clave/obtain-certificate lease session identifiers cert-key {:http-01 my-solver} {}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/http.clj#L32-L62) --- ## wrap-acme-challenge ```clojure (wrap-acme-challenge handler solver) ``` Ring middleware that serves ACME HTTP-01 challenge responses. Intercepts requests to `/.well-known/acme-challenge/{token}` and returns the key-authorization from the solver’s registry. Other requests pass through to the wrapped handler. Parameters: | name | description | |-----------|------------------------------| | `handler` | The Ring handler to wrap | | `solver` | Solver created by [`solver`](#solver) | Response behavior: - Returns 200 with key-authorization as plain text if token found - Returns 404 if token not in registry - Passes through to handler for non-challenge paths Example: ```clojure (def my-solver (solver)) (def app (-> my-handler (wrap-acme-challenge my-solver))) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/http.clj#L64-L105) --- ## handler ```clojure (handler solver) ``` Standalone Ring handler for ACME HTTP-01 challenges. Use this when you want a dedicated server for challenges rather than integrating with an existing application. Parameters: | name | description | |----------|------------------------------| | `solver` | Solver created by [`solver`](#solver) | Returns a Ring handler function. Example: ```clojure (def my-solver (solver)) ;; Start a dedicated challenge server (run-jetty (http-solver/handler my-solver) {:port 80}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/http.clj#L107-L129) ## ol.clave.acme.solver.tls-alpn # ol.clave.acme.solver.tls-alpn TLS-ALPN-01 challenge solver. Provides two solver implementations: - [`bootstrap-solver`](#bootstrap-solver) - Starts temporary SSLServerSocket (before TLS server running) - [`integrated-solver`](#integrated-solver) - Registers with KeyManager (when TLS server running) For typical use, [`switchable-solver`](#switchable-solver) creates a solver that starts in bootstrap mode and can be switched to integrated mode after your TLS server starts. Usage: ```clojure (require '[ol.clave.acme.solver.tls-alpn :as tls-alpn]) (def solver (tls-alpn/switchable-solver {:port 443})) ;; Use solver with automation system (auto/start {:solvers {:tls-alpn-01 solver} ...}) (auto/manage-domains system ["example.com"]) ;; Pass registry to sni-alpn-ssl-context (jetty-ext/sni-alpn-ssl-context lookup-fn (tls-alpn/challenge-registry solver)) ;; After TLS server starts, switch to integrated mode for renewals (tls-alpn/switch-to-integrated! solver) ``` ## bootstrap-solver ```clojure (bootstrap-solver {:keys [port] :or {port 443}}) ``` TLS-ALPN-01 solver that starts a temporary server. Use for initial certificate acquisition before the main TLS server starts. Starts an SSLServerSocket during `:present`, stops it during `:cleanup`. Options: | key | description | default | |---------|---------------------------|---------| | `:port` | Port for challenge server | 443 | Returns a solver map with `:present` and `:cleanup` functions. ```clojure (def solver (bootstrap-solver {:port 8443})) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L120-L157) --- ## integrated-solver ```clojure (integrated-solver) ``` TLS-ALPN-01 solver that registers with an existing TLS server. Use for certificate renewals when the main TLS server is running. Registers challenge cert data in a registry for the server’s KeyManager to serve during ALPN handshakes. Creates its own registry atom internally. Use [`challenge-registry`](#challenge-registry) to get the registry for `sni-alpn-ssl-context`. Returns a solver map with `:present`, `:cleanup`, and `:registry`. ```clojure (def solver (integrated-solver)) (jetty-ext/sni-alpn-ssl-context lookup-fn (challenge-registry solver)) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L159-L195) --- ## switchable-solver ```clojure (switchable-solver {:keys [port] :or {port 443}}) ``` Create a TLS-ALPN-01 solver that can switch from bootstrap to integrated mode. Returns a solver map with `:present`, `:cleanup`, `:switch-to-integrated!`, and `:registry`. Pass directly to the automation system’s `:solvers` config. Creates its own registry atom internally. Use [`challenge-registry`](#challenge-registry) to get the registry for `sni-alpn-ssl-context`. Starts in bootstrap mode (starts temporary server for initial cert). Call [`switch-to-integrated!`](#switch-to-integrated!) after your TLS server starts so renewals use the integrated solver (registers in registry for KeyManager to serve). | name | description | |--------|----------------------------------------| | `opts` | Options map with `:port` (default 443) | ```clojure (def solver (switchable-solver {:port 8443})) ;; Use solver with automation (auto/start {:solvers {:tls-alpn-01 solver} ...}) (auto/manage-domains system ["example.com"]) ;; Pass registry to sni-alpn-ssl-context (jetty-ext/sni-alpn-ssl-context lookup-fn (challenge-registry solver)) ;; After TLS server starts (switch-to-integrated! solver) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L197-L237) --- ## switch-to-integrated! ```clojure (switch-to-integrated! {:keys [switch-to-integrated!]}) ``` Switch a switchable solver from bootstrap to integrated mode. Call this after your TLS server has started. Future challenge validations will use the integrated solver (registers cert data in registry for the KeyManager to serve). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L239-L246) --- ## challenge-registry ```clojure (challenge-registry {:keys [registry]}) ``` Get the challenge registry atom from a solver. Use this to pass the registry to `sni-alpn-ssl-context` for ALPN challenge support. ```clojure (def solver (switchable-solver {:port 443})) (jetty-ext/sni-alpn-ssl-context lookup-fn (challenge-registry solver)) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L248-L258) ## ol.clave.automation.impl.cache # ol.clave.automation.impl.cache In-memory certificate cache for the automation layer. The cache provides fast certificate lookup for TLS handshakes and iteration for maintenance loop. Certificates are indexed by SAN for efficient domain-based lookups. ## cache-certificate ```clojure (cache-certificate cache_ bundle) ``` Add or update a certificate in the cache. If `:capacity` is set in the cache and adding would exceed it, one random certificate is evicted first. | key | description | |----------|-----------------------------------------------------| | `cache_` | Atom containing {:certs {} :index {} :capacity nil} | | `bundle` | Certificate bundle with :hash and :names | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L44-L70) --- ## lookup-cert ```clojure (lookup-cert cache_ hostname) ``` Find certificate for hostname. Tries exact match first, then wildcard match. | key | description | |------------|---------------------------------------| | `cache_` | Atom containing {:certs {} :index {}} | | `hostname` | Hostname to look up | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L80-L96) --- ## remove-certificate ```clojure (remove-certificate cache_ bundle) ``` Remove a certificate from the cache. | key | description | |----------|-----------------------------------------------------| | `cache_` | Atom containing {:certs {} :index {} :capacity nil} | | `bundle` | Certificate bundle with :hash and :names to remove | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L98-L112) --- ## update-ocsp-staple ```clojure (update-ocsp-staple cache_ hash ocsp-response) ``` Update OCSP staple in existing cached bundle. | key | description | |-----------------|-----------------------------------------------------| | `cache_` | Atom containing {:certs {} :index {} :capacity nil} | | `hash` | Hash of the certificate to update | | `ocsp-response` | New OCSP staple data | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L114-L128) --- ## update-ari-data ```clojure (update-ari-data cache_ hash ari-data) ``` Update ARI data in existing cached bundle. | key | description | |------------|---------------------------------------------------------------------| | `cache_` | Atom containing {:certs {} :index {} :capacity nil} | | `hash` | Hash of the certificate to update | | `ari-data` | ARI data with `:suggested-window`, `:selected-time`, `:retry-after` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L130-L143) --- ## mark-managed ```clojure (mark-managed cache_ hash) ``` Set the :managed flag on a cached bundle. Used when a previously-cached (unmanaged) certificate becomes managed via `manage-domains` after passing validation. | key | description | |----------|---------------------------------------| | `cache_` | Atom containing {:certs {} :index {}} | | `hash` | Hash of the certificate to update | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L145-L160) --- ## newer-than-cache? ```clojure (newer-than-cache? stored-bundle cached-bundle) ``` Check if a stored certificate is newer than the cached version. Compares certificates by their `:not-before` timestamp. Returns true if the stored certificate was issued after the cached one. | key | description | |-----------------|---------------------------------| | `stored-bundle` | Certificate bundle from storage | | `cached-bundle` | Certificate bundle from cache | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L162-L175) --- ## hash-certificate ```clojure (hash-certificate cert-chain) ``` Compute a consistent hash of certificate chain bytes. Uses SHA-256 to produce a unique identifier for a certificate chain. The hash is stable: same input always produces the same output. | key | description | |--------------|----------------------------------------------------------------| | `cert-chain` | Vector of byte arrays (certificate chain in DER or PEM format) | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L185-L199) --- ## create-bundle ```clojure (create-bundle certs private-key issuer-key managed?) ``` Create a certificate bundle from ACME response data. Extracts SANs, computes hash, and creates a complete bundle map suitable for caching and TLS use. | key | description | |---------------|-----------------------------------------------------| | `certs` | Vector of X509Certificate objects (chain) | | `private-key` | Private key for the certificate | | `issuer-key` | Identifier for the issuer (e.g., CA directory host) | | `managed?` | Whether cert is actively managed for renewal | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L218-L244) --- ## handle-command-result ```clojure (handle-command-result cache_ cmd result) ``` Update cache based on command result. Handles cache updates for different command types: - `:obtain-certificate` success: adds new certificate to cache - `:renew-certificate` success: removes old cert, adds new cert - `:fetch-ocsp` success: updates OCSP staple in existing bundle Does nothing on failure (`:status :error`). | key | description | |----------|-----------------------------------------------------| | `cache_` | Atom containing {:certs {} :index {}} | | `cmd` | Command descriptor with `:command` and `:bundle` | | `result` | Result map with `:status` and command-specific data | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L246-L283) ## ol.clave.automation.impl.config # ol.clave.automation.impl.config ## resolve-config ```clojure (resolve-config system domain) ``` Merge global config with per-domain overrides. Returns the resolved configuration for a specific domain by merging the global config with any per-domain overrides from config-fn. If config-fn is nil or returns nil, returns the global config unchanged. | key | description | |----------|-----------------------------------------------------------| | `system` | System map containing `:config` and optional `:config-fn` | | `domain` | Domain name to resolve configuration for | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L12-L30) --- ## select-issuer ```clojure (select-issuer config) ``` Select issuers based on the issuer-selection policy. Returns the issuers in the appropriate order based on `:issuer-selection`: - `:in-order` (default) - return issuers in original order - `:shuffle` - return issuers in random order | key | description | |----------|----------------------------------------------------------------| | `config` | Configuration with `:issuers` and optional `:issuer-selection` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L32-L47) --- ## lets-encrypt-production-url Let’s Encrypt production directory URL. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L49-L51) --- ## default-config ```clojure (default-config) ``` Returns the default configuration for the automation layer. Default values: - Issuer: Let’s Encrypt production - Key type: P256 (ECDSA) - OCSP: enabled, must-staple disabled - ARI: enabled - Key reuse: disabled - Cache capacity: unlimited [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L53-L72) --- ## issuer-key-from-url ```clojure (issuer-key-from-url url) ``` Extract issuer key from directory URL. Returns a unique identifier for the issuer based on the URL’s host and path. | key | description | |-------|--------------------| | `url` | ACME directory URL | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L92-L116) --- ## cert-storage-key ```clojure (cert-storage-key issuer-key domain) ``` Generate storage key for a certificate PEM file. Format: `certificates/{issuer-key}/{domain}/{domain}.crt` | key | description | |--------------|-------------------------------------------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | | `domain` | Primary domain name | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L118-L130) --- ## key-storage-key ```clojure (key-storage-key issuer-key domain) ``` Generate storage key for a private key PEM file. Format: `certificates/{issuer-key}/{domain}/{domain}.key` | key | description | |-----|-------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | | `domain` | Primary domain name | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L132-L144) --- ## meta-storage-key ```clojure (meta-storage-key issuer-key domain) ``` Generate storage key for certificate metadata EDN file. Format: `certificates/{issuer-key}/{domain}/{domain}.edn` | key | description | |-----|-------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | | `domain` | Primary domain name | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L146-L158) --- ## certs-prefix ```clojure (certs-prefix issuer-key) ``` Generate storage prefix for listing certificates under an issuer. Format: `certificates/{issuer-key}` | key | description | |--------------|-------------------------------------------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L160-L169) --- ## account-private-key-storage-key ```clojure (account-private-key-storage-key issuer-key) ``` Generate storage key for an account private key PEM file. Format: `accounts/{issuer-key}/account.key` | key | description | |--------------|-------------------------------------------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L171-L180) --- ## account-public-key-storage-key ```clojure (account-public-key-storage-key issuer-key) ``` Generate storage key for an account public key PEM file. | key | description | |--------------|-------------------------------------------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L182-L189) --- ## account-registration-storage-key ```clojure (account-registration-storage-key issuer-key) ``` Generate storage key for account registration EDN. Format: `accounts/{issuer-key}/registration.edn` Contains the account KID (URL) returned by the CA after registration, allowing subsequent operations to skip the newAccount call. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L191-L199) --- ## ocsp-storage-key ```clojure (ocsp-storage-key issuer-key domain) ``` Generate storage key for an OCSP staple file. Format: `certificates/{issuer-key}/{domain}/{domain}.ocsp` The OCSP staple is stored as raw DER-encoded bytes. | key | description | |--------------|-------------------------------------------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | | `domain` | Primary domain name | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L201-L215) --- ## compromised-key-storage-key ```clojure (compromised-key-storage-key domain timestamp) ``` Generate storage key for archiving a compromised private key. Format: `keys/{domain}.compromised.{timestamp}` Compromised keys are archived for audit purposes and never reused. | key | description | |-------------|----------------------------------------------------| | `domain` | Primary domain name | | `timestamp` | ISO-8601 timestamp when key was marked compromised | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L217-L231) --- ## ari-storage-key ```clojure (ari-storage-key issuer-key domain) ``` Generate storage key for ARI (ACME Renewal Information) data. Format: `certificates/{issuer-key}/{domain}/{domain}.ari.edn` The ARI data is stored as EDN containing suggested-window, selected-time, and retry-after. | key | description | |--------------|-------------------------------------------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | | `domain` | Primary domain name | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L233-L248) --- ## challenge-token-storage-key ```clojure (challenge-token-storage-key issuer-key identifier) ``` Generate storage key for a challenge token (distributed solving). Format: `challenge_tokens/{issuer-key}/{identifier}.edn` Used to store challenge data so any instance in a cluster can serve the challenge response for HTTP-01 or TLS-ALPN-01 validation. | key | description | |--------------|-------------------------------------------------| | `issuer-key` | Issuer identifier (hostname from directory URL) | | `identifier` | Domain or IP address being validated | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L250-L265) --- ## select-chain ```clojure (select-chain preference chains) ``` Select a certificate chain based on preference. Preferences: - `:any` (default) - return first chain offered - `:shortest` - return chain with fewest certificates - `{:root "Root CA Name"}` - return chain with matching root name Returns nil if chains is empty. Falls back to first chain if root name not found. | key | description | |--------------|---------------------------------------------------------------| | `preference` | Chain preference (`:any`, `:shortest`, or `{:root name}`) | | `chains` | Sequence of chain maps with `:chain` (certs) and `:root-name` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L267-L299) ## ol.clave.automation.impl.decisions # ol.clave.automation.impl.decisions Pure decision functions for the automation layer. These functions contain all the logic for determining what maintenance actions are needed for certificates. ## *renewal-threshold* Fraction of lifetime remaining that triggers renewal. Default 0.33 means renew when 1/3 of lifetime remains. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L10-L13) --- ## *emergency-override-ari-threshold* Fraction of lifetime remaining that overrides ARI guidance. Default 0.05 (1/20) = 5% of lifetime. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L15-L18) --- ## *emergency-critical-threshold* Fraction of lifetime remaining that triggers critical emergency. Default 0.02 (1/50) = 2% of lifetime. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L20-L23) --- ## *emergency-min-intervals* Minimum maintenance intervals before expiration for critical status. Default 5 intervals. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L25-L28) --- ## *ocsp-refresh-threshold* Fraction of OCSP validity window that triggers refresh. Default 0.5 = refresh at 50% of validity window. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L30-L33) --- ## *short-lived-threshold-ms* Threshold for short-lived certificates (7 days in ms). Certificates shorter than this use different renewal logic. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L35-L38) --- ## *max-retry-duration-ms* Maximum duration to retry a failing operation (30 days). After this duration, the operation fails permanently. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L40-L43) --- ## short-lived-cert? ```clojure (short-lived-cert? bundle) ``` Check if a certificate is short-lived (< 7 days lifetime). Short-lived certificates (like those from ACME staging or specialized CAs) require different renewal timing logic. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L45-L54) --- ## ari-suggests-renewal? ```clojure (ari-suggests-renewal? bundle now maintenance-interval-ms) ``` Check if ARI data suggests renewal is due. Per RFC 9710, returns true if current time is past the cutoff, where cutoff = selected-time minus maintenance-interval. This ensures we don’t miss the renewal window if maintenance runs just after the selected time. Returns false if no ARI data or no selected-time is present. | key | description | |---------------------------|----------------------------------------------| | `bundle` | Certificate bundle with optional `:ari-data` | | `now` | Current instant | | `maintenance-interval-ms` | Maintenance loop interval in milliseconds | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L56-L76) --- ## calculate-ari-renewal-time ```clojure (calculate-ari-renewal-time ari-data) (calculate-ari-renewal-time ari-data rng) ``` Calculate a random time within the ARI suggested renewal window. | key | description | |------------|---------------------------------------------------------------| | `ari-data` | ARI data with `:suggested-window` [start-instant end-instant] | | `rng` | `java.util.Random` instance for random selection (optional) | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L78-L93) --- ## needs-renewal? ```clojure (needs-renewal? bundle now maintenance-interval-ms) ``` Check if certificate needs renewal based on expiration, ARI, and emergency. Returns true if: - Less than 5% (`**emergency-override-ari-threshold**`) of lifetime remains (supersedes ARI guidance for safety), OR - ARI selected-time cutoff has passed (per RFC 9710), OR - Less than `**renewal-threshold**` (default 1/3) of lifetime remains The emergency override ensures we never rely solely on ARI when the certificate is dangerously close to expiration. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L95-L119) --- ## emergency-renewal? ```clojure (emergency-renewal? bundle now maintenance-interval-ms) ``` Check if certificate is dangerously close to expiration. Tiered thresholds: - `:critical` - 1/50 (2%) lifetime remaining OR fewer than 5 maintenance intervals - `:override-ari` - 1/20 (5%) lifetime remaining, overrides ARI guidance - `nil` - no emergency [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L121-L144) --- ## needs-ocsp-refresh? ```clojure (needs-ocsp-refresh? bundle config now) ``` Check if OCSP staple needs refresh. Returns true if: - OCSP is enabled in config AND - Certificate is not short-lived (>= 7 days) AND - Staple is nil OR past 50% of validity window Returns false if: - OCSP is disabled, OR - Certificate is short-lived (< 7 days lifetime) Short-lived certificates don’t benefit from OCSP stapling because the certificate will expire before the OCSP response provides value. | key | description | |----------|----------------------------------------------------------| | `bundle` | Certificate bundle with optional `:ocsp-staple` | | `config` | Configuration with `:ocsp` map containing `:enabled` key | | `now` | Current instant | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L169-L196) --- ## check-cert-maintenance ```clojure (check-cert-maintenance bundle config now maintenance-interval-ms) ``` Returns commands needed for this certificate. Pure function that examines certificate state and returns a vector of command descriptors. Does not perform any I/O. | key | description | |---------------------------|-------------------------------------------| | `bundle` | Certificate bundle from cache | | `config` | Resolved configuration for this domain | | `now` | Current instant | | `maintenance-interval-ms` | Maintenance loop interval in milliseconds | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L198-L221) --- ## command-key ```clojure (command-key cmd) ``` Generate a key for command deduplication. Returns a vector of [command-type domain] that uniquely identifies a command. Commands with the same key are considered duplicates and can be deduplicated by the job queue. | key | description | |-------|-------------------------------------------------------| | `cmd` | Command descriptor with `:command` and `:domain` keys | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L223-L234) --- ## fast-command? ```clojure (fast-command? cmd) ``` Check if a command is fast. | key | description | |-------|----------------------------------------| | `cmd` | Command descriptor with `:command` key | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L240-L247) --- ## classify-error ```clojure (classify-error ex) ``` Classify an exception into an error category. Categories: - `:network-error` - connection failures, DNS issues, timeouts - `:rate-limited` - HTTP 429 responses - `:acme-error` - ACME protocol errors (4xx responses) - `:server-error` - server-side failures (5xx responses) - `:config-error` - configuration problems - `:storage-error` - I/O and storage failures - `:unknown` - unrecognized exceptions | key | description | |------|-----------------------| | `ex` | Exception to classify | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L261-L302) --- ## retryable-error? ```clojure (retryable-error? error-type) ``` Check if an error type should be retried. Retryable errors: - `:network-error` - transient network issues - `:rate-limited` - should back off and retry - `:server-error` - server may recover - `:storage-error` - storage may become available Non-retryable errors: - `:acme-error` - client errors unlikely to succeed - `:config-error` - configuration must be fixed - `:unknown` - cannot determine if safe to retry [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L308-L322) --- ## event-for-result ```clojure (event-for-result cmd result) ``` Create an event from a command result. Event types by command and status: - `:obtain-certificate` success -> `:certificate-obtained` - `:renew-certificate` success -> `:certificate-renewed` - `:obtain-certificate` error -> `:certificate-failed` - `:renew-certificate` error -> `:certificate-failed` - `:fetch-ocsp` success -> `:ocsp-stapled` - `:fetch-ocsp` error -> `:ocsp-failed` | key | description | |----------|----------------------------------------------------| | `cmd` | Command descriptor with `:command` and `:domain` | | `result` | Result map with `:status` (`:success` or `:error`) | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L326-L410) --- ## create-certificate-loaded-event ```clojure (create-certificate-loaded-event bundle) ``` Create an event for a certificate loaded from storage. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L412-L421) --- ## retry-intervals Exponential backoff intervals for failed operations (in milliseconds). The pattern is front-loaded: aggressive early retries (1-2 min) catch transient failures quickly, then backs off through medium intervals (5-20 min) for rate-limiting or brief outages, then longer intervals (30 min - 1 hr) for issues requiring propagation or human intervention. Caps at 6 hours to avoid wasting resources during persistent outages. Repeated consecutive values control dwell time at each tier before escalating (e.g., three 10-minute intervals means 3 attempts at that tier before moving to 20 minutes). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L425-L461) --- ## calculate-maintenance-jitter ```clojure (calculate-maintenance-jitter maintenance-jitter) (calculate-maintenance-jitter maintenance-jitter rng) ``` Calculate random jitter for maintenance loop scheduling. Returns a random value in [0, maintenance-jitter) to spread out maintenance operations and avoid thundering herd problems. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L463-L471) ## ol.clave.automation.impl.domain # ol.clave.automation.impl.domain Domain validation for the automation layer. ## validate-domain ```clojure (validate-domain domain config) ``` Validate a domain name for ACME certificate issuance. Returns nil if the domain is valid, or an error map if it cannot receive ACME certificates. Error map structure: - `:error` - always `:invalid-domain` - `:message` - human-readable explanation Security validations: - Directory traversal patterns (.., /, \) - Invalid characters - Invalid format (leading/trailing dots) - Wildcard format and DNS-01 solver requirement - (when using a public ca) the following are not allowed - localhost, .local, .internal, .test TLDs - private IP addresses | key | description | |----------|---------------------------------| | `domain` | Domain name to validate | | `config` | Configuration with :solvers map | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/domain.clj#L100-L156) ## ol.clave.automation.impl.system # ol.clave.automation.impl.system System lifecycle and component wiring for the automation layer. The system map contains all components and is passed to internal functions. Components access what they need via destructuring. ## *maintenance-interval-ms* Maintenance loop interval in milliseconds (1 hour). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L36-L38) --- ## *maintenance-jitter-ms* Maximum jitter for maintenance loop (5 minutes). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L40-L42) --- ## *fast-semaphore-permits* Concurrent fast command permits (OCSP, ARI). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L44-L46) --- ## *slow-semaphore-permits* Concurrent slow command permits (obtain, renew). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L48-L50) --- ## *shutdown-timeout-ms* Timeout for graceful shutdown in milliseconds (30 seconds). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L52-L54) --- ## *config-fn-timeout-ms* Timeout for config-fn calls in milliseconds (5 seconds). If config-fn takes longer than this, the domain is skipped. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L56-L59) --- ## submit-command! [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L62-L62) --- ## trigger-maintenance! ```clojure (trigger-maintenance! system) ``` See [`ol.clave.automation/trigger-maintenance!`](api/ol-clave-automation.adoc#trigger-maintenance-BANG-) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L388-L391) --- ## create ```clojure (create config) ``` See [`ol.clave.automation/create`](api/ol-clave-automation.adoc#create) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L412-L420) --- ## start! ```clojure (start! system) ``` See [`ol.clave.automation/start!`](api/ol-clave-automation.adoc#start-BANG-) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L422-L428) --- ## stop ```clojure (stop system) ``` See [`ol.clave.automation/stop`](api/ol-clave-automation.adoc#stop) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L430-L444) --- ## started? ```clojure (started? system) ``` See [`ol.clave.automation/started?`](api/ol-clave-automation.adoc#started-QMARK-) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L446-L449) --- ## lookup-cert ```clojure (lookup-cert system hostname) ``` See [`ol.clave.automation/lookup-cert`](api/ol-clave-automation.adoc#lookup-cert) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L488-L507) --- ## manage-domains ```clojure (manage-domains system domains) ``` See [`ol.clave.automation/manage-domains`](api/ol-clave-automation.adoc#manage-domains) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1196-L1232) --- ## unmanage-domains ```clojure (unmanage-domains system domains) ``` See [`ol.clave.automation/unmanage-domains`](api/ol-clave-automation.adoc#unmanage-domains) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1234-L1246) --- ## list-domains ```clojure (list-domains system) ``` See [`ol.clave.automation/list-domains`](api/ol-clave-automation.adoc#list-domains) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1248-L1257) --- ## get-domain-status ```clojure (get-domain-status system domain) ``` See [`ol.clave.automation/get-domain-status`](api/ol-clave-automation.adoc#get-domain-status) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1259-L1267) --- ## has-valid-cert? ```clojure (has-valid-cert? system domain) ``` See [`ol.clave.automation/has-valid-cert?`](api/ol-clave-automation.adoc#has-valid-cert-QMARK-) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1269-L1272) --- ## get-event-queue ```clojure (get-event-queue system) ``` See [`ol.clave.automation/get-event-queue`](api/ol-clave-automation.adoc#get-event-queue) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1274-L1282) --- ## renew-managed ```clojure (renew-managed system) ``` See [`ol.clave.automation/renew-managed`](api/ol-clave-automation.adoc#renew-managed) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1284-L1297) --- ## revoke ```clojure (revoke system certificate opts) ``` See [`ol.clave.automation/revoke`](api/ol-clave-automation.adoc#revoke) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1299-L1337) ## ol.clave.automation # ol.clave.automation Public API for the ACME certificate automation layer. The automation layer manages TLS certificate lifecycle automatically: - Obtains certificates for managed domains - Renews certificates before expiration - Handles OCSP stapling - Provides events for monitoring ## Quick Start ```clojure (require '[ol.clave.automation :as auto] '[ol.clave.storage.file :as fs]) ;; Create the automation system (def system (auto/create {:storage (fs/file-storage "/var/lib/acme") :issuers [{:directory-url "https://acme-v02.api.letsencrypt.org/directory" :email "admin@example.com"}] :solvers {:http-01 my-http-solver}})) ;; Optionally get the event queue before starting (def queue (auto/get-event-queue system)) ;; Start the maintenance loop (auto/start! system) ;; Add domains to manage (auto/manage-domains system ["example.com"]) ;; Look up certificate for TLS handshake (auto/lookup-cert system "example.com") ;; Stop the system (auto/stop system) ``` ## Configuration The config map supports: | key | description | |---------------------|------------------------------------------------------------------------------| | `:storage` | Storage implementation (required) | | `:issuers` | Vector of issuer configs with `:directory-url` and optional `:email`, `:eab` | | `:issuer-selection` | `:in-order` (default) or `:shuffle` | | `:key-type` | `:p256` (default), `:p384`, `:rsa2048`, `:rsa4096`, `:rsa8192`, `:ed25519` | | `:key-reuse` | Reuse private key on renewal (default false) | | `:solvers` | Map of solver types to implementations | | `:ocsp` | OCSP config with `:enabled`, `:must-staple` | | `:ari` | ARI config with `:enabled` | | `:cache-capacity` | Max certificates in cache (nil = unlimited) | | `:config-fn` | Function: domain -> config overrides | | `:http-client` | HTTP client options for ACME requests | ## create ```clojure (create config) ``` Creates the automation system without starting the maintenance loop. Returns a system handle that is not yet started. Throws if configuration is invalid or storage cannot be initialized. After calling this function you might be interested in [`get-event-queue`](#get-event-queue) and [`start!`](#start!). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L58-L67) --- ## start! ```clojure (start! system) ``` Starts the maintenance loop on a created system. Call this after [`create`](#create) to begin automatic certificate management. Idempotent: calling on an already-started system is a no-op. Returns the system handle. See also [`create`](#create). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L69-L79) --- ## stop ```clojure (stop system) ``` Stops the automation system. Signals the maintenance loop to stop, waits for in-flight operations, and releases resources. | key | description | |-----|-------------| | `system` | System handle from `start` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L86-L96) --- ## started? ```clojure (started? system) ``` Returns true if the system is in started state. | key | description | |-----|-------------| | `system` | System handle from `start` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L98-L105) --- ## manage-domains ```clojure (manage-domains system domains) ``` Adds domains to management, triggering immediate certificate obtain. Returns `nil` on success. Throws with `:errors` in ex-data if any domain is invalid. | key | description | |-----------|----------------------------------| | `system` | System handle from `start` | | `domains` | Vector of domain names to manage | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L107-L118) --- ## unmanage-domains ```clojure (unmanage-domains system domains) ``` Removes domains from management. Stops renewal and maintenance for these domains. Certificates remain in storage but are no longer actively managed. | key | description | |-----------|------------------------------------| | `system` | System handle from `start` | | `domains` | Vector of domain names to unmanage | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L120-L131) --- ## lookup-cert ```clojure (lookup-cert system hostname) ``` Finds a certificate for a hostname. Tries exact match first, then wildcard match. Returns the certificate bundle or nil if not found. | key | description | |------------|----------------------------| | `system` | System handle from `start` | | `hostname` | Hostname to look up | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L133-L144) --- ## list-domains ```clojure (list-domains system) ``` Lists all managed domains with status. Returns a vector of maps with `:domain`, `:status`, and `:not-after`. | key | description | |-----|-------------| | `system` | System handle from `start` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L146-L155) --- ## get-domain-status ```clojure (get-domain-status system domain) ``` Gets detailed status for a specific domain. Returns a map with `:domain`, `:status`, `:not-after`, `:issuer`, `:needs-renewal`, or nil if domain is not managed. | key | description | |-----|-------------| | `system` | System handle from `start` | | `domain` | Domain name to check | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L157-L168) --- ## has-valid-cert? ```clojure (has-valid-cert? system domain) ``` Returns true if the system has a valid certificate for the domain. | key | description | |-----|-------------| | `system` | System handle from `start` | | `domain` | Domain name to check | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L170-L178) --- ## get-event-queue ```clojure (get-event-queue system) ``` Gets the event queue handle for monitoring. The queue is created lazily on first call. Subsequent calls return the same queue instance. Returns a `java.util.concurrent.LinkedBlockingQueue` Poll with `.poll`, `.poll(timeout, unit)`, or `.take`. When the system is stopped via [`stop`](#stop), a `:ol.clave/shutdown` keyword is placed on the queue. Consumers should check for this sentinel to know when to stop polling. | key | description | |----------|------------------------------| | `system` | System handle from `start` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L180-L197) --- ## renew-managed ```clojure (renew-managed system) ``` Forces renewal of all managed certificates. Submits renewal commands for every managed certificate in the cache. Commands are submitted asynchronously - this function returns immediately. Normally certificates are renewed automatically. Use this for testing or when you need to force renewal. Returns the number of certificates queued for renewal. | key | description | |-----|-------------| | `system` | System handle from `start` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L199-L214) --- ## revoke ```clojure (revoke system certificate opts) ``` Revokes a certificate. The `certificate` parameter can be: - A domain string - looks up the certificate from the cache - A bundle map - uses the bundle directly | key | description | |-----|-------------| | `system` | System handle from `start` | | `certificate` | Domain string or bundle map | | `opts` | Options map (see below) | Options: | key | description | |-----|-------------| | `:remove-from-storage` | When true, deletes certificate files from storage | | `:reason` | RFC 5280 revocation reason code (0-6, 8-10) | Returns `{:status :success}` on successful revocation, or `{:status :error :message ...}` on failure. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L216-L239) --- ## trigger-maintenance! ```clojure (trigger-maintenance! system) ``` Manually triggers a maintenance cycle. This is primarily useful for testing - in normal operation the maintenance loop runs automatically at regular intervals. | key | description | |-----|-------------| | `system` | System handle from `start` | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L241-L251) ## ol.clave.certificate.impl.csr # ol.clave.certificate.impl.csr Pure Clojure PKCS#10 CSR generation with no external dependencies. Supports RSA (2048, 3072, 4096), ECDSA (P-256, P-384), and Ed25519. Automatically handles IDNA conversion for internationalized domains. Validates and normalizes Subject Alternative Names. No other extensions or key types are supported. If you need more features then you will need to use external tools to provide your own CSR. ## create-csr ```clojure (create-csr key-pair sans & [opts]) ``` See [`ol.clave.certificate/csr`](api/ol-clave-certificate.adoc#csr) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/csr.clj#L342-L402) ## ol.clave.certificate.impl.keygen # ol.clave.certificate.impl.keygen Key generation utilities for creating keypairs to back TLS certificates. Supports multiple key types for certificate signing: - ECDSA curves: P-256 (secp256r1), P-384 (secp384r1) - EdDSA: Ed25519 - RSA: 2048, 4096, and 8192 bit keys Note: ACME account keys are managed separately via `ol.clave.impl.crypto`. This namespace is specifically for certificate keypairs. ## supported-key-types Set of supported key types for certificate keypair generation. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L22-L29) --- ## pem-encode ```clojure (pem-encode type der) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L31-L36) --- ## private-key->pem ```clojure (private-key->pem private-key) ``` See [`ol.clave.certificate/private-key->pem`](api/ol-clave-certificate.adoc#private-key--GT-pem) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L38-L41) --- ## gen-ed25519 ```clojure (gen-ed25519) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L48-L51) --- ## gen-p256 ```clojure (gen-p256) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L53-L57) --- ## gen-p384 ```clojure (gen-p384) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L59-L63) --- ## gen-rsa2048 ```clojure (gen-rsa2048) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L65-L69) --- ## gen-rsa4096 ```clojure (gen-rsa4096) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L71-L75) --- ## gen-rsa8192 ```clojure (gen-rsa8192) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L77-L81) --- ## generate ```clojure (generate key-type) ``` See [`ol.clave.certificate/keypair`](api/ol-clave-certificate.adoc#keypair) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L83-L95) ## ol.clave.certificate.impl.ocsp # ol.clave.certificate.impl.ocsp Pure Clojure OCSP (Online Certificate Status Protocol) utilities. Provides functionality to: - Extract OCSP responder URLs from certificates - Fetch OCSP responses from responders - Parse and validate OCSP responses ## extract-ocsp-urls ```clojure (extract-ocsp-urls cert) ``` Extract OCSP responder URLs from a certificate’s AIA extension. Returns a vector of OCSP URLs, or empty vector if none found. | key | description | |-----|-------------| | `cert` | X509Certificate to extract OCSP URLs from | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L61-L90) --- ## create-ocsp-request ```clojure (create-ocsp-request cert issuer) ``` Create an OCSP request for a certificate. Requires both the leaf certificate and its issuer certificate. Returns the DER-encoded OCSP request bytes. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L193-L202) --- ## parse-ocsp-response ```clojure (parse-ocsp-response response-bytes) ``` Parse an OCSP response and extract status information. Returns a map with: - `:status` - One of :good, :revoked, :unknown, or :error - `:this-update` - When this response was generated - `:next-update` - When the response expires - `:revocation-time` - For revoked certs, when it was revoked - `:revocation-reason` - For revoked certs, the reason code - `:raw-bytes` - The original DER-encoded response - `:error-code` - For error responses, the OCSP error code - `:message` - For error responses, the error message [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L342-L387) --- ## fetch-ocsp-response ```clojure (fetch-ocsp-response cert issuer responder-url http-opts) ``` Fetch OCSP response for a certificate from the specified responder. | key | description | |-----|-------------| | `cert` | The X509Certificate to check | | `issuer` | The issuer certificate | | `responder-url` | URL of the OCSP responder | | `http-opts` | HTTP client options map | Returns a result map: - On success: `{:status :success :ocsp-response {...}}` - On failure: `{:status :error :message "..."}` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L391-L428) --- ## fetch-ocsp-for-bundle ```clojure (fetch-ocsp-for-bundle bundle http-opts responder-overrides) ``` Fetch OCSP response for a certificate bundle. Extracts the OCSP URL from the leaf certificate and fetches the response. Supports responder URL overrides for testing. | key | description | |-----|-------------| | `bundle` | Certificate bundle with `:certificate` chain | | `http-opts` | HTTP client options | | `responder-overrides` | Optional map of original-url -> override-url | Returns a result map: - On success: `{:status :success :ocsp-response {...}}` - On failure: `{:status :error :message "..."}` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L430-L468) ## ol.clave.certificate.impl.parse # ol.clave.certificate.impl.parse ## parse-pem-chain ```clojure (parse-pem-chain pem) ``` Parse a PEM-encoded certificate chain into structured data. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/parse.clj#L36-L44) --- ## parse-pem-response ```clojure (parse-pem-response resp url) ``` Validate and parse a PEM certificate response. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/parse.clj#L46-L61) ## ol.clave.certificate.impl.tls-alpn # ol.clave.certificate.impl.tls-alpn TLS-ALPN-01 challenge certificate generation. Builds self-signed X.509 v3 certificates containing the acmeValidationV1 extension required by RFC 8737 for TLS-ALPN-01 ACME challenges. ## acme-tls-1-protocol ALPN protocol identifier for TLS-ALPN-01 challenges (RFC 8737 Section 6.2). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/tls_alpn.clj#L25-L27) --- ## tlsalpn01-challenge-cert ```clojure (tlsalpn01-challenge-cert identifier key-authorization) ``` See [`ol.clave.acme.challenge/tlsalpn01-challenge-cert`](api/ol-clave-acme-challenge.adoc#tlsalpn01-challenge-cert) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/tls_alpn.clj#L184-L223) ## ol.clave.certificate.impl.x509 # ol.clave.certificate.impl.x509 X.509 encoding utilities for certificates and CSRs. We don’t implement all of X.509 (lol), we implement just enough to: - generate CSRs - generate TLS-ALPN-01 challenge certificates Provides: - IDNA encoding for internationalized domain names in SANs - GeneralName encoding for Subject Alternative Names - Extension encoding for certificate extensions ## idna-encode ```clojure (idna-encode domain) ``` Convert Unicode domain to ASCII (Punycode) using IDNA. Normalizes to lowercase first, then applies IDNA conversion. Throws ex-info with ::errors/invalid-idna on failure. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L22-L36) --- ## encode-extension ```clojure (encode-extension oid critical? value) ``` Encode a single X.509 extension. Arguments: oid - OID string (e.g., "2.5.29.17" for subjectAltName) critical? - Boolean indicating if extension is critical value - DER-encoded extension value bytes Returns DER-encoded extension SEQUENCE. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L38-L53) --- ## encode-dns-general-name ```clojure (encode-dns-general-name domain) ``` Encode DNS GeneralName (context tag 2). Applies IDNA encoding to the domain before encoding. Returns DER-encoded GeneralName for DNS identifier. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L55-L62) --- ## encode-ip-general-name ```clojure (encode-ip-general-name ip-bytes) ``` Encode IP GeneralName (context tag 7) from raw bytes. Arguments: ip-bytes - Raw IP address bytes (4 bytes for IPv4, 16 bytes for IPv6) Returns DER-encoded GeneralName for IP identifier. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L64-L72) ## ol.clave.certificate # ol.clave.certificate Certificate acquisition and management. This namespace provides high-level functions for obtaining TLS certificates from ACME servers, abstracting away the multi-step protocol workflow. The main entry point is [`obtain`](#obtain) which orchestrates the complete ACME workflow: creating orders, solving challenges, finalizing with a CSR, and downloading the issued certificate. Solvers are maps containing functions that handle challenge provisioning: - `:present` (required) - provisions resources for ACME validation - `:cleanup` (required) - removes provisioned resources - `:wait` (optional) - waits for slow provisioning (e.g., DNS propagation) - `:payload` (optional) - generates custom challenge response payload See `ol.clave.solver.http` for an HTTP-01 solver with Ring middleware. See also `ol.clave.commands` for low-level plumbing operations. ## validate-solvers ```clojure (validate-solvers solvers) ``` Validate that all solvers have required :present and :cleanup functions. Solvers are maps that must contain: - `:present` - function to provision challenge resources - `:cleanup` - function to clean up provisioned resources Optional keys are allowed (permissive mode): - `:wait` - function for slow provisioning operations - `:payload` - function to generate custom challenge payload - Any other keys (for user metadata) Returns nil on success, throws on validation failure. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L38-L63) --- ## wrap-solver-for-distributed ```clojure (wrap-solver-for-distributed storage issuer-key storage-key-fn solver) ``` Wraps a solver to store/cleanup challenge tokens in shared storage. This enables distributed challenge solving where multiple instances behind a load balancer can serve ACME validation responses. On `:present`: stores challenge data to storage before calling underlying solver. On `:cleanup`: calls underlying solver cleanup, then deletes from storage. The stored data is a JSON map containing the full challenge plus key-authorization, enabling any instance to reconstruct and serve the response. | key | description | |-----|-------------| | `storage` | Storage implementation for persisting challenge tokens | | `issuer-key` | Issuer identifier for storage key namespacing | | `storage-key-fn` | Function `(fn [issuer-key identifier]) -> storage-key` | | `solver` | The underlying solver map to wrap | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L67-L118) --- ## wrap-solvers-for-distributed ```clojure (wrap-solvers-for-distributed storage issuer-key storage-key-fn solvers) ``` Wraps all solvers in a map for distributed challenge solving. | key | description | |-----|-------------| | `storage` | Storage implementation | | `issuer-key` | Issuer identifier | | `storage-key-fn` | Function to generate storage keys | | `solvers` | Map of challenge-type -> solver | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L120-L134) --- ## lookup-challenge-token ```clojure (lookup-challenge-token storage issuer-key storage-key-fn identifier) ``` Lookup a stored challenge token from shared storage. Returns the challenge data map if found, nil otherwise. | key | description | |-----|-------------| | `storage` | Storage implementation | | `issuer-key` | Issuer identifier | | `storage-key-fn` | Function to generate storage keys | | `identifier` | Domain or IP being validated | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L136-L150) --- ## obtain ```clojure (obtain the-lease session identifiers cert-keypair solvers opts) ``` Obtain a certificate from an ACME server using configured solvers. This function automates the complete ACME workflow defined in RFC 8555 Section 7.1: creating an order, solving authorization challenges, finalizing with a CSR, and downloading the issued certificate. Parameters: | name | description | |---------------|------------------------------------------------------------| | `the-lease` | Lease for cancellation and timeout control | | `session` | Authenticated ACME session with account key and KID | | `identifiers` | Vector of identifier maps from `order/create-identifier` | | `cert-keypair`| KeyPair for the certificate (distinct from account key) | | `solvers` | Map of challenge type keyword to solver map | | `opts` | Optional configuration map | Options map: | key | description | |-------------------------|----------------------------------------------------| | `:not-before` | Requested validity start (java.time.Instant) | | `:not-after` | Requested validity end (java.time.Instant) | | `:profile` | ACME profile name when CA supports profiles | | `:preferred-challenges` | Vector of challenge types in preference order | | `:poll-interval-ms` | Polling interval for authorization/order | | `:poll-timeout-ms` | Polling timeout | Returns `[updated-session result]` where result is a map: | key | description | |----------------|----------------------------------------------------------| | `:order` | Final order map with status "valid" | | `:certificates`| Vector of certificate maps | | `:attempts` | Number of order creation attempts made | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L285-L443) --- ## identifiers-from-sans ```clojure (identifiers-from-sans sans) ``` Convert a sequence of SAN strings to identifier maps. Automatically detects IP addresses (IPv4 and IPv6) vs DNS names. Example: ```clojure (identifiers-from-sans ["example.com" "192.168.1.1" "2001:db8::1"]) ;; => [{:type "dns" :value "example.com"} ;; {:type "ip" :value "192.168.1.1"} ;; {:type "ip" :value "2001:db8::1"}] ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L447-L465) --- ## obtain-for-sans ```clojure (obtain-for-sans the-lease session sans cert-key solvers) (obtain-for-sans the-lease session sans cert-key solvers opts) ``` Simplified certificate acquisition for the common case. Automatically creates identifiers from SAN strings. Parameters: | name | description | |--------------|------------------------------------------------| | `the-lease` | Lease for cancellation/timeout | | `session` | Authenticated ACME session | | `sans` | Vector of SAN strings (domains, IPs) | | `cert-key` | KeyPair for certificate | | `solvers` | Map of challenge type keyword to solver map | | `opts` | Optional configuration map (see [`obtain`](#obtain)) | Returns `[updated-session result]` as with `obtain-certificate`. Example: ```clojure (obtain-certificate-for-sans (lease/background) session ["example.com" "www.example.com"] cert-key {:http-01 http-solver}) ;; With options (obtain-certificate-for-sans (lease/background) session ["example.com"] cert-key {:http-01 http-solver :tls-alpn-01 tls-solver} {:preferred-challenges [:http-01 :tls-alpn-01]}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L467-L506) --- ## csr ```clojure (csr keypair sans & [opts]) ``` Generate a PKCS#10 CSR from a KeyPair SANs (Subject Alternative Names) are automatically processed: - Unicode domains are converted to Punycode using IDNA encoding. - Wildcard usage is validated per RFC 6125 (DNS names only). - IP address format is validated for both IPv4 and IPv6. - Values are deduplicated and normalized. Arguments: key-pair - java.security.KeyPair (RSA, EC, or EdDSA). Required. sans - Vector of strings (domain names or IP addresses). Required. Examples: ["example.com" "*.example.com" "192.0.2.1" "2001:db8::1"] opts - Map of options. Optional, defaults to {}. :use-cn? - Boolean. If true, set Subject CN to the first DNS SAN. Default false. IPs are skipped when searching for CN value. When false, Subject is empty and all identifiers are in SANs only (one of three valid options per RFC 8555 Section 7.4). Returns: {:csr-pem String - PEM-encoded CSR :csr-der bytes - Raw DER bytes :csr-b64url String - Base64URL-encoded DER (no padding) for ACME :algorithm Keyword - :rsa-2048, :rsa-3072, :rsa-4096, :ec-p256, :ec-p384, or :ed25519 :details Map - Algorithm OIDs, signature info} Supports RSA (2048, 3072, 4096), ECDSA (P-256, P-384), and Ed25519. Automatically handles IDNA conversion for internationalized domains. Validates and normalizes Subject Alternative Names. No other extensions or key types are supported. If you need more features then you will need to use external tools to provide your own CSR. Examples: ;; Modern ACME: no CN in subject (use-cn? = false, the default) (create-csr kp ["example.com" "*.example.com"]) (create-csr kp ["example.com" "www.example.com"] {}) ;; Legacy: CN = first DNS SAN (IPs are skipped) (create-csr kp ["example.com" "www.example.com"] {:use-cn? true}) ;; Mixed DNS and IP SANs (auto-detected) (create-csr kp ["example.com" "192.0.2.1" "2001:db8::1"]) ;; Unicode domains (auto-converted to Punycode) (create-csr kp ["münchen.example" "www.münchen.example"]) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L508-L555) --- ## private-key->pem ```clojure (private-key->pem private-key) ``` Encode a private key as PKCS#8 PEM-formatted string. ```clojure (private-key->pem (.getPrivate keypair)) ;; => "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L557-L565) --- ## keypair ```clojure (keypair) (keypair key-type) ``` Generate a keypair of the specified type. `key-type` must be one of [`supported-key-types`](api/ol-clave-certificate-impl-keygen.adoc#supported-key-types): - `:ed25519` - Ed25519 curve - `:p256` - ECDSA P-256 (default, recommended) - `:p384` - ECDSA P-384 - `:rsa2048` - RSA 2048-bit - `:rsa4096` - RSA 4096-bit - `:rsa8192` - RSA 8192-bit If you don’t know which one to choose, just use the default. Returns a `java.security.KeyPair`. ```clojure (generate :p256) ;; => #object[java.security.KeyPair ...] (.getPublic (generate :ed25519)) ;; => #object[sun.security.ec.ed.EdDSAPublicKeyImpl ...] ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L567-L593) ## ol.clave.crypto.impl.der # ol.clave.crypto.impl.der DER (Distinguished Encoding Rules) encoding and decoding for ASN.1 structures. This namespace provides low-level functions for encoding and decoding ASN.1 data structures according to ITU-T X.690 (DER encoding rules). We implement just enough to: - Generate CSRs - Generate TLS-ALPN-01 challenge certificates - Parse certificate extensions (AIA, AKI) - Parse OCSP responses Encoding functions return byte arrays. Decoding functions work with byte arrays and return Clojure data structures. ## tag-boolean [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L29-L29) --- ## tag-integer [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L30-L30) --- ## tag-bit-string [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L31-L31) --- ## tag-octet-string [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L32-L32) --- ## tag-null [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L33-L33) --- ## tag-oid [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L34-L34) --- ## tag-enumerated [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L35-L35) --- ## tag-utf8-string [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L36-L36) --- ## tag-printable-string [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L37-L37) --- ## tag-ia5-string [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L38-L38) --- ## tag-utc-time [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L39-L39) --- ## tag-generalized-time [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L40-L40) --- ## tag-sequence [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L41-L41) --- ## tag-set [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L42-L42) --- ## class-context-specific [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L44-L44) --- ## constructed-bit [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L45-L45) --- ## encode-length ```clojure (encode-length length) ``` Encode DER length octets (short or long form). Short form (length < 128): single byte with the length value. Long form (length >= 128): first byte has high bit set and indicates number of length bytes, followed by length bytes in big-endian order. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L49-L68) --- ## concat-bytes ```clojure (concat-bytes & arrays) ``` Concatenate multiple byte arrays into a new byte array. Efficiently copies all input arrays into a single output array. Returns empty array if no arrays provided. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L70-L83) --- ## der-primitive ```clojure (der-primitive tag content) ``` Encode DER primitive with given tag and content. Tag should be a single-byte tag value (e.g., 0x02 for INTEGER). Content is the raw bytes for this primitive. Returns complete TLV (tag-length-value) encoded byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L87-L100) --- ## der-constructed ```clojure (der-constructed tag content) ``` Encode DER constructed with given tag and content. Tag should include the constructed bit (0x20). Content is the concatenated encoding of all child elements. Returns complete TLV (tag-length-value) encoded byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L102-L115) --- ## der-sequence ```clojure (der-sequence & parts) ``` Encode DER SEQUENCE from parts. Concatenates all parts and wraps in SEQUENCE tag (0x30). Returns encoded SEQUENCE byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L119-L126) --- ## der-set ```clojure (der-set & parts) ``` Encode DER SET from parts. DER requires SET elements to be sorted in lexicographic order by their encoded bytes. This implementation sorts all parts before encoding. Returns encoded SET byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L128-L151) --- ## der-integer ```clojure (der-integer value) ``` Encode DER INTEGER from long value. Handles zero specially, uses BigInteger for proper two’s complement encoding. Returns encoded INTEGER byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L153-L162) --- ## der-integer-bytes ```clojure (der-integer-bytes value) ``` Encode DER INTEGER from byte array. Adds leading zero if high bit is set to avoid negative interpretation. Returns encoded INTEGER byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L164-L175) --- ## der-boolean ```clojure (der-boolean v) ``` Encode DER BOOLEAN. True encodes as 0xFF, false as 0x00. Returns encoded BOOLEAN byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L177-L183) --- ## encode-base128 ```clojure (encode-base128 value) ``` Encode a long value as base-128 (for OID encoding). Used by der-oid for encoding OID arc values. Each byte has high bit set except the last byte. Returns base-128 encoded bytes. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L185-L202) --- ## der-oid ```clojure (der-oid dotted) ``` Encode DER OBJECT IDENTIFIER from dotted string. Example: "2.5.4.3" for CN (Common Name). First two arcs are encoded specially per X.690 (40*arc1 + arc2). Returns encoded OID byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L204-L223) --- ## der-utf8-string ```clojure (der-utf8-string s) ``` Encode DER UTF8String. Encodes string using UTF-8 character set. Returns encoded UTF8String byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L225-L231) --- ## der-octet-string ```clojure (der-octet-string content) ``` Encode DER OCTET STRING. Wraps arbitrary byte content in OCTET STRING tag (0x04). Returns encoded OCTET STRING byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L233-L239) --- ## der-bit-string ```clojure (der-bit-string bytes) ``` Encode DER BIT STRING. First content byte specifies number of unused bits (always 0 here). Returns encoded BIT STRING byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L241-L250) --- ## der-utc-time ```clojure (der-utc-time date) ``` Encode DER UTCTime from Date. Format: YYMMDDHHmmssZ (2-digit year, UTC timezone). Used for dates before 2050 per X.509 conventions. Returns encoded UTCTime byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L254-L263) --- ## der-generalized-time ```clojure (der-generalized-time date) ``` Encode DER GeneralizedTime from Date. Format: YYYYMMDDHHmmssZ (4-digit year, UTC timezone). Used for dates in 2050 or later per X.509 conventions. Returns encoded GeneralizedTime byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L265-L274) --- ## der-context-specific-constructed-implicit ```clojure (der-context-specific-constructed-implicit tag-number content) ``` Encode IMPLICIT [tag] CONSTRUCTED with given content. Context-specific tag with constructed bit set (0xA0 | tag-number). Used for IMPLICIT tagging in ASN.1. Returns encoded context-specific byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L278-L286) --- ## der-context-specific-primitive ```clojure (der-context-specific-primitive tag-number raw-content) ``` Encode context-specific PRIMITIVE tag with raw content. Context-specific tag without constructed bit (0x80 | tag-number). Used for IMPLICIT tagging of primitive types in ASN.1. Returns encoded context-specific byte array. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L288-L296) --- ## read-length ```clojure (read-length data offset) ``` Read DER length and return [length bytes-consumed]. Supports short form (single byte < 128) and long form (multi-byte). Returns vector of [length-value number-of-bytes-consumed]. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L300-L320) --- ## read-tlv ```clojure (read-tlv data offset) ``` Read a complete TLV (tag-length-value) structure. Returns a map with: - `:tag` - the raw tag byte - `:tag-class` - :universal, :application, :context-specific, or :private - `:constructed?` - true if constructed (contains other TLVs) - `:tag-number` - the tag number within the class - `:value` - byte array of the value - `:total-length` - total bytes consumed including tag and length [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L322-L354) --- ## decode-sequence-elements ```clojure (decode-sequence-elements data) ``` Decode all elements in a SEQUENCE/SET value. Takes the value bytes (not including SEQUENCE tag/length) and returns a vector of TLV maps. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L356-L368) --- ## decode-integer ```clojure (decode-integer value) ``` Decode a DER INTEGER value bytes to BigInteger. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L372-L375) --- ## decode-enumerated ```clojure (decode-enumerated value) ``` Decode a DER ENUMERATED value bytes to long. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L377-L380) --- ## decode-oid ```clojure (decode-oid value) ``` Decode a DER OID value bytes to dotted string notation. First two arcs are encoded as (40 * arc1 + arc2) in the first byte(s). Subsequent arcs use base-128 encoding with high bit continuation. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L382-L412) --- ## decode-octet-string ```clojure (decode-octet-string value) ``` Return the raw bytes from an OCTET STRING value. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L414-L417) --- ## decode-bit-string ```clojure (decode-bit-string value) ``` Decode a BIT STRING value, returning the actual bits as bytes. First byte indicates unused bits in the last byte. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L419-L430) --- ## decode-ia5-string ```clojure (decode-ia5-string value) ``` Decode an IA5String (ASCII) value to String. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L432-L435) --- ## decode-utf8-string ```clojure (decode-utf8-string value) ``` Decode a UTF8String value to String. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L437-L440) --- ## decode-printable-string ```clojure (decode-printable-string value) ``` Decode a PrintableString value to String. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L442-L445) --- ## decode-generalized-time ```clojure (decode-generalized-time value) ``` Decode a GeneralizedTime value to java.time.Instant. Format: YYYYMMDDHHmmss[.fraction]Z Supports optional fractional seconds. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L458-L469) --- ## decode-utc-time ```clojure (decode-utc-time value) ``` Decode a UTCTime value to java.time.Instant. Format: YYMMDDHHmmssZ (2-digit year, interpreted as 1950-2049). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L480-L490) --- ## find-context-tag ```clojure (find-context-tag elements tag-number) ``` Find a context-specific tagged element by tag number in a sequence of TLVs. Returns the TLV map if found, nil otherwise. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L494-L501) --- ## unwrap-octet-string ```clojure (unwrap-octet-string data) ``` Unwrap a DER OCTET STRING and return its content bytes. Parses the TLV structure and extracts the value. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L503-L511) --- ## unwrap-sequence ```clojure (unwrap-sequence data) ``` Unwrap a DER SEQUENCE and return its decoded elements. Parses the TLV structure and decodes all child elements. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L513-L521) ## ol.clave.crypto.impl.json # ol.clave.crypto.impl.json ## read-str ```clojure (read-str s) (read-str s opts) ``` Returns a Clojure value from the JSON string. Options: :key-fn - Convert JSON keys using this function. Defaults to keyword. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/json.clj#L5-L12) --- ## write-str ```clojure (write-str x) (write-str x opts) ``` Returns a JSON string from the Clojure value. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/json.clj#L14-L18) ## ol.clave.crypto.impl.jwk # ol.clave.crypto.impl.jwk ## key-algorithm ```clojure (key-algorithm key) ``` Return :ol.clave.algo/es256 or :ol.clave.algo/ed25519 for supported keys. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L31-L38) --- ## public-jwk Return the public key as a JWK map. | Key Type | JWK kty | |----------|---------| | EC | "EC" | | Ed25519 | "OKP" | | RSA | "RSA" | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L59-L67) --- ## jwk->canonical-json ```clojure (jwk->canonical-json jwk-map) ``` Render a public JWK map as canonical JSON for JWS embedding. Fields are sorted alphabetically per RFC 7638. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L98-L119) --- ## jwk-thumbprint-from-jwk ```clojure (jwk-thumbprint-from-jwk jwk-map) ``` Compute RFC 7638 thumbprint from a JWK map. Returns base64url-encoded SHA-256 hash of canonical JWK. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L121-L128) --- ## jwk-thumbprint ```clojure (jwk-thumbprint public-key) ``` Compute RFC 7638 thumbprint for a public key. Returns base64url-encoded SHA-256 hash of canonical JWK. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L130-L135) ## ol.clave.crypto.impl.jws # ol.clave.crypto.impl.jws ## jws-alg Return the JWS `alg` header value for a key. | Key Type | Result | |----------|-----------| | P-256 | "ES256" | | P-384 | "ES384" | | Ed25519 | "EdDSA" | | RSA | "RS256" | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L123-L132) --- ## sign Sign data bytes, return signature bytes in JWS format. For ECDSA, returns R||S concatenated (not DER). For EdDSA and RSA, returns raw signature bytes. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L148-L153) --- ## protected-header-json ```clojure (protected-header-json alg kid nonce url jwk-json) ``` Construct the protected header JSON string with deterministic field order. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L194-L218) --- ## final-jws-json ```clojure (final-jws-json protected-b64 payload-b64 signature-b64) ``` Assemble the final JWS JSON object with deterministic ordering. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L220-L225) --- ## protected-dot-payload-bytes ```clojure (protected-dot-payload-bytes protected-b64 payload-b64) ``` Return ASCII bytes of '<protected>.<payload>'. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L227-L230) --- ## encode-payload-b64 ```clojure (encode-payload-b64 payload-json) ``` Base64url-encode the payload JSON string or return the empty string when nil. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L232-L239) --- ## encode-protected-b64 ```clojure (encode-protected-b64 alg kid nonce url jwk-json) ``` Construct and base64url-encode the protected header JSON. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L241-L245) --- ## encode-signature-b64 ```clojure (encode-signature-b64 alg private-key-or-mac protected-dot-payload) ``` Compute the signature for the given alg and return base64url-encoded value. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L247-L256) --- ## jws-encode-json ```clojure (jws-encode-json payload-json keypair kid nonce url) ``` Build a JSON-serialized JWS object. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L281-L291) --- ## jws-encode-eab ```clojure (jws-encode-eab account-key-or-keypair mac-key kid url) ``` Construct an External Account Binding JWS per RFC 8555 section 7.3.4. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L293-L306) ## ol.clave.crypto.impl.parse-ip # ol.clave.crypto.impl.parse-ip Parse a string representation of an IPv4 or IPv6 address into an InetAddress instance. Unlike `InetAddress/getByName`, the functions in this namespace never cause DNS services to be accessed. This avoids potentially blocking/side-effecting network calls that can occur when using the JDK’s built-in methods to parse IP addresses. This implementation is inspired by Google Guava’s InetAddresses class, which provides similar functionality in Java. This code focuses on strict validation of IP address formats according to relevant RFCs. Features: - Non-blocking IP address parsing with pure functions - Strict RFC-compliant validation - Support for all IPv4 formats - Support for all IPv6 formats (including compressed notation and embedded IPv4) - Support for IPv6 scope identifiers The main entry point is `parse-ip`, which takes a string and returns an InetAddress instance or nil if the input is an invalid ip address literal. ## ip-string->bytes ```clojure (ip-string->bytes ip-string) ``` Convert an IP string to bytes. Returns [byte-array scope-id] or nil if invalid. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/parse_ip.clj#L204-L225) --- ## bytes->inet-address ```clojure (bytes->inet-address addr) (bytes->inet-address addr scope) ``` Convert a byte array into an InetAddress. Args: addr: the raw 4-byte or 16-byte IP address in big-endian order scope: optional scope identifier for IPv6 addresses [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/parse_ip.clj#L235-L260) --- ## from-string ```clojure (from-string ip-string) ``` Returns the InetAddress having the given string representation or nil otherwise. This function parses IP address strings without performing DNS lookups, making it suitable for environments where DNS lookups would cause unwanted blocking or side effects. It supports: - IPv4 addresses in dotted decimal format (e.g., "192.168.1.1") - IPv6 addresses in hex format with optional compression (e.g., "2001:db8::1") - IPv6 addresses with scope IDs (e.g., "fe80::1%eth0" or "fe80::1%1") - IPv6 addresses with embedded IPv4 (e.g., "::ffff:192.168.1.1") If the input is already an InetAddress, it is returned unchanged. Args: ip-string: A string representing an IP address or an InetAddress Returns: An InetAddress object, or nil if the input couldn’t be parsed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/parse_ip.clj#L262-L294) ## ol.clave.crypto.impl.util # ol.clave.crypto.impl.util ## qualify-keys ```clojure (qualify-keys ns-prefix m) ``` Qualify the top-level keys of map `m` into `ns-prefix`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/util.clj#L5-L17) ## ol.clave.errors # ol.clave.errors Shared error keyword definitions and helpers for ex-info payloads. ## unsupported-key [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L4-L4) --- ## invalid-header [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L5-L5) --- ## invalid-eab [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L6-L6) --- ## signing-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L7-L7) --- ## ecdsa-signature-format [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L8-L8) --- ## json-escape [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L9-L9) --- ## base64 [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L10-L10) --- ## invalid-account-edn [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L12-L12) --- ## invalid-account [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L13-L13) --- ## invalid-contact [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L14-L14) --- ## invalid-contact-entry [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L15-L15) --- ## invalid-contact-uri [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L16-L16) --- ## invalid-tos [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L17-L17) --- ## invalid-directory [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L19-L19) --- ## account-creation-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L21-L21) --- ## missing-location-header [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L22-L22) --- ## cancelled [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L24-L24) --- ## timeout [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L25-L25) --- ## invalid-scope [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L26-L26) --- ## account-retrieval-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L29-L29) --- ## account-update-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L30-L30) --- ## account-deactivation-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L31-L31) --- ## external-account-required [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L32-L32) --- ## unauthorized-account [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L33-L33) --- ## missing-account-context [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L34-L34) --- ## invalid-account-key [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L35-L35) --- ## account-key-rollover-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L36-L36) --- ## account-key-rollover-verification-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L37-L37) --- ## account-not-found [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L38-L38) --- ## invalid-san [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L41-L41) --- ## invalid-idna [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L42-L42) --- ## invalid-ip [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L43-L43) --- ## encoding-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L44-L44) --- ## unsupported-identifier [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L47-L47) --- ## malformed-pem [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L49-L49) --- ## key-mismatch [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L50-L50) --- ## problem [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L53-L53) --- ## server-error [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L55-L55) --- ## value-too-large [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L57-L57) --- ## order-creation-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L60-L60) --- ## order-retrieval-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L61-L61) --- ## order-not-ready [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L62-L62) --- ## order-invalid [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L63-L63) --- ## order-timeout [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L64-L64) --- ## order-inconsistent [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L65-L65) --- ## authorization-retrieval-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L68-L68) --- ## authorization-invalid [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L69-L69) --- ## authorization-unusable [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L70-L70) --- ## authorization-timeout [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L71-L71) --- ## challenge-rejected [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L72-L72) --- ## no-compatible-challenge [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L73-L73) --- ## invalid-solver [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L76-L76) --- ## pre-authorization-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L79-L79) --- ## pre-authorization-unsupported [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L80-L80) --- ## wildcard-identifier-not-allowed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L81-L81) --- ## certificate-download-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L84-L84) --- ## unexpected-content-type [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L85-L85) --- ## revocation-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L88-L88) --- ## invalid-certificate [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L89-L89) --- ## renewal-info-failed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L92-L92) --- ## renewal-info-invalid [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L93-L93) --- ## problem-type-ns [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L101-L101) --- ## pt-account-does-not-exist [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L103-L103) --- ## pt-already-revoked [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L104-L104) --- ## pt-bad-csr [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L105-L105) --- ## pt-bad-nonce [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L106-L106) --- ## pt-bad-public-key [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L107-L107) --- ## pt-bad-revocation-reason [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L108-L108) --- ## pt-bad-signature-algorithm [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L109-L109) --- ## pt-caa [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L110-L110) --- ## pt-compound [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L111-L111) --- ## pt-connection [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L112-L112) --- ## pt-dns [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L113-L113) --- ## pt-external-account-required [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L114-L114) --- ## pt-incorrect-response [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L115-L115) --- ## pt-invalid-contact [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L116-L116) --- ## pt-malformed [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L117-L117) --- ## pt-order-not-ready [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L118-L118) --- ## pt-rate-limited [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L119-L119) --- ## pt-rejected-identifier [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L120-L120) --- ## pt-server-internal [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L121-L121) --- ## pt-tls [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L122-L122) --- ## pt-unauthorized [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L123-L123) --- ## pt-unsupported-contact [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L124-L124) --- ## pt-unsupported-identifier [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L125-L125) --- ## pt-user-action-required [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L126-L126) --- ## pt-already-replaced [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L128-L128) --- ## failed-identifiers ```clojure (failed-identifiers problem) ``` Extract identifiers from problem subproblems. Returns vector of identifier maps, e.g., `[{:type "dns" :value "example.com"}]`. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L134-L138) --- ## subproblem-for ```clojure (subproblem-for problem identifier) ``` Find subproblem for specific identifier. Returns the first subproblem matching the given identifier, or nil. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L140-L144) --- ## ex ```clojure (ex type message data) (ex type message data cause) ``` Convenience wrapper for ex-info that associates the shared :type key. Usage: (errors/ex errors/invalid-header "message" {:field :kid :reason "missing"}) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L146-L152) ## ol.clave.ext.common # ol.clave.ext.common Common utilities for clave server extensions. This namespace provides server-agnostic helpers for working with clave’s automation layer, including keystore creation and event processing. These functions can be used by any server extension (Jetty, http-kit, etc.). ## create-keystore ```clojure (create-keystore bundle) (create-keystore bundle password) ``` Create an in-memory PKCS12 KeyStore from a clave certificate bundle. No disk I/O - purely in-memory operation suitable for TLS handshakes. | key | description | |------------|-------------------------------------------------------------| | `bundle` | Certificate bundle from [`ol.clave.automation/lookup-cert`](api/ol-clave-automation.adoc#lookup-cert) | | `password` | Optional keystore password (default "changeit") | Returns a `java.security.KeyStore` ready for use with TLS servers. Returns nil if bundle is nil (no certificate available yet). ```clojure (create-keystore (auto/lookup-cert system "example.com")) ;; => #object[java.security.KeyStore ...] ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L15-L44) --- ## certificate-event? ```clojure (certificate-event? evt) ``` Check if an event indicates a certificate change. Returns true for `:certificate-obtained`, `:certificate-renewed`, and `:certificate-loaded` events. | key | description | |-------|----------------------------------------------------| | `evt` | Event from [`ol.clave.automation/get-event-queue`](api/ol-clave-automation.adoc#get-event-queue) | ```clojure (when (certificate-event? evt) (log/info "Certificate updated for" (event-domain evt))) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L46-L62) --- ## event-domain ```clojure (event-domain evt) ``` Extract the domain name from a certificate event. Returns the domain string or nil if event has no domain. | key | description | |-------|-------------| | `evt` | Event map | [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L64-L73) --- ## wrap-redirect-https ```clojure (wrap-redirect-https handler) (wrap-redirect-https handler {:keys [ssl-port] :or {ssl-port 443}}) ``` Ring middleware that redirects HTTP requests to HTTPS. | key | description | |------------|----------------------------------------------------| | `handler` | Ring handler to wrap | | `opts` | Options map with `:ssl-port` | Options: - `:ssl-port` - HTTPS port for redirect URL. Defaults to 443 (implicit, no port in URL). Use a custom port like 8443 to include it explicitly. Passes through requests that are already HTTPS (by `:scheme` or `x-forwarded-proto` header). ```clojure (wrap-redirect-https handler {:ssl-port 8443}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L75-L106) --- ## no-op-solver ```clojure (no-op-solver) ``` Create a no-op ACME solver for testing. Returns a solver that does nothing. Useful with `PEBBLE_VA_ALWAYS_VALID=1` where challenge validation is skipped. ```clojure {:solvers {:http-01 (no-op-solver)}} ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L108-L119) --- ## wait-for-certificates ```clojure (wait-for-certificates system domains) ``` Wait for certificates to be available for all domains. Polls [`ol.clave.automation/lookup-cert`](api/ol-clave-automation.adoc#lookup-cert) once per second until certificates are available for every domain. Blocks indefinitely until the automation system obtains all certificates or throws an error. | key | description | |-----------|---------------------------------| | `system` | clave automation system | | `domains` | Vector of domains to wait for | Returns nil once all certificates are available. ```clojure (wait-for-certificate system ["example.com" "www.example.com"]) (create-keystore (auto/lookup-cert system "example.com")) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L121-L146) ## ol.clave.ext.jetty # ol.clave.ext.jetty Jetty integration for clave automation. Provides SNI-based certificate selection during TLS handshakes. Certificates are looked up on-demand for each connection based on the requested hostname, so renewals take effect immediately. This ns is useful for authors integrating with jetty directly. If you want to use a jetty ring adapter see: * [`ol.clave.ext.ring-jetty-adapter`](api/ol-clave-ext-ring-jetty-adapter.adoc) ## sni-key-manager ```clojure (sni-key-manager lookup-fn) ``` Create an X509ExtendedKeyManager that looks up certificates by SNI hostname. On each TLS handshake, extracts the SNI hostname from the ClientHello and calls `lookup-fn` to retrieve the certificate bundle for that hostname. | key | description | |-------------|---------------------------------------------------------| | `lookup-fn` | Function `(fn [hostname] bundle)` returning cert bundle | The `lookup-fn` receives the SNI hostname and should return a certificate bundle map with `:certificate` (vector of X509Certificate) and `:private-key`. Returns nil if no certificate is available for that hostname. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L34-L85) --- ## sni-ssl-context ```clojure (sni-ssl-context lookup-fn) ``` Create an SSLContext configured with SNI-aware certificate selection. Uses `lookup-fn` to fetch certificates during TLS handshake based on the client’s requested hostname (SNI). Certificates are looked up fresh on each handshake, so renewals take effect immediately. | key | description | |-------------|---------------------------------------------------------| | `lookup-fn` | Function `(fn [hostname] bundle)` returning cert bundle | Returns a `javax.net.ssl.SSLContext` ready for use with Jetty. ```clojure (def ssl-ctx (sni-ssl-context (fn [hostname] (auto/lookup-cert system hostname)))) (jetty/run-jetty handler {:ssl-context ssl-ctx ...}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L87-L112) --- ## sni-alpn-key-manager ```clojure (sni-alpn-key-manager lookup-fn challenge-registry) ``` Create an X509ExtendedKeyManager that handles both SNI and ALPN challenges. Extends the SNI-based certificate selection with TLS-ALPN-01 challenge support. During TLS handshake: 1. If ALPN protocol is 'acme-tls/1' and challenge-registry has data, serve challenge cert 2. Otherwise, use lookup-fn for normal SNI-based cert selection This does NOT interfere with HTTP/2 (h2) negotiation because: - Regular clients offer ["h2", "http/1.1"] -> normal cert via SNI lookup - ACME servers offer ["acme-tls/1"] exclusively -> challenge cert | key | description | |----------------------|---------------------------------------------------| | `lookup-fn` | Function `(fn [hostname] bundle)` for SNI lookup | | `challenge-registry` | Atom with domain->challenge-cert-data map | The challenge-registry contains maps as returned by `tlsalpn01-challenge-cert`: - `:x509` - The X509Certificate to serve - `:keypair` - The KeyPair with private key [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L124-L202) --- ## sni-alpn-ssl-context ```clojure (sni-alpn-ssl-context lookup-fn tls-alpn-solver) ``` Create an SSLContext with SNI cert selection and ALPN challenge support. For normal HTTPS traffic: looks up certs by SNI hostname via lookup-fn. For ACME TLS-ALPN-01 challenges: serves challenge cert when ALPN is 'acme-tls/1'. | key | description | |-------------------|--------------------------------------------------| | `lookup-fn` | Function `(fn [hostname] bundle)` for SNI lookup | | `tls-alpn-solver` | TLS-ALPN solver with `:registry` key ) | Returns a `javax.net.ssl.SSLContext` ready for use with Jetty. ```clojure (def tls-alpn-solver (tls-alpn/switchable-solver {:port 443})) (def ssl-ctx (sni-alpn-ssl-context (fn [hostname] (auto/lookup-cert system hostname)) tls-alpn-solver)) (jetty/run-jetty handler {:ssl-context ssl-ctx ...}) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L204-L231) ## ol.clave.ext.ring-jetty-adapter # ol.clave.ext.ring-jetty-adapter Ring Jetty adapter integration for clave automation. Provides a high-level API for running Jetty with auto-renewing TLS certificates. Wraps ring-jetty-adapter with the same `[handler opts]` signature. Automatically configures both HTTP-01 and TLS-ALPN-01 solvers. Uses SNI-based certificate selection: certificates are looked up fresh on each TLS handshake, so renewals take effect immediately without server restart. ```clojure (require '[ol.clave.ext.ring-jetty-adapter :as clave-jetty]) (def ctx (clave-jetty/run-jetty handler {:port 80 :ssl-port 443 ::clave-jetty/config {:storage (file-storage/file-storage "/tmp/certs") :issuers [{:directory-url "https://acme-v02.api.letsencrypt.org/directory" :email "admin@example.com"}] :domains ["example.com"]}})) ;; Later: stop everything (clave-jetty/stop ctx) ``` ## run-jetty ```clojure (run-jetty handler {::keys [config] :as opts}) ``` Serve `handler` over HTTPS for all `domains` with automatic certificate management. This is an opinionated, high-level convenience function that applies sane defaults for production use: challenge solving, HTTP to HTTPS redirects, and SNI-based certificate selection. Blocks until the initial certificate is obtained, then starts serving. Redirects all HTTP requests to HTTPS (when HTTP port is configured). Obtains and renews TLS certificates automatically. Certificate renewals take effect immediately via SNI-based selection. For advanced use cases, use `ring.adapter.jetty/run-jetty` directly with [`ol.clave.ext.jetty`](api/ol-clave-ext-jetty.adoc) functions for certificate management. `opts` are passed through to `ring.adapter.jetty/run-jetty`. Exception: the `:join?` option from `ring.adapter.jetty/run-jetty` is not supported. Use [`stop`](#stop) to shut down the server instead. Calling this function signifies acceptance of the CA’s Subscriber Agreement and/or Terms of Service. | key | description | |-----------|-------------------------------------------------------------------------------| | `handler` | Ring handler | | `opts` | Options map, see `ring.adapter.jetty/run-jetty` for jetty-adapter’s options | Clave config is provided via the `:ol.clave.ext.ring-jetty-adapter/config` key in `opts`: | key | description | default | |-------------------|-----------------------------------------|-----------| | `:domains` | Domains to manage certs for (required) | | | `:redirect-http?` | Wrap handler with HTTP->HTTPS redirect | true | Additional automation config keys (e.g., `:issuers`, `:storage` etc.) are passed through to [`ol.clave.automation/create`](api/ol-clave-automation.adoc#create). Returns a context map for use with [`stop`](#stop). ```clojure (def server (run-jetty handler {:port 80 :ssl-port 443 ::config {:domains ["example.com"]}})) ;; Later: (stop server) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/ring_jetty_adapter.clj#L45-L119) --- ## stop ```clojure (stop {:keys [server system]}) ``` Stop a server context returned by [`run-jetty`](#run-jetty). Stops the Jetty server and automation system. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/ring_jetty_adapter.clj#L121-L127) ## ol.clave.lease # ol.clave.lease Cooperative cancellation and deadline propagation for concurrent operations. A `lease` represents a bounded lifetime for work. It carries an optional deadline, tracks cancellation state, and provides a completion signal that callers can poll or block on. ## Motivation Individual timeout parameters scattered across function calls lead to inconsistent budgets and subtle hangs. A `lease` provides one coherent boundary: the caller defines the maximum time for an entire operation, and all nested work inherits that constraint. The `lease` is intentionally minimal and does not prescribe any particulary concurrency model. Virtual threads, core.async go-loops, Java StructuredTaskScopes, etc all work equally well with leases. This design enables leases to serve as common ground between libraries with different runtime preferences. A library using one concurrency model can accept the same lease type as a library using another, allowing callers to compose them without adapter layers or runtime coupling. ## Cooperative Cancellation Cancellation are entirely advisory. A `lease` does not forcibly terminate anything. It merely records that cancellation has been requested and notifies interested parties. The lease transitions from `:active` to `:cancelled` or `:timed-out`, but running code continues unless it explicitly checks the lease and decides to stop. This cooperative model requires discipline from functions that receive a lease: they must periodically consult the lease and honor cancellation promptly. The tradeoff is predictability. Code always controls its own teardown, resources are released in an orderly fashion, and there are no surprising interruptions mid-operation. ## Concepts Lease: A value representing cancellation state (`:active`, `:cancelled`, or `:timed-out`), an optional deadline (monotonic nanoTime), and parent-child relationships for propagation. Deadline vs timeout: A timeout is a duration ("at most 2 seconds"); a deadline is an absolute monotonic bound ("stop at nanoTime T"). This library accepts either via [`with-timeout`](#with-timeout) and [`with-deadline`](#with-deadline), but internally tracks a single deadline per lease. Deadlines use `System/nanoTime` for monotonic timing, immune to wall-clock changes (NTP adjustments, manual clock changes, VM suspend/resume). ## Honoring a Lease When your function receives a lease as an argument, you are accepting responsibility to respect it. The caller trusts that if they cancel the lease, your function will notice and stop work promptly and correctly. At a minimum, check the lease before starting expensive operations. Call [`active?!`](#active?!) at the top of your function and at natural checkpoints: the start of each loop iteration, before issuing a network request, or after returning from a potentially slow sub-call. If the lease has been cancelled, [`active?!`](#active?!) throws an exception containing the cancellation cause, which unwinds the stack cleanly. For non-throwing checks, use [`active?`](#active?) and return early or break out of loops when it returns false. The choice between throwing and returning depends on your error-handling style, but the principle is the same: stop doing new work once the lease is no longer active. When calling other lease-aware functions, pass the lease through so they inherit the same cancellation boundary. If you need to spawn concurrent work, derive child leases with [`with-cancel`](#with-cancel) or [`with-timeout`](#with-timeout) and cancel them in a finally block to ensure cleanup. The goal is prompt, graceful termination without leaking resources. A well-behaved function notices cancellation within a reasonable window (tens to hundreds of milliseconds for most operations) and exits without leaving resources dangling or work half-done. ## Parent-Child Relationships Derived leases form a tree. When a parent cancels, all descendants cancel automatically. Children never cancel parents. Child deadlines adopt the earliest of the parent deadline and any explicitly provided deadline. ## Thread Safety Leases are safe for concurrent use. Multiple threads may read state, register listeners, or attempt cancellation. Only the first cancellation wins; subsequent calls are no-ops. ## Usage ```clojure (require '[ol.clave.lease :as l]) ;; Create a root lease and derive a child with timeout (let [[lease cancel] (l/with-timeout (l/background) 5000)] (try (do-work lease) (finally (cancel)))) ;; Check cancellation state (when (l/active? lease) (continue-work)) ;; Block until cancelled (deref (l/done-signal lease)) ``` See also: [`with-cancel`](#with-cancel), [`with-timeout`](#with-timeout), [`with-deadline`](#with-deadline), [`active?!`](#active?!) ## ILease Protocol for cooperative cancellation and deadline tracking. All methods are non-blocking and safe for concurrent use from multiple threads. The [`done-signal`](#done-signal) method returns a derefable; blocking occurs only when dereferencing that signal. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L125-L162) ### deadline ```clojure (deadline lease) ``` Returns the deadline as a monotonic nanoTime (Long), or `nil` if no deadline. The value is from `System/nanoTime` and is only meaningful for comparison with other nanoTime values. Use <> to get a human-readable `Duration` until expiry. --- ### done-signal ```clojure (done-signal lease) ``` Returns a read-only derefable that yields `true` when `lease` ends. Use `deref` with a timeout to wait for cancellation, or `clojure.core/realized?` for a non-blocking check. [source,clojure] ``` ;; Block with timeout (deref (done-signal lease) 1000 :still-active) ;; Non-blocking check (realized? (done-signal lease)) ``` --- ### cause ```clojure (cause lease) ``` Returns the `Throwable` that caused cancellation, or `nil` if `lease` is active. The cause contains `:type` in its `ex-data`: - `:lease/cancelled` for explicit cancellation - `:lease/deadline-exceeded` for timeout --- ### active? ```clojure (active? lease) ``` Returns `true` when `lease` has not been cancelled or timed out. --- ## background ```clojure (background) ``` Creates a root lease with no deadline or parent. The background lease is never cancelled on its own; it serves as the ancestor for all derived leases in an operation tree. Use [`with-cancel`](#with-cancel), [`with-timeout`](#with-timeout), or [`with-deadline`](#with-deadline) to derive child leases with cancellation or deadline constraints. ```clojure (let [root (background) [child cancel] (with-timeout root 5000)] (try (do-work child) (finally (cancel)))) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L335-L352) --- ## with-cancel ```clojure (with-cancel parent) ``` Derives a cancellable child lease from `parent`. Returns `[child cancel-fn]` where: - `child` is the derived lease, inheriting `parent’s deadline - `cancel-fn` cancels `child` and all its descendants The cancel function accepts an optional cause argument. Without arguments, it uses a generic `:lease/cancelled` exception. Calling cancel multiple times is safe; only the first call takes effect. When `parent` cancels, `child` cancels automatically with the same cause. ```clojure (let [[lease cancel] (with-cancel parent)] (try (do-work lease) (finally (cancel)))) ;; Cancel with custom cause (cancel (ex-info "user abort" {:reason :user-request})) ``` See also: [`with-timeout`](#with-timeout), [`with-deadline`](#with-deadline), [`background`](#background) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L354-L388) --- ## with-deadline ```clojure (with-deadline parent dl) ``` Derives a child lease from `parent` with an absolute monotonic deadline. Returns `[child cancel-fn]` where `child` will automatically cancel with `:lease/deadline-exceeded` when the deadline passes. `dl` is a monotonic nanoTime value from `System/nanoTime`. The effective deadline is the earlier of `parent’s deadline and `dl`. If `dl` has already passed, `child` is cancelled immediately. Most callers should prefer [`with-timeout`](#with-timeout) which accepts human-friendly Duration or milliseconds. ```clojure ;; 30 second deadline using nanoTime (let [dl (+ (System/nanoTime) (* 30 1000000000)) [lease cancel] (l/with-deadline parent dl)] (try (do-work lease) (finally (cancel)))) ``` See also: [`with-timeout`](#with-timeout), [`with-cancel`](#with-cancel), [`deadline`](#deadline) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L390-L424) --- ## with-timeout ```clojure (with-timeout parent timeout) ``` Derives a child lease from `parent` with a relative timeout. Returns `[child cancel-fn]` where `child` will automatically cancel with `:lease/deadline-exceeded` after `timeout` elapses. `timeout` may be: - `java.time.Duration` for precise control - Long/integer for milliseconds The effective deadline is computed as `(now + timeout)` and combined with the parent deadline using earliest-wins semantics. A zero or negative timeout cancels the child immediately. ```clojure ;; 5 second timeout (let [[lease cancel] (with-timeout parent 5000)] (try (do-work lease) (finally (cancel)))) ;; Using Duration (with-timeout parent (Duration/ofSeconds 30)) ``` See also: [`with-deadline`](#with-deadline), [`with-cancel`](#with-cancel) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L426-L454) --- ## active?! ```clojure (active?! lease) ``` Returns `lease` if active, otherwise throws the cancellation cause. Use this for explicit cancellation checks that should fail fast. The thrown exception is the same value returned by [`cause`](#cause), containing `:type` of either `:lease/cancelled` or `:lease/deadline-exceeded`. ```clojure ;; Check and continue (active?! lease) (do-next-step) ;; In a loop (loop [] (active?! lease) (when (more-work?) (process-item) (recur))) ``` See also: [`active?`](#active?), [`cause`](#cause) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L456-L480) --- ## remaining ```clojure (remaining lease) ``` Returns the time remaining until `lease` expires as a `java.time.Duration`. Returns `nil` if the lease has no deadline. Returns `Duration/ZERO` if the deadline has already passed. ```clojure (when-let [dur (l/remaining lease)] (println "Time left:" (.toMillis dur) "ms")) ``` See also: [`deadline`](#deadline), [`active?`](#active?) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L482-L499) --- ## sleep ```clojure (sleep lease ms) ``` Cooperatively wait for `ms` milliseconds or until `lease` ends. Returns `:slept` if the full duration elapsed, or `:lease-ended` if the lease was cancelled or timed out during the wait. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L501-L512) ## ol.clave.specs # ol.clave.specs ## new-nonce-url ```clojure (new-nonce-url session) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L91-L92) --- ## new-account-url ```clojure (new-account-url session) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L94-L95) --- ## new-order-url ```clojure (new-order-url session) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L97-L98) --- ## key-change-url ```clojure (key-change-url session) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L100-L101) --- ## new-authz-url ```clojure (new-authz-url session) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L103-L104) --- ## identifier? ```clojure (identifier? value) ``` Return true when `value` conforms to an ACME identifier map. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L139-L142) --- ## order-url ```clojure (order-url order) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L164-L165) --- ## certificate-url ```clojure (certificate-url order) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L167-L168) ## ol.clave.storage.file # ol.clave.storage.file Filesystem-based storage implementation. Provides a [`FileStorage`](#filestorage) record that implements [`ol.clave.storage/Storage`](api/ol-clave-storage.adoc#Storage) and [`ol.clave.storage/TryLocker`](api/ol-clave-storage.adoc#TryLocker) using the local filesystem. The presence of a lock file for a given name indicates a lock is held. ## Key-to-Path Mapping Storage keys map directly to filesystem paths relative to a root directory. Key normalization converts backslashes to forward slashes and strips leading and trailing slashes. Path traversal attempts (keys containing `..` that escape the root) throw `IllegalArgumentException`. ## Atomic Writes All writes use atomic move semantics: data is written to a temporary file and renamed into place. This ensures readers never see partial writes. On filesystems that do not support atomic moves, a best-effort rename is used. ## Locking Advisory locking is implemented with lock files in a `locks/` subdirectory. Locks are created atomically by relying on the filesystem to enforce exclusive file creation. Processes that terminate abnormally will not have a chance to clean up their lock files. To handle this, while a lock is actively held, a background virtual thread periodically updates a timestamp in the lock file (every 5 seconds). If another process tries to acquire the lock but fails, it checks whether the timestamp is still fresh. If so, it waits by polling (every 1 second). Otherwise, the stale lock file is deleted, effectively forcing an unlock. While lock acquisition is atomic, unlocking is not perfectly atomic. Filesystems offer atomic file creation but not necessarily atomic deletion. It is theoretically possible for two processes to discover the same stale lock and both attempt to delete it. If one process deletes the lock file and creates a new one before the other calls delete, the new lock may be deleted by mistake. This means mutual exclusion is not guaranteed to be perfectly enforced in the presence of stale locks. However, these cases are rare, and we prefer the simpler solution over alternatives that risk infinite loops. ## Filesystem Considerations This implementation is designed for local filesystems and relies on specific filesystem semantics: * Exclusive file creation via `O_CREAT | O_EXCL`. Lock acquisition depends on the filesystem atomically failing when creating a file that already exists. * Durable writes via `fsync`. Lock file timestamps must survive crashes to enable stale lock detection. Network filesystems (NFS, CIFS/SMB, AWS EFS) may not reliably support these semantics. In particular, some network filesystems do not honor `O_EXCL` across nodes or do not guarantee that data is persisted after `fsync`, which can leave lock files empty or corrupt after a crash or network interruption. ## Permissions On POSIX systems, files are created with `rw-------` and directories with `rwx------`. On non-POSIX systems (Windows), default permissions apply. ## Usage ```clojure (require '[ol.clave.storage.file :as fs] '[ol.clave.storage :as s]) ;; Use platform-appropriate directories for certificate storage (def storage (fs/file-storage (fs/data-dir "myapp"))) ;; Or specify a custom path (def storage (fs/file-storage "/var/lib/myapp")) ;; Store and retrieve data (s/store-string! storage nil "certs/example.com/cert.pem" cert) (s/load-string storage nil "certs/example.com/cert.pem") ``` ## Related Namespaces * [`ol.clave.storage`](api/ol-clave-storage.adoc) - Storage protocol and utilities * [`ol.clave.lease`](api/ol-clave-lease.adoc) - Cooperative cancellation ## ->LockMeta ```clojure (->LockMeta created-ms updated-ms) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225) --- ## LockMeta [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225) --- ## map->LockMeta ```clojure (map->LockMeta m) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225) --- ## ->FileStorage ```clojure (->FileStorage root) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412) --- ## FileStorage [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412) --- ## map->FileStorage ```clojure (map->FileStorage m) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412) --- ## home-dir ```clojure (home-dir) ``` Returns the user’s home directory. Uses the `user.home` system property, which the JVM resolves appropriately for each platform. Returns `nil` if the home directory cannot be determined. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L442-L449) --- ## state-dir ```clojure (state-dir) (state-dir app-name) ``` Returns the directory for persistent application state. With no arguments, returns the base directory (caller appends app name). With `app-name`, appends it as a subdirectory (unless systemd provides one). Checks environment variables in order: 1. `$STATE_DIRECTORY` - set by systemd for system units (already app-specific) 2. `$XDG_STATE_HOME` - XDG base directory spec / systemd user units 3. Platform default When systemd sets `$STATE_DIRECTORY`, it may contain multiple colon-separated paths if the unit configures multiple directories; this function returns the first path. Platform defaults when no environment variable is set: | platform | path | |----------|-------------------------------------| | Linux | `$HOME/.local/state` | | macOS | `$HOME/Library/Application Support` | | Windows | `%LOCALAPPDATA%` | Returns `nil` if a suitable directory cannot be determined. ```clojure (state-dir) ; => "/home/user/.local/state" (state-dir "myapp") ; => "/home/user/.local/state/myapp" ;; Under systemd system unit with StateDirectory=myapp: (state-dir) ; => "/var/lib/myapp" (state-dir "myapp") ; => "/var/lib/myapp" (no double append) (file-storage (state-dir "myapp")) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L451-L500) --- ## config-dir ```clojure (config-dir) (config-dir app-name) ``` Returns the directory for application configuration files. With no arguments, returns the base directory (caller appends app name). With `app-name`, appends it as a subdirectory (unless systemd provides one). Checks environment variables in order: 1. `$CONFIGURATION_DIRECTORY` - set by systemd for system units (already app-specific) 2. `$XDG_CONFIG_HOME` - XDG base directory spec / systemd user units 3. Platform default When systemd sets `$CONFIGURATION_DIRECTORY`, it may contain multiple colon-separated paths if the unit configures multiple directories; this function returns the first path. Platform defaults when no environment variable is set: | platform | path | |----------|-----------------------------| | Linux | `$HOME/.config` | | macOS | `$HOME/Library/Preferences` | | Windows | `%APPDATA%` | Returns `nil` if a suitable directory cannot be determined. ```clojure (config-dir) ; => "/home/user/.config" (config-dir "myapp") ; => "/home/user/.config/myapp" ;; Under systemd system unit with ConfigurationDirectory=myapp: (config-dir) ; => "/etc/myapp" (config-dir "myapp") ; => "/etc/myapp" (no double append) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L502-L549) --- ## cache-dir ```clojure (cache-dir) (cache-dir app-name) ``` Returns the directory for application cache files. With no arguments, returns the base directory (caller appends app name). With `app-name`, appends it as a subdirectory (unless systemd provides one). Checks environment variables in order: 1. `$CACHE_DIRECTORY` - set by systemd for system units (already app-specific) 2. `$XDG_CACHE_HOME` - XDG base directory spec / systemd user units 3. Platform default When systemd sets `$CACHE_DIRECTORY`, it may contain multiple colon-separated paths if the unit configures multiple directories; this function returns the first path. Platform defaults when no environment variable is set: | platform | path | |----------|------------------------| | Linux | `$HOME/.cache` | | macOS | `$HOME/Library/Caches` | | Windows | `%LOCALAPPDATA%` | Returns `nil` if a suitable directory cannot be determined. ```clojure (cache-dir) ; => "/home/user/.cache" (cache-dir "myapp") ; => "/home/user/.cache/myapp" ;; Under systemd system unit with CacheDirectory=myapp: (cache-dir) ; => "/var/cache/myapp" (cache-dir "myapp") ; => "/var/cache/myapp" (no double append) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L551-L598) --- ## data-dir ```clojure (data-dir) (data-dir app-name) ``` Returns the directory for persistent application data. This is the recommended directory for ACME certificates and keys because they are valuable cryptographic material with rate limits on reissuance. With no arguments, returns the base directory (caller appends app name). With `app-name`, appends it as a subdirectory (unless systemd provides one). Checks environment variables in order: 1. `$STATE_DIRECTORY` - set by systemd (`StateDirectory=` maps to `/var/lib/` which is semantically correct) 2. `$XDG_DATA_HOME` - XDG base directory spec 3. Platform default When systemd sets `$STATE_DIRECTORY`, it may contain multiple colon-separated paths if the unit configures multiple directories; this function returns the first path. Platform defaults when no environment variable is set: | platform | path | |----------|-------------------------------------| | Linux | `$HOME/.local/share` | | macOS | `$HOME/Library/Application Support` | | Windows | `%LOCALAPPDATA%` | Returns `nil` if a suitable directory cannot be determined. ```clojure (data-dir) ; => "/home/user/.local/share" (data-dir "myapp") ; => "/home/user/.local/share/myapp" ;; Under systemd system unit with StateDirectory=myapp: (data-dir) ; => "/var/lib/myapp" (data-dir "myapp") ; => "/var/lib/myapp" (no double append) (file-storage (data-dir "myapp")) ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L600-L652) --- ## file-storage ```clojure (file-storage) (file-storage root) ``` Creates a [`FileStorage`](#filestorage) rooted at `root. `root` may be a string or `java.nio.file.Path`. The directory is created if it does not exist. Default: "ol.clave" subdir inside [`data-dir`](#data-dir) Returns a record implementing [`ol.clave.storage/Storage`](api/ol-clave-storage.adoc#Storage) and [`ol.clave.storage/TryLocker`](api/ol-clave-storage.adoc#TryLocker). [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L654-L671) ## ol.clave.storage # ol.clave.storage Key-value storage with path semantics for ACME certificate data. The storage API provides a unified abstraction for persisting certificates, account keys, and lock state. Keys use forward slashes with no leading or trailing slashes. A key with an associated value is a "file"; a key with no value that serves as a prefix for other keys is a "directory". Keys passed to [`load`](#load) and [`store!`](#store!) always have file semantics; directories are implicit from the path structure. ## Key Format Keys follow path semantics similar to filesystem paths. A "prefix" is defined on a component basis: `"a"` is a prefix of `"a/b"` but not of `"ab/c"`. * Valid: `"acme/certs/example.com"` * Valid: `"locks/renewal.lock"` * Invalid: `"/leading/slash"` (leading slash removed by normalization) * Invalid: `"trailing/slash/"` (trailing slash removed by normalization) Use [`storage-key`](#storage-key) to safely join key components and [`safe-key`](#safe-key) to sanitize user-provided values for use as key segments. ## Lease Integration All operations accept a `lease` from [`ol.clave.lease`](api/ol-clave-lease.adoc) for cooperative cancellation and deadline propagation. Pass `nil` to skip cancellation checks for short, non-interruptible operations. Implementations must honor lease cancellation and throw promptly when the lease is no longer active. ## Locking The [`lock!`](#lock!) and [`unlock!`](#unlock!) methods provide advisory locking to coordinate expensive operations across processes. You do not need to wrap every storage call in a lock; [`store!`](#store!), [`load`](#load), and other basic operations are already thread-safe. Use locking for higher-level operations that need synchronization, such as certificate renewal where only one process should attempt issuance at a time. When the lock guards an idempotent operation, always verify that the work still needs to be done after acquiring the lock. Another process may have completed the task while you were waiting. Use [`with-lock`](#with-lock) for safe lock acquisition with guaranteed release. Optional protocols [`TryLocker`](#trylocker) and [`LockLeaseRenewer`](#lockleaserenewer) extend locking with non-blocking acquisition and lease renewal for long-running operations. ## Thread Safety Implementations must be safe for concurrent use from multiple threads. Methods should block until their operation is complete: [`load`](#load) should always return the value from the last call to [`store!`](#store!) for a given key, and concurrent calls to [`store!`](#store!) must not corrupt data. Callers will typically invoke storage methods from virtual threads, so blocking I/O is expected and appropriate. Implementors do not need to spawn threads or perform asynchronous operations internally. This is not a streaming API and is not suitable for very large files. ## Usage ```clojure (require '[ol.clave.storage :as s] '[ol.clave.storage.file :as fs]) (let [storage (fs/file-storage "/var/acme")] ;; Store and retrieve data (s/store-string! storage nil "certs/example.com" cert-pem) (s/load-string storage nil "certs/example.com") ;; List with prefix (s/list storage nil "certs" false) ; => ["certs/example.com" ...] ;; Coordinated access for expensive operations (s/with-lock storage lease "certs/example.com" (fn [] ;; Check if work still needed after acquiring lock (when (certificate-needs-renewal? storage "example.com") (renew-certificate!))))) ``` ## Related Namespaces * [`ol.clave.storage.file`](api/ol-clave-storage-file.adoc) - Filesystem storage implementation * [`ol.clave.lease`](api/ol-clave-lease.adoc) - Cooperative cancellation This interface was inspired by certmagic’s interface in Go. ## ->KeyInfo ```clojure (->KeyInfo key modified size terminal?) ``` Metadata about a storage key. The `key` and `terminal?` fields are required. The `modified` and `size` fields are optional if the storage implementation cannot provide them, but setting them makes certain operations more consistent and predictable. Fields: - `key` - the storage key as a string - `modified` - last modification time as `java.time.Instant`, or `nil` - `size` - size in bytes (Long), or `nil` - `terminal?` - `false` for directories (keys that act as prefix for other keys), `true` for files (keys with associated values) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L104-L119) --- ## KeyInfo Metadata about a storage key. The `key` and `terminal?` fields are required. The `modified` and `size` fields are optional if the storage implementation cannot provide them, but setting them makes certain operations more consistent and predictable. Fields: - `key` - the storage key as a string - `modified` - last modification time as `java.time.Instant`, or `nil` - `size` - size in bytes (Long), or `nil` - `terminal?` - `false` for directories (keys that act as prefix for other keys), `true` for files (keys with associated values) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L104-L119) --- ## map->KeyInfo ```clojure (map->KeyInfo m) ``` Metadata about a storage key. The `key` and `terminal?` fields are required. The `modified` and `size` fields are optional if the storage implementation cannot provide them, but setting them makes certain operations more consistent and predictable. Fields: - `key` - the storage key as a string - `modified` - last modification time as `java.time.Instant`, or `nil` - `size` - size in bytes (Long), or `nil` - `terminal?` - `false` for directories (keys that act as prefix for other keys), `true` for files (keys with associated values) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L104-L119) --- ## Storage Key-value storage with path semantics. All methods accept a `lease` from [`ol.clave.lease`](api/ol-clave-lease.adoc) for cooperative cancellation; pass `nil` to skip cancellation checks. Keys are normalized: backslashes become forward slashes, leading and trailing slashes are stripped. Implementations must be safe for concurrent use. Methods should block until their operation is complete. [`load`](#load), [`delete!`](#delete!), [`list`](#list), and [`stat`](#stat) should throw `java.nio.file.NoSuchFileException` when the key does not exist. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L121-L201) ### store! ```clojure (store! this lease key value-bytes) ``` Stores `value-bytes` at `key`, creating parent directories as needed. Overwrites any existing value at this key. Concurrent calls to <> must not corrupt data. Returns `nil`. --- ### load ```clojure (load this lease key) ``` Returns the bytes stored at `key`. Always returns the value from the last successful <> for this key. Throws `java.nio.file.NoSuchFileException` if `key` does not exist. --- ### delete! ```clojure (delete! this lease key) ``` Deletes `key` and any keys prefixed by it (recursive delete). If `key` is a directory (prefix of other keys), all keys with that prefix are deleted. Returns `nil`. Throws `java.nio.file.NoSuchFileException` if `key` does not exist. --- ### exists? ```clojure (exists? this lease key) ``` Returns `true` if `key` exists as a file or directory, `false` otherwise. --- ### list ```clojure (list this lease prefix recursive?) ``` Lists keys under `prefix`. When `recursive?` is `false`, returns only keys prefixed exactly by `prefix` (direct children). When `recursive?` is `true`, non-terminal keys are enumerated and all descendants are returned. Returns a vector of key strings. Throws `java.nio.file.NoSuchFileException` if `prefix` does not exist. --- ### stat ```clojure (stat this lease key) ``` Returns a [`KeyInfo`](#keyinfo) record describing `key`. Throws `java.nio.file.NoSuchFileException` if `key` does not exist. --- ### lock! ```clojure (lock! this lease name) ``` Acquires an advisory lock for `name`, blocking until available. Only one lock for a given name can exist at a time. A call to <> for a name that is already locked blocks until the lock is released or becomes stale. Lock names are sanitized via <>. Implementations must honor lease cancellation. Returns `nil` when the lock is acquired. --- ### unlock! ```clojure (unlock! this lease name) ``` Releases the advisory lock for `name`. This method must only be called after a successful call to <>, and only after the critical section is finished, even if it threw an exception. <> cleans up any resources allocated during <>. Returns `nil`. Throws only if the lock could not be released. --- ## TryLocker Optional non-blocking lock acquisition. Implementations that support non-blocking lock attempts should extend this protocol in addition to [`Storage`](#storage). _protocol_ [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L203-L218) ### try-lock! ```clojure (try-lock! this lease name) ``` Attempts to acquire the lock for `name` without blocking. Returns `true` if the lock was acquired, `false` if it could not be obtained (e.g., already held by another process). Implementations must honor lease cancellation. After a successful <>, you must call <> when the critical section is finished, even if it threw an exception. --- ## LockLeaseRenewer Optional lease renewal for long-running locks. When a lock is held for an extended period, the holder should periodically renew the lock lease to prevent it from being considered stale and forcibly released by another process. This is useful for long-running operations that need synchronization. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L220-L237) ### renew-lock-lease! ```clojure (renew-lock-lease! this lease lock-key lease-duration) ``` Extends the lease on `lock-key` by `lease-duration`. This prevents another process from acquiring the lock by treating it as stale. `lease-duration` is a `java.time.Duration`. Returns `nil`. Throws if the lock is not currently held or could not be renewed. --- ## safe-key ```clojure (safe-key s) ``` Returns a filesystem-safe key component from `s`. Transforms: - Converts to lowercase - Replaces spaces with underscores - Replaces `+` with `_plus_`, `*` with `wildcard_` - Replaces `:` with `-` - Removes `..` sequences - Strips characters not in `[a-zA-Z0-9_@.-]` Use this when incorporating user input or domain names into storage keys. ```clojure (safe-key "Example.COM") ; => "example.com" (safe-key "*.example.com") ; => "wildcard_example.com" ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L241-L267) --- ## storage-key ```clojure (storage-key & parts) ``` Joins key components with `/`, ignoring `nil` and blank parts. ```clojure (storage-key "acme" "certs" "example.com") ;; => "acme/certs/example.com" (storage-key "base" nil "" "file") ;; => "base/file" ``` [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L269-L284) --- ## store-string! ```clojure (store-string! storage lease key s) ``` Stores UTF-8 encoded `s` at `key`. Convenience wrapper around [`store!`](#store!) for text content. See [`load-string`](#load-string) for retrieval. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L286-L292) --- ## load-string ```clojure (load-string storage lease key) ``` Loads UTF-8 text from `key`. Convenience wrapper around [`load`](#load) for text content. Throws `java.nio.file.NoSuchFileException` if `key` does not exist. See [`store-string!`](#store-string!) for storage. [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L294-L301) --- ## with-lock ```clojure (with-lock storage lease lock-name f) ``` Executes `f` while holding the lock `lock-name`, releasing on exit. Acquires the lock via [`lock!`](#lock!), runs `f` (a zero-argument function), and releases via [`unlock!`](#unlock!) in a finally block regardless of success or failure. If the lock guards an idempotent operation, `f` should verify that the work still needs to be done. Another process may have completed the task while you were waiting to acquire the lock. ```clojure (with-lock storage lease "certs/example.com" (fn [] ;; Check if work still needed after acquiring lock (when (certificate-needs-renewal? domain) (renew-certificate!) (write-certificate!)))) ``` See also: [`lock!`](#lock!), [`unlock!`](#unlock!) [source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L303-L329) ## Changelog # Changelog All notable changes to this project will be documented in this file. This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning). ## UNRELEASED ## `v0.0.1` (2026-XX-XX) We haven’t quite got here yet.. Please report any problems and let me know if anything is unclear or inconvenient. Thank you. ## ol.clave # ol.clave > Automatic HTTPS certificate management and renewal via ACME, implemented in pure Clojure with minimal dependencies ![doc](https://img.shields.io/badge/doc-outskirtslabs-orange.svg) ![status: experimental](https://img.shields.io/badge/status-experimental-red.svg) ![alt=built with garnix](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fgarnix.io%2Fapi%2Fbadges%2Foutskirtslabs%2Fclave) [![Clojars Project](https://clojars.org/com.outskirtslabs/clave)(https://img.shields.io/clojars/v/com.outskirtslabs/clave.svg)] `ol.clave` is an [ACME client](https://datatracker.ietf.org/doc/html/rfc8555) for Clojure. It gives you a lower-level API for implementing the ACME protocol itself when you need that much control. If you want to work directly with accounts, orders, authorizations, challenges, and certificate issuance, you can do that. And if you do not want to live at RFC altitude all day, it also gives you a higher-level API for provisioning certificates from application code, managing them over time, and keeping them up to date. That is the part most people actually want. Ask for a cert. Renew the cert. Repeat. `ol.clave` also includes adapters for wiring ACME into Jetty-based applications, plus helpers for HTTP-01 and TLS-ALPN-01 validation. For DNS validation, it gives you the hooks you need, but it does not ship DNS provider integrations by default. DNS provider integrations are future work. (If you are interested in that, [get in touch](https://casey.link/about).) One of the design goals for `ol.clave` was as few dependencies as possible and avoid dragging in a bunch of Java ecosystem baggage. At runtime there are exactly two hard dependencies. The first is [`babashka/json`](https://github.com/babashka/json), which is a BYO JSON-library-library. On the JVM it will use a JSON provider from your classpath. In many applications that will just work because you already have one. If you care which provider gets picked, or want to force one, go read that project’s docs. The second runtime dep is [Peter Taoussanis’s Trove](https://github.com/taoensso/trove), which gives `ol.clave` a very lightweight way to emit signals and logging without forcing a backend on you. Same story there: wire it into whatever telemetry or logging stack you already use after reading Peter’s docs. The no-dependencies choice has consequences. I did not want to pull in Bouncy Castle as a runtime dependency. It is large, it is heavy, and it is the kind of library that likes to start version fights when two things on your classpath want different releases. But ACME certificate provisioning still needs some plumbing that the JDK does not hand you directly (despite being distributed as part of the keytool util in every JDK package), especially around DER de/encoding, CSR generation, and a few X.509-adjacent details. To be clear, `ol.clave` is not implementing any cryptography itself. The actual crypto primitives (RSA, ECC, etc) still come from the JVM. What `ol.clave` implements in pure Clojure is the narrow slice of encoding, decoding, and certificate-request machinery needed to make ACME work without the heavyweight runtime dependencies. That problem space is small enough to specify, small enough to test thoroughly and it isn’t a general-purpose PKI toolkit. Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**. ## Installation ```clojure ;; deps.edn {:deps {com.outskirtslabs/clave {:mvn/version "0.0.0"}}} ;; Leiningen [com.outskirtslabs/clave "0.0.0"] ``` ## Quick Start Start with one of the runnable examples in this repository: * [`examples/certificate.clj`](https://github.com/outskirtslabs/clave/blob/main/examples/certificate.clj) for the higher-level certificate acquisition flow. * [`examples/acme.clj`](https://github.com/outskirtslabs/clave/blob/main/examples/acme.clj) for a lower-level step-by-step ACME transaction. * [`examples/ring_jetty.clj`](https://github.com/outskirtslabs/clave/blob/main/examples/ring_jetty.clj) for auto-renewing HTTPS with `ring-jetty-adapter`. These examples use [Pebble](https://github.com/letsencrypt/pebble) for local ACME testing. ## Documentation * [Docs](https://docs.outskirtslabs.com/ol.clave/next/) * [API Reference](https://docs.outskirtslabs.com/ol.clave/next/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/clave/issues) ## Recommended Reading * [RFC 8555: Automatic Certificate Management Environment (ACME)](https://datatracker.ietf.org/doc/html/rfc8555) * [RFC 9773: ACME Renewal Information (ARI) Extension](https://datatracker.ietf.org/doc/html/rfc9773) * [Best Practices for ACME Client Operations](https://github.com/https-dev/docs/blob/master/acme-ops.md) ## Security See [Security](security.adoc) for security reporting and policy links. ## License Copyright (C) 2025-2026 Casey Link Distributed under the [EUPL-1.2](https://spdx.org/licenses/EUPL-1.2.html). Some files included in this project are from third-party sources and retain their original licenses as indicated in [NOTICE](./NOTICE). Special thanks to [Michiel Borkent (@borkdude)](https://github.com/borkdude/) for the use of [babashka/http-client](https://github.com/babashka/http-client) and [babashka/json](https://github.com/babashka/json). ## Security # Security Please report vulnerabilities through [GitHub Security Advisories](https://github.com/outskirtslabs/clave/security/advisories). For general policy and support expectations, see [Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).