Contextfulv0.23.0

Docs / Specification

contract authority · 50-authority.md · 51-enforcement.md · guide · card

Admission and authority

A caller reaches Contextful holding a capability credential. This file states what the credential says, who mints it, what a holder derives from it offline, what a checkpoint decides about it, and the admitted-authority value that decision hands to every effect. The relation a grant compiles into is stated in spec/51-enforcement.md.

The path of a credential from issuance to the effects it authorizes:

Flowchart diagram Flowchart, top to bottom. 12 nodes, 12 edges. identity provider → token exchange [verified assertion] admin grant holder → signing port [mint] issuer: token exchange → signing port [role and default grants] issuer: signing port → capability credential [subject tuple, grants] holder: capability credential → child credential [attenuate: append a block]; → chain verifies? [presented] holder: child credential → chain verifies? [presented] checkpoint: issuer key set → chain verifies? [static pins, published route] checkpoint: denylist → chain verifies? [revocation epoch] checkpoint: chain verifies? → admitted authority [yes]; → refused credential [no] admitted authority → registered relations [reads and row-landing effects] enforcement: registered relations issuer role and default grants token exchange signing port holder attenuate: append a block capability credential child credential checkpoint static pins, published route revocation epoch issuer key set denylist chain verifies? enforcement registered relations verified assertion mint subject tuple, grants presented presented yes no reads and row-landing effects identity provider admin grant holder admitted authority refused credential

identify

The subject tuple, its per-member attestation, hygiene at the mint, and the identity links connecting a subject to a source’s principals.

unsettled: Does a subject with no verified principal need a case distinct from the project owner presenting nothing? owner: authority affects: authority.identify

profile

The versioned delegation profile over the attenuable-credential library: admitted elements, reserved facts, the bounded evaluator, the scoped session.

grant

What a credential says a holder does: actions, table patterns, tenant scope, the template allowlist, ceilings, and resources answering by name.

unsettled: Does a credentialed run start or pipeline run also need execute over its pipeline beside write over its tables? owner: authority affects: authority.grant

attenuate

Deriving a narrower child offline, and the narrowing legality of each grant dimension.

unsettled: What delegation depth does the library format support, and what verification cost does a chain carry at that depth? owner: authority affects: authority.attenuate

issue

Minting: the persisted lifetime ceiling, the principal a row-landing grant needs, signing custody, and the authoring posture.

signing keys

One signing port signs every credential, and its key rotates on a recorded cadence.

  • signing-porttestOne signing port signs every credential’s authority block and every audit root and tip; no mint path reads a private key. Seed files, secret references resolved at mint time and remote signing oracles are its adapters.
    WhyA-authority#pinned-keys
  • oracle-custodytestUnder the oracle adapter the seed stays inside the custodian, which records each mint as a signing call.
    WhyA-authority#pinned-keys
  • signature-encodingtestA signing port names its encoding: ed25519, a 32-byte key and 64-byte signature; es256-der or es256-raw, a SEC1 P-256 point signing ECDSA over SHA-256 of the message as ASN.1 DER or 64-byte r ‖ s.
    Whybecause an enclave, a cloud KMS and a browser key each answer ES256 in one encoding, and the tag lets every adapter answer in its own
  • der-at-the-edgetestAn es256-raw signature converts to DER where it leaves the port; a credential and an audit root or tip carry ES256 only as DER.
    Whybecause the credential format verifies ES256 only in DER, and one stored encoding holds a verifier to one decoder
  • encoding-invalidtestA port signature that does not decode under its encoding tag, or does not verify under the port’s public key, raises SignatureEncodingInvalid; nothing is minted and no root or tip is written.
    Whybecause a mislabelled signature yields a credential every checkpoint rejects, and refusing at the port names the adapter at fault
  • public-key-texttestA port’s public key prints as ed25519:<hex> or es256:<hex> of its SEC1 point; either form, with a compressed or uncompressed point, parses back to the key it names.
  • algorithmtestA credential names Ed25519, the default, or ECDSA over P-256; the pinned key’s scheme is authoritative.
    WhyA-authority#pinned-keys
  • algorithm-mismatchtestA credential naming a scheme other than its pinned key’s raises SignatureAlgorithmMismatch.
    WhyA-authority#pinned-keys
  • key-rotationtestThe issuer signing key rotates every 90 d, and at once on suspected compromise.
    WhyA-authority#pinned-keys
  • rotation-not-duetestA rotation without --compromise before key-rotation falls due raises IssuerKeyRotationNotDue, naming the instant it falls due.
    Whybecause a scheduled rotation ahead of its cadence churns every verifier’s pins with nothing withdrawn
  • unrecorded-keytestA rotation without --compromise of an issuer key the key-set ledger does not date raises IssuerKeyUnrecorded; contextful token record --since dates it.
    Whybecause an undated key otherwise rotates at once, skipping the cadence the ledger enforces

