fix(okf): a mapping construct no longer degrades into a string, and there were two routes
T2 gives the mapping class no expressible form by design. Two routes escaped that by parsing "successfully" into the wrong TYPE instead of raising: sources:\n - uri: https://e.com/a -> the string 'uri: https://e.com/a' attester: resource: attesters/x.py -> the string 'resource: attesters/x.py' Only the first was documented (LIMITATIONS.md:43). The inline second colon was found by measurement while closing it -- a real YAML parser refuses that line outright, ours accepted it. Shipping the list half alone would have left a LIMITATIONS rewrite that overclaims. Same consequence either way: a pointer parked in a degraded mapping rides through in a key the `resource` allowlist never inspects, and mode-b import_bundle wrote the merged concept verbatim (WARN). Both now FAIL_SECURE at T2, before the allowlist is reached. The boundary is where YAML puts it, ground-truthed against PyYAML 6.0.3 rather than reasoned: ": " and a trailing ":" are exactly the two shapes where a plain scalar becomes a mapping. A colon carrying neither a space nor a line end opens no mapping -- domain:security and https://e.com:8443/a still parse -- and a quoted scalar is still a scalar. Over-blocking a conformant bundle is itself a failure mode, so the seven admitted shapes get rows of their own. Iron Law: the four rejected rows and both import_bundle rows were written first and seen red (7 failures, each DID NOT RAISE) before okf.py was touched. Suite 792 -> 802. LIMITATIONS stays at 35: the bullet is reworded, not retired -- the restricted grammar is still a limitation, the silent misparse is no longer part of it.
This commit is contained in:
parent
6cd4694613
commit
da30211bc7
3 changed files with 122 additions and 39 deletions
|
|
@ -577,15 +577,57 @@ def test_v02_flat_frontmatter_still_parses(cid, fm):
|
|||
assert parse_frontmatter(f"---\nid: x\n{fm}---\n\nbody\n")[0]["id"] == "x"
|
||||
|
||||
|
||||
def test_one_key_block_sequence_item_is_misparsed_as_a_string():
|
||||
# The documented defect: two keys per item hard-reject (loud, safe), but ONE key
|
||||
# parses "successfully" into the wrong type. A consumer reading
|
||||
# frontmatter["sources"][0].get("uri") gets a string, not a mapping.
|
||||
fm, _ = parse_frontmatter(
|
||||
"---\nid: x\nsources:\n - uri: https://e.com/a\n---\n\nbody\n"
|
||||
)
|
||||
assert fm["sources"] == ["uri: https://e.com/a"], "shape changed — update LIMITATIONS.md"
|
||||
assert not isinstance(fm["sources"][0], dict)
|
||||
# --- the type-confusion defect, closed in 1.1.0 (2026-08-13) ----------------
|
||||
# Was: a mapping construct that the restricted grammar cannot represent degraded
|
||||
# into a STRING instead of failing. Two routes did this, not the one documented.
|
||||
# Ground-truthed against PyYAML 6.0.3: every shape below that we now reject is a
|
||||
# shape a real YAML parser reads as a MAPPING (or refuses outright), and every
|
||||
# shape we still admit is one PyYAML reads as a plain scalar.
|
||||
|
||||
_DEGRADED_TO_STRING = [
|
||||
# (id, frontmatter, what PyYAML 6.0.3 makes of it)
|
||||
("one key per item", "sources:\n - uri: https://e.com/a\n", "[{'uri': ...}]"),
|
||||
("item, trailing colon", "sources:\n - uri:\n", "[{'uri': None}]"),
|
||||
("inline double colon", "attester: resource: attesters/sql_equality.py\n", "parse error"),
|
||||
("top value, trailing colon", "description: see below:\n", "parse error"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cid,fm,yaml_reads_as", _DEGRADED_TO_STRING,
|
||||
ids=[c[0] for c in _DEGRADED_TO_STRING])
|
||||
def test_a_mapping_construct_never_degrades_into_a_string(cid, fm, yaml_reads_as):
|
||||
# The mapping *class* has no expressible form through T2 — so a mapping
|
||||
# construct must RAISE, never parse "successfully" into the wrong type. A
|
||||
# consumer reading frontmatter["sources"][0].get("uri") must not be handed a str.
|
||||
with pytest.raises(OKFFrontmatterError):
|
||||
parse_frontmatter(f"---\nid: x\n{fm}---\n\nbody\n")
|
||||
|
||||
|
||||
_STILL_SCALARS = [
|
||||
# PyYAML reads every one of these as a plain scalar: the colon carries no
|
||||
# space and no line end, so it never opens a mapping. Over-blocking a
|
||||
# conformant bundle is itself a failure mode (brief principle 5).
|
||||
("colon, no space", "tags:\n - domain:security\n", "tags", ["domain:security"]),
|
||||
("url item", "sources:\n - https://e.com/a\n", "sources", ["https://e.com/a"]),
|
||||
("url item with port", "sources:\n - https://e.com:8443/a\n", "sources",
|
||||
["https://e.com:8443/a"]),
|
||||
("url value with port", "resource: https://e.com:8443/a\n", "resource",
|
||||
"https://e.com:8443/a"),
|
||||
("double-quoted item", 'sources:\n - "uri: https://e.com/a"\n', "sources",
|
||||
['"uri: https://e.com/a"']),
|
||||
("single-quoted item", "sources:\n - 'uri: https://e.com/a'\n", "sources",
|
||||
["'uri: https://e.com/a'"]),
|
||||
("quoted top value", 'description: "Note: careful"\n', "description",
|
||||
'"Note: careful"'),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cid,fm,key,expected", _STILL_SCALARS,
|
||||
ids=[c[0] for c in _STILL_SCALARS])
|
||||
def test_scalars_that_merely_contain_a_colon_still_parse(cid, fm, key, expected):
|
||||
# Quotes are retained rather than stripped — a pre-existing divergence from
|
||||
# YAML, pinned here so closing the mapping hole is not read as fixing it.
|
||||
assert parse_frontmatter(f"---\nid: x\n{fm}---\n\nbody\n")[0][key] == expected
|
||||
|
||||
|
||||
def test_relative_resource_pointer_fails_the_allowlist():
|
||||
|
|
@ -595,15 +637,18 @@ def test_relative_resource_pointer_fails_the_allowlist():
|
|||
validate_resource_url(pointer)
|
||||
|
||||
|
||||
def test_pointer_in_one_key_sequence_reaches_the_consumer_tree():
|
||||
# The security-relevant consequence of the misparse above: the pointer never
|
||||
# touches the top-level `resource` key, so the https allowlist never inspects it
|
||||
# and door C admits the concept. Not conformant OKF — a well-formed bundle will
|
||||
# not produce this shape — but mode-b writes the merged concept verbatim.
|
||||
doc = ("---\nid: x\ntype: Attested Computation\n"
|
||||
"attester:\n - resource: attesters/sql_equality.py\n---\n\nbody\n")
|
||||
@pytest.mark.parametrize("cid,carrier", [
|
||||
("block sequence", "attester:\n - resource: attesters/sql_equality.py\n"),
|
||||
("inline double colon", "attester: resource: attesters/sql_equality.py\n"),
|
||||
])
|
||||
def test_pointer_in_a_degraded_mapping_no_longer_reaches_the_consumer_tree(cid, carrier):
|
||||
# The security-relevant consequence, closed at door C. Both carriers put the
|
||||
# pointer in a key the https allowlist never inspects, so while the shape
|
||||
# parsed, mode-b wrote the merged concept verbatim. It now fails secure at T2,
|
||||
# before the allowlist is even reached.
|
||||
doc = f"---\nid: x\ntype: Attested Computation\n{carrier}---\n\nbody\n"
|
||||
result = import_bundle({"computations/x.md": doc})
|
||||
assert result.disposition is Disposition.WARN, "hole closed — update LIMITATIONS.md"
|
||||
assert result.disposition is Disposition.FAIL_SECURE, "hole reopened — see LIMITATIONS.md"
|
||||
|
||||
|
||||
def test_every_route_to_a_mapping_fails_on_a_different_rule():
|
||||
|
|
@ -615,6 +660,7 @@ def test_every_route_to_a_mapping_fails_on_a_different_rule():
|
|||
"flow": "generated: { by: x, at: y }\n",
|
||||
"block": "generated:\n by: x\n",
|
||||
"dotted": "generated.by: x\n",
|
||||
"inline": "generated: by: x\n",
|
||||
}
|
||||
errors = {}
|
||||
for name, fm in routes.items():
|
||||
|
|
@ -624,13 +670,15 @@ def test_every_route_to_a_mapping_fails_on_a_different_rule():
|
|||
assert "indicator" in errors["flow"]
|
||||
assert "nested mappings" in errors["block"]
|
||||
assert "key" in errors["dotted"]
|
||||
assert len(set(errors.values())) == 3, "routes must fail distinctly, not collapse"
|
||||
assert "mapping" in errors["inline"]
|
||||
assert len(set(errors.values())) == 4, "routes must fail distinctly, not collapse"
|
||||
|
||||
|
||||
_BLOCK_LIST_ITEM_SHAPES = [
|
||||
# A consumer called all three "the sources block list"; the parser does not.
|
||||
# The one-key-per-item row lived here until 1.1.0, admitted as the string
|
||||
# "id: a"; it now hard-rejects with the two-key row (_DEGRADED_TO_STRING).
|
||||
("flat scalars", "sources:\n - file://x\n - file://y\n", ["file://x", "file://y"]),
|
||||
("one key per item", "sources:\n - id: a\n", ["id: a"]), # silent misparse
|
||||
("single-element", "verified:\n - human:ktg\n", ["human:ktg"]),
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue