Your first verification, in Python
In this tutorial you run loquitur-verify, the Python verifier, over the test bundles that ship in
its repository. Only the first two steps use the network: the clone, and the installation, which
downloads the two dependencies of the verifier and its build backend unless they are already
cached. Every verification after them runs offline, on your machine, and contacts no service.
You need Git and Python 3.11 or newer. The commands are written for a POSIX shell; the Windows section at the end says what changes in PowerShell.
Get the material
Clone the repository at the release tag. This step needs the network, and so does the installation in the next section.
git clone --branch v0.1.0 https://github.com/loquitur-dev/loquitur-verify-py.gitInstall the verifier
You need Python 3.11 or newer and a checkout of the loquitur-verify-py repository. From the
directory that contains the checkout:
python -m pip install ./loquitur-verify-pycd loquitur-verify-pyloquitur-verify --versionThat installs the verifier and its two dependencies, rfc8785 and cryptography. Network access
is needed only to download them when they are not already cached.
The optional narrative extra, which adds --narrative, needs loquitur-schemas
(>=0.1.0,<0.2.0): install it the same way from its own checkout, before loquitur-verify.
Included from loquitur-verify-py/README.md, “Installation”. MIT OR Apache-2.0.
A PASS, a FAIL and a REVIEW
The first run asks the two questions every run asks: is this the shape of a Loquitur bundle
(structure), and do the files match the digests that the bundle itself declares (integrity). The
tampered bundles answer the second with FAIL. A bundle that leaves one input out answers REVIEW:
nothing failed, but one question could not be answered.
With --json a run prints the whole JSON report, a few kilobytes of it. Look in it for status,
verdict and exit_code. Each expected result below lists the status and the verdict of the
report and the exit code of the process. To see the exit code of the last command, run echo $?
in a POSIX shell or $LASTEXITCODE in PowerShell. For a short summary instead of the report, use
--pretty in place of --json.
With the verifier installed, run these commands from the root of the loquitur-verify-py
checkout. They verify the golden fixtures under tests/fixtures/.
Verify the valid golden fixture:
loquitur-verify tests/fixtures/valid_bundle.zip --jsonExpected result:
status: OFFLINE_OKverdict: PASSexit code: 0Verify a tampered manifest fixture:
loquitur-verify tests/fixtures/tampered_manifest_bundle.zip --jsonExpected result:
status: OFFLINE_FAILverdict: FAILexit code: 1Verify a tampered canonical input fixture:
loquitur-verify tests/fixtures/tampered_canonical_input_bundle.zip --jsonExpected result:
status: OFFLINE_FAILverdict: FAILexit code: 1Verify a bundle missing canonical input:
loquitur-verify tests/fixtures/missing_canonical_input_bundle.zip --jsonExpected result:
status: OFFLINE_REVIEWverdict: REVIEWexit code: 2For a local workflow that accepts a named review reason, --accept-review lists the reasons to
accept; the report is unchanged:
loquitur-verify tests/fixtures/missing_canonical_input_bundle.zip --json --accept-review canonical_input_not_providedExpected result:
status: OFFLINE_REVIEWverdict: REVIEWexit code: 0Only the process exit changes: the report still says OFFLINE_REVIEW, and its exit_code member
stays 2
(report-contract.md
section 4).
Each Expected result block lists status, verdict and the process exit code, then members of
the JSON report named by their path in it.
Included from loquitur-verify-py/README.md, “5-minute quickstart from source”. MIT OR Apache-2.0.
Link the bundle to its public entry
A registry entry is the small public record an issuer publishes. Giving it to the verifier adds the
link check: does this public entry refer to this bundle, and to no other.
The link check runs only when you supply a registry entry:
loquitur-verify tests/fixtures/link_valid_bundle.zip --registry-entry tests/fixtures/link_valid_registry_entry.json --jsonExpected result:
status: OFFLINE_OKverdict: PASSexit code: 0outcome.reason: local_integrity_and_registry_link_checks_passedThe check holds the supplied entry and the bundle’s receipt to their closed contracts, then
compares them with each other and with the bundle, value for value. The entry’s own committed
verdict caps the result: only a PASS entry can reach OFFLINE_OK. A passing link check
means only that the supplied entry is consistent with the supplied bundle, not who signed the
entry or that any registry holds it.
registry-link.md
specifies every comparison. Without --registry-entry, the report’s registry_link is null.
Included from loquitur-verify-py/README.md, “Link check”. MIT OR Apache-2.0.
Check who signed the entry
A trust registry says which keys may sign entries, and a trust root says who may sign the trust
registry. Giving both adds the authorization check. The three runs below reach three of the
outcomes it can have: REVIEW without a trust root, PASS with one, and REVIEW again at an
evaluation time after the trust registry expired. A revoked key, or a signature that does not
verify, gives FAIL.
The commands below continue their lines with \, which a POSIX shell reads. In PowerShell, end
each line with a backtick instead, or write the command on one line.
The authorization check runs only when you supply a registry entry and a trust registry. Three
runs over the same fixtures show the three outcomes it can reach. Run them from the repository
root.
Without a trust root, a run that passes every other authorization condition still reviews: nothing authenticates the trust registry itself.
loquitur-verify tests/fixtures/link_valid_bundle.zip \ --registry-entry tests/fixtures/authorization_valid_registry_entry.json \ --trust-registry tests/fixtures/authorization_quickstart_trust_registry.json \ --evaluation-time 2026-05-20T00:00:00Z \ --jsonExpected result:
status: OFFLINE_REVIEWverdict: REVIEWexit code: 2outcome.reason: trust_registry_root_not_pinnedtrust_registry.authorization_status: AUTHORIZED_ACTIVE_KEYtrust_registry.signature_status: VALID_SIGNATUREtrust_registry.trust_registry_root_signature_status: NO_PINNED_TRUST_ROOTWith --trust-root, the trust registry is authenticated against the root that the trust-root
document names, and the run passes:
loquitur-verify tests/fixtures/link_valid_bundle.zip \ --registry-entry tests/fixtures/authorization_valid_registry_entry.json \ --trust-registry tests/fixtures/authorization_quickstart_trust_registry.json \ --trust-root tests/fixtures/authorization_quickstart_trust_root.json \ --evaluation-time 2026-05-20T00:00:00Z \ --jsonExpected result:
status: OFFLINE_OKverdict: PASSexit code: 0outcome.reason: local_integrity_registry_link_and_trust_checks_passedtrust_registry.trust_registry_root_signature_status: ROOT_SIGNATURE_VALIDThe evaluation time is yours, never one of the issuer’s dates; without --evaluation-time,
authorization is not established. One second after the trust registry expires, the run reviews
rather than fails, because no time attested to the signature exists in this version, and root
authentication is not reached:
loquitur-verify tests/fixtures/link_valid_bundle.zip \ --registry-entry tests/fixtures/authorization_valid_registry_entry.json \ --trust-registry tests/fixtures/authorization_quickstart_trust_registry.json \ --trust-root tests/fixtures/authorization_quickstart_trust_root.json \ --evaluation-time 2026-05-29T12:00:01Z \ --jsonExpected result:
status: OFFLINE_REVIEWverdict: REVIEWexit code: 2outcome.reason: trust_registry_not_currenttrust_registry.authorization_status: TRUST_REGISTRY_EXPIREDtrust_registry.trust_registry_root_signature_status: ROOT_SIGNATURE_NOT_EVALUATEDThe trust root of these runs was generated in memory, used once to sign
authorization_quickstart_trust_registry.json and discarded; nothing can sign with it again.
That root has no authority over anything: it exists to show what --trust-root does. A real
deployment pins a root it obtained out of band, from a party it trusts
(trust-root.md
section 7).
The check first establishes that the supplied trust registry authorizes the entry’s issuer key at
your evaluation time, and only then verifies the entry’s signature with that key. The conditions,
their order and the status each one reports are specified in
trust-registry-authorization.md.
--trust-registry requires --registry-entry; without --trust-registry, the report’s
trust_registry is null.
A key is judged as a key and not by its label. The check stops with TRUST_REGISTRY_AMBIGUOUS_KEY
when more than one trust entry of the issuer carries the entry’s issuer_key_id, and when the trust
entry found shares its secp256k1 point with another trust entry of the registry, whatever their
statuses, issuers and key types and in either SEC1 spelling, compressed or uncompressed. The same
issuer_key_id under another issuer_id with a key of its own is not a duplicate.
Included from loquitur-verify-py/README.md, “Authorization check”. MIT OR Apache-2.0.
What you have shown
With the last PASS you have shown three things about the test bundle: its bytes match the
digests committed to in the registry entry you supplied; the registry entry refers to this bundle;
and the entry’s signature verifies with a key that the trust registry authorizes at the evaluation
time you chose, a trust registry that the root you pinned authenticates.
You have not shown that a valuation is right, who the issuer is in the world, when the entry was
signed, or that any registry other people see holds the entry.
Section 2 of the threat model states exactly what a PASS asserts,
and What a PASS means explains it.
On Windows
In PowerShell, from the directory that contains the checkout:
python -m pip install .\loquitur-verify-pycd loquitur-verify-pyloquitur-verify tests\fixtures\valid_bundle.zip --prettyExpected abbreviated result:
Loquitur offline verification: OFFLINE_OK / PASSregistry: REGISTRY_ENTRY_NOT_PROVIDED (signature_supported=False)The other loquitur-verify commands of the quickstart and of the link and authorization checks
are written for a POSIX shell. They run unchanged in PowerShell, except that the authorization
commands continue lines with \: in PowerShell, end those lines with a backtick instead, or
write the command on one line.
Included from loquitur-verify-py/README.md, “Windows quickstart”. MIT OR Apache-2.0.
Next
- Use the verifier from Python code: the library API.
- Gate a CI job on a verification: the how-to guide.
- Every option of the command line: the command-line reference.
Project site: loquitur.dev
Written for this site; text licensed CC BY 4.0. Included sections keep the licence of their source, named under each.