# Webhook replies, handshakes and further signature schemes

This is the detail behind [Webhooks that need a reply](receiving-from-outside.md#webhooks-that-need-a-reply)
and [Handshakes](receiving-from-outside.md#handshakes): how the Twilio, Slack and SNS signatures
are checked, how each handshake is answered, and how a reply is made,
kept and limited.

- **Twilio.** `SignatureScheme.twilio`: `X-Twilio-Signature`, the Base64
  of the HMAC-SHA1 under the auth token (the connection's secret) of the
  URL Twilio requested followed by each form member's name and value,
  sorted by name and, for a name sent twice, by value. The URL is
  `https://`, the request's `Host` and its path and query, tried with
  `:443` written and without, as Twilio's own libraries do; Twilio sends
  only to HTTPS and a proxy in front of the host ends the TLS. It signs no
  time, so no age is checked; a repeat is dropped by its id. The body is
  a form (`400 invalid_request` otherwise), read as JSON of its members'
  text, a name sent twice a list, so the places and fields name form
  members (`event_id: "CallSid"`).
- **Slack.** `SignatureScheme.slack`, since no other scheme checks
  Slack's events: `X-Slack-Signature: v0=<hex>`, the HMAC-SHA-256 of
  `v0:<X-Slack-Request-Timestamp>:<body>` under the signing secret, with
  the five-minute tolerance.
- **SNS.** `SignatureScheme.sns`: the message's own `Signature`, RSA
  PKCS #1 v1.5 over the members SNS lists for its `Type`, with SHA-1
  (`SignatureVersion` 1) or SHA-256 (2), by the RSA key (2,048 bits or
  more) of the certificate at its `SigningCertURL`, valid at the host's
  time; the chain isn't checked, since the certificate comes over HTTPS
  from AWS's own host. Any AWS account can have SNS sign a message from
  its own topic, so the connection's `secret:` is the topic's ARN, and a
  message from any other topic is refused before anything is fetched.
  The certificate is fetched only from `https://sns.<region>.amazonaws.com`
  (or `.amazonaws.com.cn`), a `.pem` path with no port, credentials or
  query, by the host's guard for the runtime's own requests: HTTPS only,
  no redirect followed, connected only to the public address it checked
  (never loopback, private, link-local, shared, reserved or multicast,
  and an IPv4 address carried inside IPv6, by NAT64, 6to4 or Teredo,
  judged as itself),
  at most 16 KB within five seconds; each certificate is kept for a day.
  SNS signs the time it published and its retries keep it, so the
  five-minute tolerance holds only ahead of the host's clock: a message
  signed 30 days ago or more, as long as a received id is kept, is
  refused, so any replay within that is a repeat by its `MessageId`. A
  certificate that can't be fetched is `503 unavailable`, so SNS sends it
  again; a host that reaches nothing answers so.
  The `Message`, when it holds JSON, is read as that JSON, so places reach
  inside it (`"Message.type"`). Anything but a `Notification` carries no
  event: answered `204`.
- **Handshakes.** `handshake:` names `WebhookHandshake.sns`, `.slack` or
  `.microsoft_graph` (`package.provider_invalid` otherwise); SNS's and
  Slack's need `SignatureScheme.sns` and `.slack`, which sign them
  (`package.provider_invalid` otherwise). SNS's `SubscriptionConfirmation`,
  signed and from the topic, is confirmed by a `GET` of its `SubscribeURL`
  on an SNS host through the same guard (`204` when SNS took it, `503`
  otherwise). Slack's `url_verification`, once signed, is answered
  `200` with `{"challenge": ...}`. Microsoft Graph's validation is the
  only request answered before a signature is looked for, since Graph
  signs none: a `POST` whose query gives one `validationToken` of at most
  1,024 characters, none a control character, answered `200` with the
  token as `text/plain; charset=utf-8` and `X-Content-Type-Options:
  nosniff` (`400 invalid_request` for one that isn't).
- **Replies.** `reply:` in a webhook event a package declares names an
  `Object` of the package's own (`package.wire_invalid` otherwise); on
  any other event `reply` is reserved (`type.reserved_field_name`), so it
  is never a field. The connection's `replies:` keys each event needing a
  reply by its name in lower_snake_case, `incoming_call:`, naming an
  action `<domain>.<action>` (any domain; the app's `use` line for it, as
  `on_first_sign_in:`'s) that takes the one event and returns the reply
  type: a key no such event has is `connection.key_invalid`, a value that
  isn't such an action `connection.value_invalid`, and an event needing a
  reply with none `connection.key_missing`. Subscribing to such an event
  is `subscriber.event_invalid`: it isn't kept in the outbox.
- **Answering.** After the signature and the body are read, the event's
  id is claimed in `jadpo_received_events` (its new `reply` column empty)
  in a statement of its own; the action runs as the system, reaching no
  tenant's records, in its own transaction, as
  `on_first_sign_in:`'s does; its reply is encoded with
  the reply type's codec on the provider's wire and sent `200` as
  `application/json`, and kept over the claim once the action has
  committed. A repeat that finds the reply is answered with it, one that
  finds a claim younger than a minute `409 conflict` ("The webhook is
  still being answered."), and a claim older than that, left by a process
  that stopped, is taken over. A failure gives the claim back, so the
  provider's retry runs the action again: a declared failure answered as
  a route answers it, `unavailable` `503`, a fault `500`. Keeping the
  reply follows the action's commit rather than joining it, since the
  action may write another store: if that statement fails, a retry after
  the claim lapses runs the action again.
- **What a reply may show.** What only `Access.public` lets the reply
  action in to is `policy.system_grant_missing` at its `replies:` entry.
  The reply goes back to whoever sent the webhook, so each field it
  carries, or that decided it, is one anyone at all may read
  (`policy.output_field_unproved`), a record the action created holding
  what it set. Whether records are there reaches the sender too: a reply
  counting or testing for records only the system may read, or a failure
  decided by it, is refused the same way, and the action's failure,
  answered to the sender as its reply is, is held to the same proof, what
  decides it and its client fields
  ([Field restrictions](field-restrictions.md)).

> Graph's own notification check, its `clientState` in the body, isn't a
> signature scheme yet. Twilio's TwiML replies wait for an XML byte
> format.