unsettled: Does a read face — query, mcp, a served face — refuse a manifest declaring no authoring_posture, or does the authoring posture bind table write verbs alone? owner: authority affects: authority.issue

verify

Admission at a checkpoint: signatures, audience, timestamps, possession proof, key sets, and the admitted-authority value.

local transport

A pipe or Unix socket admits a credential through a holder proof or the peer’s uid.

key sets

A checkpoint verifies against pinned or published issuer keys and calls no issuer.

unsettled: Does a proof replayed against a sibling checkpoint need a shared nonce store or a checkpoint-issued nonce? owner: authority affects: authority.verify

revoke

Ending a credential’s usefulness ahead of expiry: short lifetimes, the denylist, the scoped epoch, key rotation, format withdrawal.

  • short-lifetimetestA short lifetime is the primary withdrawal: a credential’s remaining lifetime bounds its exposure after a provider sign-out or before a denylist entry propagates.
    WhyA-authority#issuance-ceiling
  • denylisttestA checkpoint reads a denylist keyed on credential identifier. An entry ages out once its identifier verifies under no live key version.
    WhyA-authority#offline-narrowing
  • revocation-idtestEvery derivation carries its own revocation identifier, withdrawing one subtree without ending the root it descends from.
    WhyA-authority#offline-narrowing
  • epochtestA revocation epoch scoped on project, tenant and principal class invalidates that slice of outstanding authority.
    WhyA-authority#issuance-ceiling
  • epoch-storetestA project persists each scope’s current epoch and each issuer key version’s retirement in .contextful/keyset.toml beside the issuer seed; a mint stamps its scope’s current epoch, and a checkpoint re-reads the file at each admission.
    Whybecause a file the checkpoint reads locally keeps a policy service off the admission path, and the seed’s directory already holds the issuer’s custody
  • ledger-unavailabletestA checkpoint pointed at a key-set ledger, or that has read one, raises KeySetLedgerUnavailable and admits nothing once the file is absent or malformed.
    Whybecause a vanished ledger read as empty lifts every key retirement and epoch bump
  • unknown-principal-classtestcontextful token revoke --principal-class naming neither delegated nor unattributed raises RevocationPrincipalClassUnknown and bumps nothing.
    Whybecause an epoch under a class no credential carries withdraws nothing while reporting success
  • revokedtestA credential on the denylist, or carrying an epoch below the current scoped epoch, raises AuthorityRevoked at the next effect boundary.
    WhyA-authority#offline-narrowing
  • compromisetestSuspected compromise of signing material runs a project-wide epoch bump together with immediate retirement of the key version. Waiting out a grace window withdraws nothing.
    WhyA-authority#issuance-ceiling
  • rotation-gracetestA rotation policy declares a cadence and a grace window, validated against the persisted issuance policy, during which the retiring key version still verifies.
    WhyA-authority#issuance-ceiling
  • short-gracetestA declared or overridden grace window shorter than the effective issuance ceiling raises RotationGraceTooShort.
    WhyA-authority#issuance-ceiling
  • immediate-retiretestImmediate retirement removes a key version from the published set and every static pin; a checkpoint drops it at its next refresh.
    WhyA-authority#issuance-ceiling
  • format-withdrawntestFrom a declared cutover instant, every admission path raises CredentialFormatWithdrawn for a credential in a withdrawn format. Only an explicit declaration restores acceptance.
    WhyA-authority#issuance-ceiling

