forgetest: exthost.catalog on the catalog's own repository and its judged index

The address is the catalog repository's index-1.ffi. On the scratch root
the index lists the probe in two versions: 1.1.0 asks for a capability no
firmware has, and read back it is kept and not offered, the offer being
1.0.0. An older index is refused however well signed, and the one kept
stays. An index withdrawing the probe's 1.0.0 makes inspect refuse it in
OpenGlow's words. GET /ext/catalog carries core_checked. The covers widen
from forgeext src/install.c to src/install.*. The test still never
requests the index's address.

Its own module (suite/extcat.py), so only exthost.catalog's fingerprint
moves. forgetest's 500 unit tests pass; the function was read against
_forgeext, fc.get and fc.post, and ctx.check, since no host test runs a
suite function.
This commit is contained in:
ScottW514
2026-09-25 18:22:31 -04:00
parent d2307a899e
commit bfcfd7a376
+63 -31
View File
@@ -5,10 +5,12 @@
"""The catalog: the signed index, on the machine. """The catalog: the signed index, on the machine.
Its own module, for the reason extcore.py gives. The endorsement is shown Its own module, for the reason extcore.py gives. The endorsement, the
on a scratch root under /tmp with a throwaway key standing in for the judgment of each listed version, the index that never goes back, and a
OpenGlow extension key, because only OpenGlow holds the real one; the withdrawn version are shown on a scratch root under /tmp with a throwaway
machine's own root is where every index that key did not sign is refused. key standing in for the OpenGlow extension key, because only OpenGlow holds
the real one; the machine's own root is where every index that key did not
sign is refused.
""" """
import hashlib import hashlib
@@ -23,7 +25,7 @@ import tempfile
from ..catalog import test from ..catalog import test
from .exthost import FWUP, _forgeext, _write from .exthost import FWUP, _forgeext, _write
INDEX_URL = "https://github.com/openglow-org/forgefirm-extensions/releases/latest/download/index.ffi" INDEX_URL = "https://github.com/openglow-org/forgefirm-extensions-catalog/releases/latest/download/index-1.ffi"
STAGE_DIR = "/data/forgefirm/tmp" STAGE_DIR = "/data/forgefirm/tmp"
PROBE_ID, OTHER_ID = "org.example.catprobe", "org.example.catother" PROBE_ID, OTHER_ID = "org.example.catprobe", "org.example.catother"
@@ -58,9 +60,8 @@ def _package(work, id_, key):
return _archive(work, id_, "ForgeFIRM extension", {"manifest.json": json.dumps(m)}, "1.0.0", key) return _archive(work, id_, "ForgeFIRM extension", {"manifest.json": json.dumps(m)}, "1.0.0", key)
def _index(work, entries, key): def _index(work, doc, key, version):
return _archive(work, "index", "ForgeFIRM extension index", {"index.json": json.dumps({"index": 1, "packages": entries})}, return _archive(work, "index-" + version, "ForgeFIRM extension index", {"index.json": json.dumps(doc)}, version, key)
"2026.9.23", key)
def _staged(): def _staged():
@@ -70,24 +71,28 @@ def _staged():
return [] return []
@test("exthost.catalog", title="The catalog: the signed index verified, kept, and endorsing one key for one id", @test("exthost.catalog", title="The catalog: the signed index verified, kept, judged, and endorsing one key for one id",
subsystem="exthost", kind="auto", hardware="api", est_min=2, subsystem="exthost", kind="auto", hardware="api", est_min=2,
covers=[("forgectrl", "src/extpkg.*"), ("forgectrl", "src/main.c"), ("forgeext", "src/index.*"), covers=[("forgectrl", "src/extpkg.*"), ("forgectrl", "src/main.c"), ("forgeext", "src/index.*"),
("forgeext", "src/pkg.*"), ("forgeext", "src/install.c"), ("forgeext", "src/main.c")], ("forgeext", "src/pkg.*"), ("forgeext", "src/install.*"), ("forgeext", "src/main.c")],
description="GET /ext/catalog answers the index the host keeps (or null) and the one address it is fetched " description="GET /ext/catalog answers the index the host keeps (or null), judged against this firmware, and "
"from. On a scratch root, with a throwaway key standing in for the OpenGlow extension key, the " "the one address it is fetched from. On a scratch root, with a throwaway key standing in for the "
"machine's own forgeext keeps an index signed with it, and the author key it names for one id " "OpenGlow extension key, the machine's own forgeext keeps an index signed with it, and the author "
"makes a package of that id signed with it read as community and endorsed, where before it was " "key it names for one id makes a package of that id signed with it read as community and "
"unverified; the same key on another id counts for nothing. On the machine's own root, that " "endorsed, where before it was unverified; the same key on another id counts for nothing. Read "
"index (signed by a key that is not the OpenGlow extension key) is refused in words, a package " "back, each listed version is judged on the machine: one that asks for a capability this "
"handed over as an index is refused by the product gate, and the index kept is left as it was. " "firmware does not have is kept and not offered, and the offer is the newest it runs. An index "
"POST /ext/catalog/get refuses an id with no such form (400), and one the kept index does not " "older than the one kept is refused however well signed, and one that withdraws the probe's "
"list (404, or 409 with no index kept), before anything is fetched. Nothing is left in the " "version makes the machine refuse to install it, in OpenGlow's words. On the machine's own root, "
"staging directory. POST /ext/catalog/refresh is not asked here: GitHub counts every request " "the stand-in's index (signed by a key that is not the OpenGlow extension key) is refused in "
"of the index's address as a download, and that count is the operators'. The refresh (curl, " "words, a package handed over as an index is refused by the product gate, and the index kept "
"https alone and bounded, 502 in curl's words, 409 in the host's, the file removed), the fetch " "is left as it was. POST /ext/catalog/get refuses an id with no such form (400), and one the "
"of a listed package held to its size and SHA-256, and the tiers an install takes from it, " "kept index does not list (404, or 409 with no index kept), before anything is fetched. Nothing "
"are forgectrl's extpkg_test and forgeext's install_test.") "is left in the staging directory. POST /ext/catalog/refresh is not asked here: GitHub counts "
"every request of the index's address as a download, and that count is the operators'. The "
"refresh (curl, https alone and bounded, 502 in curl's words, 409 in the host's, the file "
"removed), the fetch of the offered version held to its size and SHA-256, and the tiers an "
"install takes from it, are forgectrl's extpkg_test and forgeext's install_test.")
def catalog(ctx): def catalog(ctx):
fc = ctx.forgectrl fc = ctx.forgectrl
ev = ctx.evidence ev = ctx.evidence
@@ -100,7 +105,8 @@ def catalog(ctx):
try: try:
st, doc = fc.get("/ext/catalog") st, doc = fc.get("/ext/catalog")
ctx.check(st == 200 and isinstance(doc, dict) and doc.get("url") == INDEX_URL and "index" in doc ctx.check(st == 200 and isinstance(doc, dict) and doc.get("url") == INDEX_URL and "index" in doc
and doc.get("index") == kept.get("index"), "GET /ext/catalog -> %s %s", st, doc) and doc.get("index") == kept.get("index") and "core_checked" in doc,
"GET /ext/catalog -> %s %s", st, doc)
# The endorsement, on a scratch root with a stand-in for the OpenGlow extension key. # The endorsement, on a scratch root with a stand-in for the OpenGlow extension key.
root = os.path.join(work, "root") root = os.path.join(work, "root")
@@ -111,9 +117,13 @@ def catalog(ctx):
data = f.read() data = f.read()
with open(author + ".pub") as f: with open(author + ".pub") as f:
author_pub = f.read().strip() author_pub = f.read().strip()
idx = _index(work, [{"id": PROBE_ID, "name": "catalog probe", "author": "forgetest", "version": "1.0.0", listed = {"version": "1.0.0", "url": "https://example.org/catprobe.ffx", "sha256": hashlib.sha256(data).hexdigest(),
"url": "https://example.org/catprobe.ffx", "sha256": hashlib.sha256(data).hexdigest(), "size": len(data), "capabilities": [], "api": "0.1"}
"size": len(data), "capabilities": [], "key": author_pub}], og) later = dict(listed, version="1.1.0", url="https://example.org/catprobe-1.1.0.ffx",
capabilities=["forgetest.not-a-capability"])
entry = {"id": PROBE_ID, "name": "catalog probe", "author": "forgetest", "key": author_pub,
"versions": [later, listed]}
idx = _index(work, {"index": 1, "packages": [entry]}, og, "2026.923.2")
def scratch(*args): def scratch(*args):
return _forgeext("--root", root, "--official-key", og + ".pub", "--no-reserve", *args) return _forgeext("--root", root, "--official-key", og + ".pub", "--no-reserve", *args)
@@ -121,10 +131,17 @@ def catalog(ctx):
ctx.check(r.get("ok") is True and r.get("tier") == "unverified", "before an index: %s %s", r.get("tier"), r.get("error")) ctx.check(r.get("ok") is True and r.get("tier") == "unverified", "before an index: %s %s", r.get("tier"), r.get("error"))
r = scratch("index-verify", idx) r = scratch("index-verify", idx)
ev["scratch_verify"] = r ev["scratch_verify"] = r
ctx.check(r.get("ok") is True and r.get("packages") == 1 and r.get("version") == "2026.9.23", ctx.check(r.get("ok") is True and r.get("packages") == 1 and r.get("version") == "2026.923.2",
"the stand-in's index is kept on the scratch root: %s", r) "the stand-in's index is kept on the scratch root: %s", r)
listed = ((scratch("index").get("index") or {}).get("packages") or [{}])[0] read = scratch("index")
ctx.check(listed.get("id") == PROBE_ID and len(listed.get("key_id", "")) == 64, "and read back: %s", listed) mine = ((read.get("index") or {}).get("packages") or [{}])[0]
vs = {v.get("version"): v for v in mine.get("versions", [])}
ev["judged"] = {"offer": mine.get("offer"), "1.1.0": vs.get("1.1.0", {}).get("why"),
"core_checked": read.get("core_checked")}
ctx.check(mine.get("id") == PROBE_ID and len(mine.get("key_id", "")) == 64, "and read back: %s", mine)
ctx.check(vs.get("1.1.0", {}).get("usable") is False and "not a capability" in vs.get("1.1.0", {}).get("why", "")
and vs.get("1.0.0", {}).get("usable") is True and mine.get("offer") == "1.0.0",
"judged on the machine: 1.1.0 kept and not offered, 1.0.0 offered: %s", ev["judged"])
r = scratch("inspect", probe) r = scratch("inspect", probe)
ev["endorsed"] = {"tier": r.get("tier"), "endorsed": r.get("endorsed")} ev["endorsed"] = {"tier": r.get("tier"), "endorsed": r.get("endorsed")}
ctx.check(r.get("tier") == "community" and r.get("endorsed") is True, ctx.check(r.get("tier") == "community" and r.get("endorsed") is True,
@@ -133,6 +150,21 @@ def catalog(ctx):
ctx.check(r.get("tier") == "unverified" and r.get("endorsed") is False, ctx.check(r.get("tier") == "unverified" and r.get("endorsed") is False,
"the same key on another id: %s %s", r.get("tier"), r.get("endorsed")) "the same key on another id: %s %s", r.get("tier"), r.get("endorsed"))
# Never back: an older index is refused, and the one kept stays.
r = scratch("index-verify", _index(work, {"index": 1, "packages": []}, og, "2026.923.1"))
ev["older"] = r.get("error")
ctx.check(r.get("ok") is False and "never goes back" in (r.get("error") or ""), "an older index -> %s", r.get("error"))
ctx.check((scratch("index").get("index") or {}).get("version") == "2026.923.2", "and the one kept stays")
# A withdrawn version does not install, from the catalog or from anywhere else.
gone = dict(entry, versions=[later], withdrawn=[{"version": "1.0.0", "reason": "forgetest withdrew it"}])
r = scratch("index-verify", _index(work, {"index": 1, "packages": [gone]}, og, "2026.923.3"))
ctx.check(r.get("ok") is True, "an index withdrawing the probe's version is kept: %s", r.get("error"))
r = scratch("inspect", probe)
ev["withdrawn"] = r.get("error")
ctx.check(r.get("ok") is False and "OpenGlow withdrew %s 1.0.0 from its catalog: forgetest withdrew it" % PROBE_ID
in (r.get("error") or ""), "the withdrawn version -> %s", r.get("error"))
# The machine's own root keeps no index but OpenGlow's. # The machine's own root keeps no index but OpenGlow's.
refused = {} refused = {}
for name, path, words in (("the stand-in's index", idx, "not signed with the OpenGlow extension key"), for name, path, words in (("the stand-in's index", idx, "not signed with the OpenGlow extension key"),