# `X402.HTTPSignature`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/http_signature.ex#L1)

RFC 9421 HTTP Message Signatures: sign and verify requests and responses.

Implements the profile the x402 `http-message-signatures` extension
relies on: the derived components `@method`, `@target-uri`,
`@authority`, `@scheme`, `@request-target`, `@path`, `@query`,
`@query-param;name` and `@status`, plain HTTP fields (with the `req`
flag for request components covered by a response signature, RFC 9421
§2.4), the `created`, `expires`, `nonce`, `alg`, `keyid` and `tag`
parameters, and the `ed25519`, `ecdsa-p256-sha256` and
`rsa-pss-sha512` algorithms (`X402.HTTPSignature.Key`).

## Messages

A message is a plain map:

* `:method` — the request method (`"GET"` or `:get`);
* `:url` — the absolute target URI of the request;
* `:headers` — a list of `{name, value}` pairs (any case, repeated
  names allowed) or a map of name to value or list of values;
* `:status` — the status code, which marks the message as a response;
* `:request` — for a response, the request it answers, used by
  components carrying the `req` flag.

## Signing a request

    {:ok, key} = X402.HTTPSignature.Key.generate("ed25519")

    message = %{
      method: "GET",
      url: "https://api.example.com/premium?limit=10",
      headers: [{"payment-signature", encoded_payload}]
    }

    {:ok, headers} = X402.HTTPSignature.sign(message, key, tag: "web-bot-auth")
    # [{"signature-input", ~S|sig1=("@method" "@authority" "@path" "payment-signature");created=...|},
    #  {"signature", "sig1=:...:"}]

## Signing a response

Response signatures cover `@status`, the x402 header and, bound to the
request through `;req`, the request authority and path — the shape the
extension specification recommends:

    response = %{status: 200, headers: [{"payment-response", encoded}], request: message}
    {:ok, headers} = X402.HTTPSignature.sign(response, key, tag: "x402-response")

## Verifying

`verify/2` takes the message (with its `Signature-Input` and
`Signature` headers) and resolves the key from the `keyid` parameter:

    X402.HTTPSignature.verify(message,
      keys: fn keyid -> MyApp.Keys.fetch(keyid) end,
      required_components: ["@method", "@authority", "@path"],
      max_age: 300
    )
    # {:ok, %{label: "sig1", key: key, params: %{"created" => ..., "keyid" => ...}, components: [...]}}

