1.2.0-rc.1 / Agreements and authority
Authority history
Retain observations, reject known rollback and distinguish retired or compromised keys.
This local persistence profile complements the draft 3 operational-authority resolver. It preserves the exact authority and policy bytes a particular installation has observed, rejects subsequent rollback and identity redefinition, and records provider route changes. It is implemented in profiles/authority_history.py. It creates no access permission, spending authority, signature, receipt or network trust.
The stored state is one highest observed authority/policy pair per origin, plus historical snapshots. It is not cached authorization. A fresh trusted origin observation and the per-operation resolver remain mandatory whenever the surrounding protocol requires current authority. Historical documents can explain an old signature or decision; they cannot reactivate a retired key, removed delegation or expired grant.
Inputs and trust boundary
AuthorityHistory(db_path, *, clock=None, fault_hook=None) accepts a path to a private SQLite file. Its parent directory must be owned by the running OS user with no group/other permission bits. The file must be private, owned, regular, and not a symlink or hard link. Newly created directories/files use modes 0700/0600. The application must control ancestor directories and prevent same-user file replacement. This is local access control, not encryption or protection from a compromised application or administrator.
observe(snapshot, *, now=None) accepts an AuthoritySnapshot or equivalent in-process object containing:
authority_bytes: exact UTF-8 draft 3 origin authority document bytes.policy_bytes: exact UTF-8 draft 1 native policy bytes.observation: the existing resolver testimony: source, origin, canonical publication URLs, exact document digests, checked time, availability, TLS verification, redirect and revalidation flags.
The supplied observation must come from the application’s trusted HTTPS or colocated transaction adapter. This method does not authenticate arbitrary incoming JSON, perform HTTPS, verify a remote signature, or prove the origin published the supplied bytes. A forged in-process observation would defeat that boundary. Requesting agents must never be allowed to supply it as authority.
Validation inherits the resolver’s strict document shape, size and numeric-token rules. Publication URLs must be the origin’s /odexa-service.json and /odexa.json; digests must match exact bytes. Availability and revalidation must be true; redirects must be false; https observations require TLS verification. Authority and policy must both be valid at the sampled time, and the observation must be between zero and five seconds old. Retirement/revocation timestamps cannot be future effective times. A future active key uses its normal not_before boundary.
clock and now are trusted callables returning exact UTC seconds. Scalar timestamps are rejected by this profile. The time is sampled after acquiring the SQLite write transaction, so a lock wait cannot preserve a stale pre-lock clock. Snapshot testimony is copied before that wait to prevent a mutable caller object changing during the transaction.
Immutable revisions and identity
Each origin has one pinned policy lineage (policy_id). An ordinary observation cannot switch lineage or reset either revision counter. Authority and policy revisions progress independently: a higher policy revision may accompany an unchanged authority revision, provided the authority still validates against it. Revision gaps are allowed; the store does not pretend to possess revisions it never observed.
For each origin and document kind, a revision identifies exact bytes. Even a whitespace-only replacement under the same revision is a conflict. The store rejects any revision lower than that origin’s highest observed revision. The new pair must not regress either counter. All document, identity-pin and high-water updates are one transaction; failure leaves no partial acceptance.
Each observed pair has a snapshot_digest: SHA-256 of the reference implementation’s sorted-key compact JSON serialization of {protocol_version, origin, policy_id, authority_revision, policy_revision, authority_digest, policy_digest}. This is an internal deterministic history reference, not a new general JSON canonicalization standard or signature. Exact source bytes remain stored separately. Only pairs actually observed can be retrieved; the API does not combine an arbitrary historical authority revision with an unrelated policy revision.
Observation time and recorded time cannot move backward within an origin’s retained state. Equal times are allowed. If the host wall clock moves backward, new observations fail closed until the trusted clock catches up or an explicitly separate recovery procedure is adopted. This profile supplies no automatic reset or administrative rollback bypass.
Keys and provider migration
These rules are a deliberately explicit draft 3 history decision, additional to validating one isolated authority document:
| Identity | Retained invariant |
|---|---|
| Origin-scoped service ID | Its issuer cannot change. A new issuer uses a new service ID. |
Key ID (kid), within the origin history |
Public key material, issuer, validity interval and permitted signature types are immutable. A changed interpretation uses a new key ID. |
| Key state | active → retired → revoked or active → revoked; remaining in the same state is allowed. No terminal state becomes active again. |
| Terminal key timestamps | Once recorded, they cannot be changed or removed. A revoked record is thereafter immutable. |
| Origin-revoked public material | Once observed revoked, the same public key cannot appear as active or retired under another ID for that origin. A first snapshot cannot simultaneously call the same material revoked and active/retired. |
| Omitted service/key ID | Locally withdrawn when absent from an accepted newer document; it cannot later resume under the same ID. New identities are required for reintroduction. |
An issuer may expose the same key ID through multiple simultaneously present service IDs, but its identity and state must agree in every occurrence. Revoked-material memory survives removal of the old ID and local restart. It is strictly per origin, not a cross-origin blacklist. Retirement alone does not blacklist the public material under a new ID: the old retired ID remains terminal, while a newly published ID is a distinct current authorization. A service’s capabilities and scopes remain current origin decisions: changing them requires a higher authority revision and a fresh operational evaluation. Stored history does not union permissions from old revisions.
A freshly authenticated, higher-revision origin document may change the base_url of a still-present service with the same service ID and issuer. No manual client allowlist or additional operator approval is required. The audit records the old route, new route, issuer and service ID. A change of provider identity uses a new service ID, issuer and appropriate key identity. Removing the old service withdraws its current operational authority while preserving its history.
Historical receipts, accepted quotes, payer mandates and transaction bindings are never rewritten by this store. An existing quote pinned to the former verification endpoint does not silently migrate: the current resolver’s exact route binding and the payment engine’s immutable accepted bindings must both pass. Reusing an old quote at a new route may therefore fail and require a separately authorized new agreement/quote. History is not a transfer of debt, rights or payment authority.
The store detects identity changes and key state transitions it has itself observed. It is not a global key-compromise registry; it cannot discover an omitted revocation that occurred before installation, establish that another installation saw the same sequence, or prove a never-observed predecessor. Origin compromise, first-observation trust, and restoring the entire database from an old backup require separate operational controls.
API and transaction boundary
observe returns the immutable reference, observation sequence and recorded time, authority_status: "highest_observed", resource_permission_granted: false, and any endpoint changes. Every accepted refresh creates an observation record, including a same-byte refresh; the original documents are not duplicated or altered.
current(origin) returns the highest locally observed pair, exact bytes and latest retained observation. Its name does not assert present network availability, unexpired authority or permission. history(origin, authority_revision, policy_revision) returns an actually observed pair labeled historical_only. Neither method uses the current time to revive or validate authority.
require_current(snapshot) performs a read-only exact comparison against the local high-water mark. It rejects an unknown, byte-conflicting or superseded snapshot. It intentionally does not assert freshness, current origin availability, key validity or operational permission. It is intended as a final local-supersession guard alongside the resolver inside the caller’s protected payment operation:
- Obtain fresh origin documents from the trusted transport and call
observe. - Construct the exact operation and request using their pinned current authority; perform the read-only provider request.
- Re-obtain and observe origin authority as required by the network adapter; authenticate the provider response against the exact current origin-pinned key.
- Inside the payment write transaction, call
require_currentfor that snapshot, then sample trusted time after the history read returns and reevaluate authority, validity and all immutable payment bindings before applying the response. A history read can itself wait on a database lock.
The history store never acquires a payment lock, preventing a history→payment lock order. Its own transactions use SQLite BEGIN IMMEDIATE, rollback journaling and synchronous=FULL. Validation failures roll back. There is no network call while a history write transaction is held.
The guard’s guarantee is equality at its read linearization point. History and payment are separate databases, so it is not one atomic cross-database commit: a concurrent observation can advance history after the guard returns. The caller must not describe this as globally atomic revocation or instantaneous origin-wide consistency. The five-second observation bound, post-lock clock and per-operation checks remain distinct controls. A stronger cross-process atomicity requirement would need a shared transaction/coordinator and its own tests.
observations(origin, after_sequence=0, limit=100) returns a local audit page (maximum 1,000 records). It is neither a signed export nor proof of complete history. Exact documents and observations are retained; no deletion/compaction or disk quota manager is implemented. Operators must preserve the database when moving providers or restoring service, and arrange private backups and storage monitoring. This profile does not implement a remote history endpoint or migrate old receipts.
Verification and limits
Focused verification is in tests/test_authority_history.py: 33 tests passed with the command below. It exercises exact-byte retention and restart, independent revision progress, rollback, same-revision conflicts, key rotation/retirement/revocation, revoked-material aliases, issuer and key redefinition, withdrawal, provider route migration, stale observations after lock waits, concurrent conflicting writers, rollback after partial internal writes, and actual subprocess exits immediately before/after commit. It also checks private file boundaries and read-only historical qualification.
Run from the candidate directory:
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_authority_history.py -v
These tests establish behavior of this local reference component under their simulated clock and transport testimony. They do not establish production deployment, real provider interoperability, a full migration/export protocol, resistance to storage hardware lying about durability, or release-candidate status for the whole protocol.