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.authorizationThe 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_idThe 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
| Namespace | Suite | Selectors |
|---|---|---|
V1 | BAP1-Ed25519-SHA256 | equals, one_of, all |
V2 | BAP2-Ed25519-SHA256 | adds lte, gte |
V3 | BAP3-ES256-SHA256 | adds 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}.