Every block runs, in order, in the repository's test suite.

Keys and grants

Keys

alias BoundedAuthorityProtocol.V1
alias BoundedAuthorityProtocol.V1.{Credentials, ExpectedGrant, ExpectedRequest, Grant, Operation, Proof, TrustedIssuer}

{issuer_pub, issuer_priv} = :crypto.generate_key(:eddsa, :ed25519)
{holder_pub, holder_priv} = :crypto.generate_key(:eddsa, :ed25519)
{:ok, thumbprint} = V1.Jwk.public_key_thumbprint_raw(holder_pub, %{})
sign = &:crypto.sign(:eddsa, :none, &1, [&2, :ed25519])
byte_size(thumbprint)

The value is 32.

Sign a grant

grant = %Grant{
  key_id: "iss-1", issuer: "https://iss.example", grant_id: "g-1",
  audiences: ["https://api.example"], issued_at: 1_800_000_000,
  not_before: 1_800_000_000, expires_at: 1_800_003_600,
  holder_thumbprint: thumbprint,
  operations: [%Operation{name: "read_file", selectors: [{:equals, ["path"], {:string, "/notes.txt"}}]}]
}

{:ok, gi} = V1.grant_signing_input(grant, %{})
{:ok, grant_c} = V1.assemble_compact(gi, sign.(gi.message, issuer_priv))
length(String.split(grant_c, "."))

The value is 3.

Proofs and verification

Sign a proof

args = {:object, [{"path", {:string, "/notes.txt"}}]}

proof = %Proof{
  holder_public_key: holder_pub, proof_id: "p-1", method: "GET",
  target_uri: "https://api.example/files", issued_at: 1_800_000_100, nonce: nil,
  invocation_id: "1f9b2c4d-6e8a-4b0c-9d2e-3f5a7b9c1d0e", operation: "read_file",
  grant_compact: grant_c, cast_arguments: args
}

{:ok, pi} = V1.proof_signing_input(proof, %{})
{:ok, proof_c} = V1.assemble_compact(pi, sign.(pi.message, holder_priv))
length(String.split(proof_c, "."))

The value is 3.

Verify an envelope

trusted = %TrustedIssuer{key_id: "iss-1", public_key: issuer_pub}

expected = %ExpectedRequest{
  trusted_issuer: trusted, issuer: "https://iss.example", audience: "https://api.example",
  method: "GET", target_uri: "https://api.example/files",
  invocation_id: "1f9b2c4d-6e8a-4b0c-9d2e-3f5a7b9c1d0e", operation: "read_file",
  cast_arguments: args, evaluation_time: 1_800_000_100, clock_skew: 60, proof_max_age: 300,
  nonce: :not_required, bounds: %{}
}

{:ok, facts} = V1.check_envelope(%Credentials{grant: grant_c, proof: proof_c}, expected)
facts.authorization

The value is :not_evaluated.

Verify a grant alone

expected_grant = %ExpectedGrant{
  issuer: "https://iss.example", audience: "https://api.example",
  evaluation_time: 1_800_000_100, clock_skew: 60, bounds: %{}
}

{:ok, grant_facts} = V1.verify_grant(grant_c, trusted, expected_grant)
grant_facts.grant_id

The value is "g-1".

Decoding and diagnosis

Read the key id (untrusted)

{:ok, locator} = V1.untrusted_key_locator(grant_c, %{})
{locator.kid, locator.trust}

The value is {"iss-1", :not_evaluated}.

Decode without verifying

{:ok, decoded} = V1.decode_grant(grant_c, %{})
{decoded.issuer, decoded.verification}

The value is {"https://iss.example", :not_evaluated}.

Tagged JSON from bytes

V1.Json.decode(~s({"n":2,"x":2.5,"ok":true}), %{})

The value is {:ok, {:object, [{"n", {:integer, 2}}, {"x", {:float, 2.5}}, {"ok", {:boolean, true}}]}}.

Normalize a URI

V1.Uri.normalize("HTTPS://API.example:443/files", %{})

The value is {:ok, "https://api.example/files"}.

Limits and errors

Every failure

V1.check_envelope(%Credentials{grant: grant_c, proof: proof_c}, %{expected | method: "PUT"})

The value is {:error, :invalid}.

Tighten a bound

{V1.Bounds.coerce(%{depth: 8}) |> elem(0), V1.Bounds.coerce(%{depth: 10_000})}

The value is {:ok, {:error, :invalid}}: bounds only tighten.

Contract-majors and suites

Pick a namespace

NamespaceSuiteSelectors
V1BAP1-Ed25519-SHA256equals, one_of, all
V2BAP2-Ed25519-SHA256adds lte, gte
V3BAP3-ES256-SHA256adds lte, gte

ES256 signature from DER

alias BoundedAuthorityProtocol.Es256
{_es_pub, es_priv} = :crypto.generate_key(:ecdh, :prime256v1)
der = :crypto.sign(:ecdsa, :sha256, "message", [es_priv, :prime256v1])
{:ok, raw} = Es256.der_to_raw(der)
{:ok, low_s} = Es256.normalize_low_s(raw)
{byte_size(low_s), Es256.canonical_signature?(low_s)}

The value is {64, true}.