Verification trusts nothing the signature says about itself: the key
comes from `:keys` (the `keyid` parameter is only a lookup hint and
must match the key's `kid`), the algorithm is the key's and must be in
`:algorithms` (an `alg` parameter that disagrees is rejected), and the
caller states which components and parameters must be covered. A
message carrying two candidate signatures, or a `Signature-Input`
with a repeated label or parameter, is rejected as ambiguous rather
than picking one.

## Profile

This is a bounded profile of RFC 9421, not a complete implementation.
Out of scope, and rejected when encountered:

* the `hmac-sha256`, `rsa-v1_5-sha256` and `ecdsa-p384-sha384`
  algorithms, and the `sf`, `key`, `bs` and `tr` component parameters
  (RFC 9421 §2.1.1–§2.1.4);
* `Accept-Signature` negotiation (§5) and `Content-Digest` (RFC 9530),
  which callers cover as a plain field when they compute it;
* key discovery: `verify/2` never fetches a directory, the caller's
  `:keys` does the lookup.

# `base_error`

```elixir
@type base_error() ::
  {:missing_component, String.t()}
  | {:duplicate_component, String.t()}
  | {:invalid_component, String.t()}
  | {:unknown_component, String.t()}
  | {:unsupported_component_parameter, String.t()}
  | {:invalid_field_value, String.t()}
  | :non_ascii
  | :invalid_structured_field
```

# `component`

```elixir
@type component() :: {String.t(), [{String.t(), true | String.t()}]}
```

A component identifier: the lowercased name and its ordered parameters.

# `component_spec`

```elixir
@type component_spec() :: String.t() | {String.t(), keyword()}
```

A component as given to `sign/3`: a name, or a name with parameters
such as `{"@authority", req: true}` or `{"@query-param", name: "Pet"}`.

# `message`

```elixir
@type message() :: %{
  optional(:method) =&gt; String.t() | atom(),
  optional(:url) =&gt; String.t(),
  optional(:headers) =&gt;
    [{String.t(), String.t()}] | %{optional(String.t()) =&gt; term()},
  optional(:status) =&gt; 100..599,
  optional(:request) =&gt; map()
}
```

A request or response message.

# `params`

```elixir
@type params() :: [{String.t(), X402.HTTPSignature.StructuredField.bare()}]
```

Signature parameters in serialization order.

# `sign_error`

```elixir
@type sign_error() :: base_error() | X402.HTTPSignature.Key.error()
```

# `verified`

```elixir
@type verified() :: %{
  label: String.t(),
  key: X402.HTTPSignature.Key.t(),
  params: %{optional(String.t()) =&gt; X402.HTTPSignature.StructuredField.bare()},
  components: [component()]
}
```

A successful verification.

# `verify_error`

```elixir
@type verify_error() ::
  base_error()
  | :missing_signature
  | :malformed_signature_input
  | :malformed_signature
  | :signature_not_found
  | :ambiguous_signature
  | :unknown_key
  | :invalid_key
  | :algorithm_mismatch
  | {:unsupported_algorithm, term()}
  | {:missing_parameter, String.t()}
  | :signature_expired
  | :signature_too_old
  | :signature_not_yet_valid
  | :invalid_signature
```

# `default_components`
*since 0.9.0* 

```elixir
@spec default_components(message()) :: [component_spec()]
```

Returns the default covered components for a message (see `sign/3`).

## Examples

    iex> X402.HTTPSignature.default_components(%{method: "GET", url: "https://example.com/"})
    ["@method", "@authority", "@path"]

    iex> X402.HTTPSignature.default_components(%{method: "GET", url: "https://example.com/", headers: [{"PAYMENT-SIGNATURE", "abc"}]})
    ["@method", "@authority", "@path", "payment-signature"]

    iex> X402.HTTPSignature.default_components(%{status: 402, headers: %{"payment-required" => "abc"}, request: %{}})
    ["@status", "payment-required", {"@authority", [req: true]}, {"@path", [req: true]}]

    iex> X402.HTTPSignature.default_components(%{status: 200})
    ["@status"]

# `directory`
*since 0.9.0* 

```elixir
@spec directory([
  X402.HTTPSignature.Key.t() | {X402.HTTPSignature.Key.t(), keyword() | map()}
]) :: map()
```

Builds the `/.well-known/http-message-signatures-directory` document
(draft-meunier-http-message-signatures-directory §3): a JWK Set of the
keys' public parts.

Each entry is a `X402.HTTPSignature.Key` or `{key, extra}` where
`extra` is a keyword list or map of additional JWK members such as
`nbf` and `exp`.

## Examples

    iex> jwk = %{"kty" => "OKP", "crv" => "Ed25519", "x" => "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"}
    iex> {:ok, key} = X402.HTTPSignature.Key.from_jwk(jwk)
    iex> X402.HTTPSignature.directory([{key, nbf: 1_712_793_600, exp: 1_715_385_600}])
    %{
      "keys" => [
        %{
          "kty" => "OKP",
          "crv" => "Ed25519",
          "x" => "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
          "kid" => "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U",
          "alg" => "ed25519",
          "use" => "sig",
          "nbf" => 1712793600,
          "exp" => 1715385600
        }
      ]
    }

# `form_encode`
*since 0.9.0* 

```elixir
@spec form_encode(String.t()) :: String.t()
```

Percent-encodes a query parameter name or value as RFC 9421 §2.2.8
requires (the form-urlencoded percent-encode set, space as `%20`).

## Examples

    iex> X402.HTTPSignature.form_encode("with plus whitespace")
    "with%20plus%20whitespace"

    iex> X402.HTTPSignature.form_encode("façade\": ")
    "fa%C3%A7ade%22%3A%20"

    iex> X402.HTTPSignature.form_encode("a-b_c.d*e")
    "a-b_c.d*e"

# `merge_headers`
*since 0.9.0* 

```elixir
@spec merge_headers([[{String.t(), String.t()}]]) :: [{String.t(), String.t()}]
```

Merges the headers produced by several `sign/3` calls (with distinct
labels) into one `signature-input` and one `signature` header, as a
directory response signed with every key requires.

## Examples

    iex> X402.HTTPSignature.merge_headers([
    ...>   [{"signature-input", "a=();created=1"}, {"signature", "a=:AQ==:"}],
    ...>   [{"signature-input", "b=();created=2"}, {"signature", "b=:Ag==:"}]
    ...> ])
    [{"signature-input", "a=();created=1, b=();created=2"}, {"signature", "a=:AQ==:, b=:Ag==:"}]

# `normalize_components`
*since 0.9.0* 

```elixir
@spec normalize_components([component_spec() | component()]) ::
  {:ok, [component()]} | {:error, {:invalid_component, String.t()}}
```

Normalizes component specs (as accepted by `sign/3`) to component
identifiers with string parameter keys.

## Examples

    iex> X402.HTTPSignature.normalize_components(["@Method", {"@authority", req: true}, {"@query-param", name: "Pet"}])
    {:ok, [{"@method", []}, {"@authority", [{"req", true}]}, {"@query-param", [{"name", "Pet"}]}]}

    iex> X402.HTTPSignature.normalize_components([{"@path", req: "yes"}])
    {:error, {:invalid_component, "@path"}}

    iex> X402.HTTPSignature.normalize_components([:method])
    {:error, {:invalid_component, ":method"}}

# `sign`
*since 0.9.0* 

```elixir
@spec sign(message(), X402.HTTPSignature.Key.t(), keyword()) ::
  {:ok, [{String.t(), String.t()}]} | {:error, sign_error()}
```

Signs a message, returning the `signature-input` and `signature`
headers to attach to it.

The key must carry private material. Parameters are serialized in the
order `created`, `expires`, `keyid`, `alg`, `nonce`, `tag`.

## Options

* `:components` (list of `t:term/0`) - Covered components, in order. Defaults to `"@method"`, `"@authority"`
  and `"@path"` plus `"payment-signature"` when present for a request,
  and to `"@status"`, `"payment-required"`/`"payment-response"` when
  present, and `"@authority";req`/`"@path";req` when `:request` is
  given for a response.

* `:label` (`t:String.t/0`) - The signature label. The default value is `"sig1"`.

* `:created` - The `created` timestamp; defaults to now. `false` omits it.

* `:expires` (`t:non_neg_integer/0`) - Absolute `expires` timestamp.

* `:ttl` (`t:pos_integer/0`) - Sets `expires` to `created` plus this many seconds.

* `:nonce` - A `nonce` value; `true` generates a random one. The default value is `false`.

* `:tag` (`t:String.t/0`) - The application `tag`.

* `:keyid` - Overrides the key's `kid`; `false` omits the parameter.

* `:alg` (`t:boolean/0`) - Include the `alg` parameter. The default value is `false`.

## Examples

    iex> seed = Base.url_decode64!("n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU", padding: false)
    iex> {:ok, key} = X402.HTTPSignature.Key.new(alg: "ed25519", private_key: seed, kid: "test-key-ed25519")
    iex> message = %{
    ...>   method: "POST",
    ...>   url: "https://example.com/foo?param=Value&Pet=dog",
    ...>   headers: [
    ...>     {"Date", "Tue, 20 Apr 2021 02:07:55 GMT"},
    ...>     {"Content-Type", "application/json"},
    ...>     {"Content-Length", "18"}
    ...>   ]
    ...> }
    iex> X402.HTTPSignature.sign(message, key,
    ...>   label: "sig-b26",
    ...>   components: ["date", "@method", "@path", "@authority", "content-type", "content-length"],
    ...>   created: 1_618_884_473
    ...> )
    {:ok, [
      {"signature-input", ~S|sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"|},
      {"signature", "sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"}
    ]}

    iex> {:ok, key} = X402.HTTPSignature.Key.generate("ed25519")
    iex> X402.HTTPSignature.sign(%{method: "GET", url: "https://example.com/"}, key, components: ["content-type"])
    {:error, {:missing_component, ~S|"content-type"|}}

# `signature_base`
*since 0.9.0* 

```elixir
@spec signature_base(message(), [component_spec()], params()) ::
  {:ok, String.t()} | {:error, base_error()}
```

Builds the signature base (RFC 9421 §2.5) for a message, covered
components and signature parameters.

## Examples

    iex> message = %{method: "POST", url: "https://example.com/foo?param=Value&Pet=dog",
    ...>   headers: [{"Content-Type", "application/json"}]}
    iex> {:ok, base} = X402.HTTPSignature.signature_base(message,
    ...>   ["@method", "@authority", "@path", "content-type"],
    ...>   [{"created", 1_618_884_473}, {"keyid", "test-key-rsa-pss"}])
    iex> String.split(base, "\n")
    [
      ~S|"@method": POST|,
      ~S|"@authority": example.com|,
      ~S|"@path": /foo|,
      ~S|"content-type": application/json|,
      ~S|"@signature-params": ("@method" "@authority" "@path" "content-type");created=1618884473;keyid="test-key-rsa-pss"|
    ]

    iex> X402.HTTPSignature.signature_base(%{method: "GET", url: "https://example.com/"}, ["@status"], [])
    {:error, {:invalid_component, ~S|"@status"|}}

    iex> X402.HTTPSignature.signature_base(%{method: "GET", url: "https://example.com/"}, ["@path", "@path"], [])
    {:error, {:duplicate_component, ~S|"@path"|}}

# `verify`
*since 0.9.0* 

```elixir
@spec verify(
  message(),
  keyword()
) :: {:ok, verified()} | {:error, verify_error()}
```

Verifies a signature on a message.

Exactly one signature is verified: the one selected by `:label` and/or
`:tag`, or the only signature present; several candidates are
`:ambiguous_signature`. The key is resolved from the `keyid` parameter
through `:keys`; a key with a `kid` different from the parameter is
rejected, as is a key whose algorithm differs from an explicit `alg`
parameter or is not in `:algorithms`.

Time checks use the `created`/`expires` parameters when present:
`expires` in the past, `created` in the future, or `created` older
than `:max_age` fail the verification (`:clock_skew` widens each check).
Nothing is bounded by default: set `:max_age` (which also demands
`created`) or `required_params: ["expires"]` to enforce freshness.

## Options

* `:keys` (`t:term/0`) - Required. The verification keys: a `X402.HTTPSignature.Key`, a list of them
  (matched by `kid`), or a function receiving the `keyid` parameter
  (or `nil`) and returning a key, a JWK map, `{:ok, key}`, or
  `nil`/`:error`.

* `:label` (`t:String.t/0`) - Verify the signature with this label only.

* `:tag` (`t:String.t/0`) - Verify the signature carrying this `tag` only.

* `:algorithms` (list of `t:String.t/0`) - Accepted algorithms. The default value is `["ed25519", "ecdsa-p256-sha256", "rsa-pss-sha512"]`.

* `:required_components` (list of `t:term/0`) - Components that must be covered (same forms as `sign/3`). The default value is `[]`.

* `:required_params` (list of `t:String.t/0`) - Signature parameters that must be present, for example `["created"]`. The default value is `[]`.

* `:max_age` (`t:pos_integer/0`) - Maximum seconds since `created`.

* `:clock_skew` (`t:non_neg_integer/0`) - Tolerance for time checks. The default value is `0`.

* `:now` (`t:non_neg_integer/0`) - The current UNIX time; defaults to the system clock.

## Examples

    iex> jwk = %{"kty" => "OKP", "crv" => "Ed25519", "kid" => "test-key-ed25519", "x" => "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"}
    iex> {:ok, key} = X402.HTTPSignature.Key.from_jwk(jwk)
    iex> message = %{
    ...>   method: "POST",
    ...>   url: "https://example.com/foo?param=Value&Pet=dog",
    ...>   headers: [
    ...>     {"date", "Tue, 20 Apr 2021 02:07:55 GMT"},
    ...>     {"content-type", "application/json"},
    ...>     {"content-length", "18"},
    ...>     {"signature-input", ~S|sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"|},
    ...>     {"signature", "sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"}
    ...>   ]
    ...> }
    iex> {:ok, verified} = X402.HTTPSignature.verify(message, keys: [key])
    iex> {verified.label, verified.params["created"], length(verified.components)}
    {"sig-b26", 1618884473, 6}

    iex> X402.HTTPSignature.verify(%{method: "GET", url: "https://example.com/"}, keys: [])
    {:error, :missing_signature}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
