defmodule AppIdentity do
@moduledoc """
`AppIdentity` is an Elixir implementation of the Kinetic Commerce application
[identity proof algorithm](spec.md).
It implements identity proof generation and validation functions. These
functions expect to work with an application struct (`t:AppIdentity.App.t/0`).
## Telemetry Support
If [telemetry](https://hexdocs.pm/telemetry) is a dependency in your
application, and the telemetry is not explicitly disabled, telemetry events
will be emitted for `AppIdentity.generate_proof/2`,
`AppIdentity.verify_proof/3`, and `AppIdentity.Plug`. See
`AppIdentity.Telemetry` for more information.
### Disabling Telemetry
Telemetry may be disabled by setting this for your configuration:
config :app_identity, AppIdentity.Telemetry, enabled: false
Recompile `app_identity` if this setting is changed.
"""
alias AppIdentity.{App, AppIdentityError, Proof}
@typedoc """
The App Identity app unique identifier. Validation of the `id` value will
convert non-string IDs using Kernel.to_string/1.
If using integer IDs, it is recommended that the `id` value be provided as
some form of extended string value, such as that provided by Rails [global
ID](https://github.com/rails/globalid) or the `absinthe_relay`
[Node.IDTranslator](https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Node.IDTranslator.html).
Such representations are _also_ recommended if the ID is a compound value.
`t:id/0` values _must not_ contain a colon (`:`) character.
"""
@type id :: binary()
@typedoc """
The App Identity app secret value. This value is used _as provided_ with no
encoding or decoding. Because this is a sensitive value, it may be provided as
a closure in line with the EEF Security Working Group sensitive data
[recommendation](https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/sensitive_data#wrapping).
`AppIdentity.App` always stores this value as a closure.
"""
@type secret :: binary()
@typedoc """
The positive integer version of the App Identity algorithm to use. Will be
validated to be a supported version for app creation, and not an explicitly
disallowed version during proof validation.
If provided as a string value, it must convert cleanly to an integer value,
which means that a version of `"3.5"` is not a valid value.
App Identity algorithm versions are strictly upgradeable. That is, a version
1 app can verify version 1, 2, 3, or 4 proofs. However, a version 2 app will
_never_ validate a version 1 proof.
<table>
<thead>
<tr>
<th rowspan=2>App Identity<br />Version</th>
<th rowspan=2>Nonce</th>
<th rowspan=2>Digest Algorithm</th>
<th colspan=4>Can Verify Proof<br />from Version</th>
</tr>
<tr><th>1</th><th>2</th><th>3</th><th>4</th></tr>
</thead>
<tbody>
<tr>
<th>1</th>
<td>random</td>
<td>SHA2-256</td>
<td align="center">Yes</td>
<td align="center">Yes</td>
<td align="center">Yes</td>
<td align="center">Yes</td>
</tr>
<tr>
<th>2</th>
<td>timestamp ± fuzz</td>
<td>SHA2-256</td>
<td align="center">-</td>
<td align="center">Yes</td>
<td align="center">Yes</td>
<td align="center">Yes</td>
</tr>
<tr>
<th>3</th>
<td>timestamp ± fuzz</td>
<td>SHA2-384</td>
<td align="center">-</td>
<td align="center">-</td>
<td align="center">Yes</td>
<td align="center">Yes</td>
</tr>
<tr>
<th>4</th>
<td>timestamp ± fuzz</td>
<td>SHA2-512</td>
<td align="center">-</td>
<td align="center">-</td>
<td align="center">-</td>
<td align="center">Yes</td>
</tr>
</tbody>
</table>
"""
@type version :: pos_integer()
@typedoc """
A nonce value used in the algorithm proof. The shape of the nonce depends on
the algorithm `t:version/0`.
Version 1 `t:nonce/0` values should be cryptographically secure and
non-sequential, but sufficiently fine-grained timestamps (those including
microseconds, as `yyyymmddHHMMSS.sss`) _may_ be used. Version 1 proofs verify
that the nonce is at least one byte long and do not contain a colon (`:`).
Version 2, 3, and 4 `t:nonce/0` values only permit fine-grained timestamps
that should be generated from a clock in sync with Network Time Protocol
servers. The timestamp will be parsed and compared to the server time (also in
sync with an NTP server).
"""
@type nonce :: binary()
@typedoc """
A list of algorithm versions that are not allowed.
The presence of an app in this list will prevent the generation or
verification of proofs for the specified version.
If `nil`, an empty list, or missing, all versions are allowed.
"""
@type disallowed :: {:disallowed, list(version())}
@typedoc """
Options for generating or verifying proofs.
- `nonce` can specify a precomputed nonce for proof generation. It will be
verified by the algorithm version for correctness and compatibility, but
will otherwise be used unmodified. This option is ignored for proof
verification.
- `version` can specify the generation of a proof compatible with, but
different than, the application version. This option is ignored for proof
verification.
"""
@type option ::
disallowed
| {:nonce, nonce()}
| {:version, version()}
@doc """
Generate an identity proof string for the given application. Returns `{:ok,
proof}` or `:error`.
If `nonce` is provided, it must conform to the shape expected by the proof
version. If not provided, it will be generated.
If `version` is provided, it will be used to generate the nonce and the proof.
This will allow a lower level application to raise its version level.
### Examples
A version 1 app can have a fixed nonce, which will always produce the same
value.
iex> {:ok, app} = AppIdentity.App.new(%{version: 1, id: "decaf", secret: "bad"})
iex> AppIdentity.generate_proof(app, nonce: "hello")
{:ok, "ZGVjYWY6aGVsbG86RDNGNjJCQTYyOEIyMzhEOTgwM0MyNEU4NkNCOTY3M0ZEOTVCNTdBNkJGOTRFMkQ2NTMxQTRBODg1OTlCMzgzNQ=="}
A version 2 app fails when given a non-timestamp nonce.
iex> AppIdentity.generate_proof(v1(), version: 2, nonce: "hello")
:error
A version 2 app _cannot_ generate a version 1 nonce.
iex> AppIdentity.generate_proof(v2(), version: 1)
:error
A version 2 app will be rejected if the version has been disallowed.
iex> AppIdentity.generate_proof(v2(), disallowed: [1, 2])
:error
### Telemetry
When telemetry is enabled, `generate_proof/2` will emit:
- `[:app_identity, :generate_proof, :start]`
- `[:app_identity, :generate_proof, :stop]`
"""
@spec generate_proof(App.t() | App.loader() | App.t(), [option()]) ::
{:ok, binary()} | :error
def generate_proof(app, options \\ []) do
case AppIdentity.Internal.generate_proof(app, options) do
{:ok, _} = ok -> ok
_ -> :error
end
end
@doc """
Generate an identity proof string for the given application. Returns the proof
string or raises an exception on error.
If `nonce` is provided, it must conform to the shape expected by the proof
version. If not provided, it will be generated.
If `version` is provided, it will be used to generate the nonce and the proof.
This will allow a lower level application to raise its version level.
### Examples
A version 1 app can have a fixed nonce, which will always produce the same
value.
iex> {:ok, app} = AppIdentity.App.new(%{version: 1, id: "decaf", secret: "bad"})
iex> AppIdentity.generate_proof!(app, nonce: "hello")
"ZGVjYWY6aGVsbG86RDNGNjJCQTYyOEIyMzhEOTgwM0MyNEU4NkNCOTY3M0ZEOTVCNTdBNkJGOTRFMkQ2NTMxQTRBODg1OTlCMzgzNQ=="
A version 2 app fails when given a non-timestamp nonce.
iex> AppIdentity.generate_proof!(v1(), version: 2, nonce: "hello")
** (AppIdentity.AppIdentityError) Error generating proof
A version 2 app _cannot_ generate a version 1 nonce.
iex> AppIdentity.generate_proof!(v2(), version: 1)
** (AppIdentity.AppIdentityError) Error generating proof
A version 2 app will be rejected if the version has been disallowed.
iex> AppIdentity.generate_proof!(v2(), disallowed: [1, 2])
** (AppIdentity.AppIdentityError) Error generating proof
### Telemetry
When telemetry is enabled, `generate_proof!/2` will emit:
- `[:app_identity, :generate_proof, :start]`
- `[:app_identity, :generate_proof, :stop]`
Telemetry events are emitted *before* any error exceptions are thrown.
"""
@spec generate_proof!(App.t() | App.loader() | App.t(), [option()]) :: binary()
def generate_proof!(app, options \\ []) do
case AppIdentity.Internal.generate_proof(app, options) do
{:ok, value} -> value
_ -> raise AppIdentityError, :generate_proof
end
end
@doc """
Parses a proof string into an AppIdentity.Proof struct. Returns `{:ok, proof}`
or `:error`.
"""
@spec parse_proof(Proof.t() | binary()) :: {:ok, Proof.t()} | :error
def parse_proof(proof) do
case AppIdentity.Internal.parse_proof(proof) do
{:ok, _} = ok -> ok
_ -> :error
end
end
@doc """
Parses a proof string into an AppIdentity.Proof struct. Returns the parsed
proof or raises an exception.
"""
@spec parse_proof!(Proof.t() | binary()) :: Proof.t()
def parse_proof!(proof) do
case AppIdentity.Internal.parse_proof(proof) do
{:ok, value} -> value
_ -> raise AppIdentityError, :parse_proof
end
end
@doc """
Verify a `AppIdentity` proof value using a a provided `app`. Returns `{:ok,
app}`, `{:ok, nil}`, or `:error`.
The `proof` may be provided either as a string or a parsed
`t:AppIdentity.Proof.t/0` (from `parse_proof/1`). String proof values are
usually obtained from HTTP headers. At Kinetic Commerce, this has generally
jeen `KCS-Application` or `KCS-Service`.
The `app` can be provided as one of `t:AppIdentity.App.input/0`,
`t:AppIdentity.App.t/0`, or `t:AppIdentity.App.finder/0`. If provided
a finder, it will be called with the `proof` value.
`verify_proof/3` has three possible return values:
- `{:ok, AppIdentity.App}` when the proof is validated against the provided or
located application;
- `{:ok, nil}` when the proof matches the provided or located application, but
it does not validate.
- `:error` when there is any error during proof validation.
```elixir
AppIdentity.verify_proof(proof, &IdentityApplications.get!(&1.id))
```
### Telemetry
When telemetry is enabled, `verif_proof/3` will emit:
- `[:app_identity, :verify_proof, :start]`
- `[:app_identity, :verify_proof, :stop]`
"""
@spec verify_proof(Proof.t() | binary(), App.finder() | App.input() | App.t(), [
option()
]) ::
{:ok, App.t() | nil} | :error
def verify_proof(proof, app, options \\ []) do
case AppIdentity.Internal.verify_proof(proof, app, options) do
{:ok, _} = ok -> ok
_ -> :error
end
end
@doc """
Verify a `AppIdentity` proof value using a a provided `app`. Returns the app,
nil, or raises an exception on error.
The `proof` may be provided either as a string or a parsed
`t:AppIdentity.Proof.t/0` (from `parse_proof/1`). String proof values are
usually obtained from HTTP headers. At Kinetic Commerce, this has generally
jeen `KCS-Application` or `KCS-Service`.
The `app` can be provided as one of `t:AppIdentity.App.input/0`,
`t:AppIdentity.App.t/0`, or `t:AppIdentity.App.finder/0`. If provided
a finder, it will be called with the `proof` value.
`verify_proof/3` has two possible return values:
- `AppIdentity.App` when the proof is validated against the provided or
located application;
- `nil` when the proof matches the provided or located application, but it
does not validate.
It raises an exception on any error during proof validation.
```elixir
AppIdentity.verify_proof(proof, &IdentityApplications.get!(&1.id))
```
### Telemetry
When telemetry is enabled, `verify_proof!/3` will emit:
- `[:app_identity, :verify_proof, :start]`
- `[:app_identity, :verify_proof, :stop]`
Telemetry events are emitted *before* any error exceptions are thrown.
"""
@spec verify_proof!(Proof.t() | binary(), App.finder() | App.t(), [option()]) ::
App.t() | nil
def verify_proof!(proof, app, options \\ []) do
case AppIdentity.Internal.verify_proof(proof, app, options) do
{:ok, value} -> value
_ -> raise AppIdentityError, :verify_proof
end
end
@info %{
name: AppIdentity.MixProject.project()[:name],
version: AppIdentity.MixProject.project()[:version],
spec_version: 4
}
@doc """
The name, version, and supported specification version of this App Identity
package for Elixir.
"""
@spec info :: %{name: String.t(), version: String.t(), spec_version: pos_integer()}
def info do
@info
end
@doc """
The name, version, or supported specification version of this App Identity
package for Elixir.
"""
@spec info(:name | :spec_version | :version) :: String.t() | pos_integer()
def info(key) when key in [:name, :spec_version, :version] do
@info[key]
end
end