The life of one issuer key version:

State diagram State diagram, top to bottom. 5 states, 6 transitions. start → Current [published set and static pins carry it] Current → Retiring [rotation every 90 d]; → Retired [compromise, project-wide epoch bump] Retiring → Retired [grace window lapses]; → Retired [immediate retirement] Retired → end [checkpoints drop it at the next refresh] published set and static pinscarry it rotation every 90 d grace window lapses immediate retirement compromise, project-wideepoch bump checkpoints drop it at thenext refresh Current Retiring Retired

unsettled: How does a bumped epoch reach a checkpoint on another node before the credentials it supersedes lapse? owner: authority affects: authority.revoke

exchange

Exchanging a verified external assertion for a scoped credential under injected verifying material and a declared policy.

Shapes

The authority block’s content, as the profile maps it:

{
  "iss": "contextful://acme-research",
  "aud": "contextful://acme-research",
  "jti": "01JBQ7K4F3S9W2X6",
  "iat": 1770000000,
  "exp": 1770000900,
  "alg": "Ed25519",
  "cnf": { "jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" },
  "sub": {
    "on_behalf_of": "user://dana@acme.example",
    "agent": "agent://research-loop",
    "host": "host://dana-laptop",
    "task": "task://q4-review",
    "zone": "local:device",
    "incognito": false
  },
  "att": { "on_behalf_of": "verified", "agent": "asserted", "host": "asserted", "task": "asserted", "zone": "asserted" },
  "grants": [
    {
      "actions": ["read"],
      "tables": ["workspace/*"],
      "tenant": { "table": "workspace/documents", "value": "acme-eu" },
      "templates": ["quarterly_rollup"],
      "max_rows": 5000
    }
  ],
  "rev": { "id": "rev://01JBQ7K4F3S9W2X6", "epoch": 7 }
}

Table-pattern coverage:

name                    workspace/*  workspace/documents  *
workspace               no           no                   yes
workspace/documents     yes          yes                  yes
workspace/documents/eu  yes          no                   yes
other/records           no           no                   yes

Narrowing legality, parent to proposed child:

dimension      broader    equal      narrower   absent on child
actions        refused    admitted   admitted   admitted
tables         refused    admitted   admitted   admitted
tenant         refused    admitted   admitted   refused
templates      refused    admitted   admitted   admitted
aggregate      refused    admitted   admitted   inherits parent
max_rows              refused    admitted   admitted   inherits parent
max_duration_ms       refused    admitted   admitted   inherits parent
max_response_bytes    refused    admitted   admitted   inherits parent
expiry         refused    admitted   admitted   inherits parent
horizon        refused    admitted   admitted   inherits parent
on_behalf_of   refused    admitted   refused    inherits parent
incognito      refused    admitted   admitted   inherits parent

An exchange policy:

expected_iss   = "https://login.acme.example/"
expected_aud   = "contextful-console"
tenant_claim   = "org_id"
role_claim     = "roles"
default_grants = []
ttl_secs       = 900
minted_iss     = "contextful://acme-research"
minted_aud     = "contextful://acme-research"

[subject_map]
on_behalf_of = { claim = "sub", template = "user://{}" }

[[role_grants.reader]]
actions   = ["read"]
tables    = ["workspace/*"]
templates = ["quarterly_rollup"]

[[role_grants.loader]]
actions = ["write"]
tables  = ["workspace/documents"]