HTTP Message Signatures by RFC 9421: sign a request or a response, and verify the signatures of one.
A message is a request or response map like those of wary-fetch. A request has :method, :url, :headers and :body, and a response has :status, :headers and :body. Header names can be in any case. A header's value is text or a vector of texts, and the body is text, bytes or nil. You can hide any of these texts, and a key's PEM or secret, with wary-fetch's secret/hide.
A key is a map with a :keyid, an :alg (one of algorithms) and the key material, one of these:
Without an :alg, the algorithm follows from the kind of key, e.g. rsa-v1_5-sha256 for RSA as in Mastodon, and rsa-pss-sha512 for a key of RSASSA-PSS, which can't have another :alg. For verifying, a key map can also have the :public-pem of its private key, as generate-key returns it.
The functions that sign and verify return values on the JVM and promises in ClojureScript, since WebCrypto only has promises. Chain them with wary-fetch's async/then to write code for both.
HTTP Message Signatures by RFC 9421: sign a request or a response, and verify the signatures of one. A message is a request or response map like those of wary-fetch. A request has :method, :url, :headers and :body, and a response has :status, :headers and :body. Header names can be in any case. A header's value is text or a vector of texts, and the body is text, bytes or nil. You can hide any of these texts, and a key's PEM or secret, with wary-fetch's secret/hide. A key is a map with a :keyid, an :alg (one of algorithms) and the key material, one of these: - :pem, a key in PEM: SPKI or PKCS#8, or PKCS#1 and SEC1 - :jwk, a JWK as a map - :secret, the bytes or the text of a shared secret for hmac-sha256 - :key, a webcrypto-clj key, e.g. a CryptoKey that a page keeps unextractable Without an :alg, the algorithm follows from the kind of key, e.g. rsa-v1_5-sha256 for RSA as in Mastodon, and rsa-pss-sha512 for a key of RSASSA-PSS, which can't have another :alg. For verifying, a key map can also have the :public-pem of its private key, as generate-key returns it. The functions that sign and verify return values on the JVM and promises in ClojureScript, since WebCrypto only has promises. Chain them with wary-fetch's async/then to write code for both.
(accept-signature requests)The value of an Accept-Signature field that asks for the signatures in
requests, by label. Each is a map of the :components to cover and the
:params to include, e.g.
{"sig1" {:components ["@method"]
:params {:keyid "k" :created true}}}
A parameter of true asks the signer to choose its value.
The value of an Accept-Signature field that asks for the signatures in
`requests`, by label. Each is a map of the :components to cover and the
:params to include, e.g.
{"sig1" {:components ["@method"]
:params {:keyid "k" :created true}}}
A parameter of true asks the signer to choose its value.The names of the algorithms in RFC 9421's registry (section 6.2.2). The library signs and verifies with all of them, though Ed25519 needs Java 15 or later on the JVM.
The names of the algorithms in RFC 9421's registry (section 6.2.2). The library signs and verifies with all of them, though Ed25519 needs Java 15 or later on the JVM.
(component message id)(component message id opts)The value of the component id of the message, by the :request and
:field-types of opts, as sign takes them.
(component {:url "https://Example.com:443/a?b"} "@authority")
;; => "example.com"
The value of the component `id` of the `message`, by the :request and
:field-types of `opts`, as sign takes them.
(component {:url "https://Example.com:443/a?b"} "@authority")
;; => "example.com"The default of each option of sign and verify that has one:
The default of each option of sign and verify that has one: - :alg?, true: a new signature names its algorithm, which Mastodon ignores but Gancio's beta requires - :max-age-seconds, 3600: how long ago a signature's created can be, an hour as the SWICG report on the fediverse's signatures suggests - :max-skew-seconds, 300: how far ahead of the clock its created can be - :max-signatures, 3: how many of a message's signatures verify tries - :min-rsa-bits, 2048: the smallest RSA key that verify accepts - :algorithms, all of algorithms: the algorithms that verify accepts
(generate-key alg keyid)A new key map of the algorithm alg with the keyid (in ClojureScript
a promise of it). An HMAC key has a :secret, and any other a :pem and
the :public-pem to publish, e.g. in an actor document. The secret and
the private PEM are hidden with wary-fetch's secret/hide, so they print
as #<secret>.
(generate-key "rsa-v1_5-sha256" "https://example.com/actor#main-key")
A new key map of the algorithm `alg` with the `keyid` (in ClojureScript
a promise of it). An HMAC key has a :secret, and any other a :pem and
the :public-pem to publish, e.g. in an actor document. The secret and
the private PEM are hidden with wary-fetch's secret/hide, so they print
as #<secret>.
(generate-key "rsa-v1_5-sha256" "https://example.com/actor#main-key")(sender send key)(sender send key opts)A function like wary-fetch's send! that signs each request with the
key and sends it with send, by the opts below and those of sign.
(def send-signed!
(sender http/send! actor-key))
It tries each of :signers in turn (sign by default): when the server answers 400 or 401, it sends the request again, signed by the next one. For the fediverse, [cavage/sign sign] sends draft-cavage first and RFC 9421 next, as Mastodon does. It sends a request to any URL but an http or https one unsigned, e.g. one to a Nostr relay.
A signature covers the URL, so each hop of a redirect needs its own. The sender adds a :prepare-fn to the request, which wary-fetch calls to sign each hop before sending it. It signs a hop to another origin only when it's a GET or a HEAD, so that a server can't redirect a signed POST to a URL of its choosing. A browser follows its own redirects, so there only the first hop has a signature.
A function like wary-fetch's send! that signs each request with the
`key` and sends it with `send`, by the `opts` below and those of sign.
(def send-signed!
(sender http/send! actor-key))
It tries each of :signers in turn (sign by default): when the server
answers 400 or 401, it sends the request again, signed by the next one.
For the fediverse, [cavage/sign sign] sends draft-cavage first and RFC
9421 next, as Mastodon does. It sends a request to any URL but an http
or https one unsigned, e.g. one to a Nostr relay.
A signature covers the URL, so each hop of a redirect needs its own. The
sender adds a :prepare-fn to the request, which wary-fetch calls to sign
each hop before sending it. It signs a hop to another origin only when
it's a GET or a HEAD, so that a server can't redirect a signed POST to a
URL of its choosing. A browser follows its own redirects, so there only
the first hop has a signature.(sign message key)(sign message key opts)The message signed with the key by the opts below and
default-options: the message with a Signature-Input and a Signature
added (in ClojureScript a promise of it).
Unless :components says otherwise, a request's signature covers both its @method and its @target-uri, and a response's covers its @status. With :request, a response also covers the method and the URL of the request (as @method and @target-uri with the req parameter), so that nobody can pass it off as the answer to another request. Both cover content-digest when the message has a body.
It adds a Content-Digest when it covers one and the message has none. It also rewrites a request's URL as each platform's client sends it, so that @target-uri is the URL on the wire.
The opts are these:
It throws an ex-info with a :type for a message that it can't sign, e.g. ::missing-component for a covered field that the message lacks.
The `message` signed with the `key` by the `opts` below and
default-options: the message with a Signature-Input and a Signature
added (in ClojureScript a promise of it).
Unless :components says otherwise, a request's signature covers both
its @method and its @target-uri, and a response's covers its @status.
With :request, a response also covers the method and the URL of the
request (as @method and @target-uri with the req parameter), so that
nobody can pass it off as the answer to another request. Both cover
content-digest when the message has a body.
It adds a Content-Digest when it covers one and the message has none.
It also rewrites a request's URL as each platform's client sends it, so
that @target-uri is the URL on the wire.
The `opts` are these:
- :components, the covered components in order, each a name (e.g.
"@authority" or "content-type"), or a pair of a name and a map of
its parameters, e.g. ["@query-param" {"name" "Pet"}]
- :label, the signature's label, sig1 by default
- :created, an inst (the time now by default), or false for none
- :expires, an inst
- :nonce and :tag, texts
- :alg?, whether the signature names its algorithm
- :request, the request that a response answers, for components with
req
- :field-types, the type of each structured field (e.g. {"example-dict"
:dictionary}) for components with sf
- :now-fn, a function that returns the time now
It throws an ex-info with a :type for a message that it can't sign, e.g.
::missing-component for a covered field that the message lacks.(signature-base message components params)(signature-base message components params opts)The signature base of the message for a signature that covers the
components with the params, by the opts: the text that the
signature signs.
The components are those that sign takes, and the params a map or
pairs of their names and values, e.g. {:created #inst "…" :keyid "k"}.
The opts are the :request and :field-types that sign takes. It throws
::missing-component or ::malformed when it can't make the base.
The signature base of the `message` for a signature that covers the
`components` with the `params`, by the `opts`: the text that the
signature signs.
The components are those that sign takes, and the `params` a map or
pairs of their names and values, e.g. {:created #inst "…" :keyid "k"}.
The `opts` are the :request and :field-types that sign takes. It throws
::missing-component or ::malformed when it can't make the base.(signatures message)The well-formed signatures of the message in order, without verifying
them, e.g. to read a keyid before verify. Each is a map of its :label,
:components, :params (by keyword, with insts for created and expires)
and its :signature as bytes.
The well-formed signatures of the `message` in order, without verifying them, e.g. to read a keyid before verify. Each is a map of its :label, :components, :params (by keyword, with insts for created and expires) and its :signature as bytes.
(verify message key-fn)(verify message key-fn opts)The result of verifying a signature of the message with the key that
key-fn returns for it, by the opts below and default-options (in
ClojureScript a promise of it).
The key-fn takes the parameters of a signature as a map of its :label,
:keyid, :alg, :created, :expires, :nonce and :tag, and returns a key map
(or nil, or a future or a promise of either). Return nil for a key that
you don't know or trust.
It tries at most :max-signatures of the message's signatures, in order. For the first that verifies, the result is a map of its :label, :keyid, :alg, :created, :expires, :nonce and :tag, its covered :components, and the :key that verified it. That key has no private key or secret, so that a log can show it.
When none verifies, the result has the :error of the first, its :label and :keyid when it has them, and the :component that the error is about, if any. The error is one of these:
key-fn returns no keyThe opts are those of default-options, and these:
It doesn't check a nonce for replays, since that needs a store: check the :nonce of the result against your own.
The result of verifying a signature of the `message` with the key that `key-fn` returns for it, by the `opts` below and default-options (in ClojureScript a promise of it). The `key-fn` takes the parameters of a signature as a map of its :label, :keyid, :alg, :created, :expires, :nonce and :tag, and returns a key map (or nil, or a future or a promise of either). Return nil for a key that you don't know or trust. It tries at most :max-signatures of the message's signatures, in order. For the first that verifies, the result is a map of its :label, :keyid, :alg, :created, :expires, :nonce and :tag, its covered :components, and the :key that verified it. That key has no private key or secret, so that a log can show it. When none verifies, the result has the :error of the first, its :label and :keyid when it has them, and the :component that the error is about, if any. The error is one of these: - ::unsigned, for a message without a signature that the options pick - ::malformed, for fields or parameters that RFC 9421 doesn't allow - ::not-covered, for a component or a created that the policy needs - ::missing-component, for a covered component that the message lacks, or whose value can't go in a signature base - ::expired and ::not-yet-valid, for a signature outside its time - ::digest-mismatch, for a body that doesn't match its covered digest - ::unknown-key, when `key-fn` returns no key - ::invalid-key, for a key that doesn't import or is too small - ::unsupported-algorithm, for an algorithm that the platform lacks - ::wrong-algorithm, for an alg that doesn't match the key or isn't one of the :algorithms - ::invalid-signature, for a signature that doesn't verify The `opts` are those of default-options, and these: - :label, to verify the signature of this label alone - :tag, to verify the signatures with this tag alone - :required-components, the components that a signature has to cover, in place of the default policy: a request covers @method and its whole URL, as @target-uri, or as @authority with @request-target or with both @path and @query. A response covers its @status, and with :request the same of the request, with the req parameter. Both cover content-digest when the message has a body. - :request, the request that a response answers, for components with req - :field-types, as sign takes them - :now-fn, a function that returns the time now It doesn't check a nonce for replays, since that needs a store: check the :nonce of the result against your own.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |