Align authorization request digests with Go canonical wire profile
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
d5ff4c7421
commit
3ffb6a138e
7 changed files with 399 additions and 9 deletions
47
docs/canonical-request-digest-profile.md
Normal file
47
docs/canonical-request-digest-profile.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Access request digest interoperability profile
|
||||
|
||||
Source-only consumer repair, 2026-10-11. The authoritative algorithm remains
|
||||
Access Engine `api.RequestDigest` / `api.ApprovalBindingDigest` using Go
|
||||
`encoding/json`. The opaque `sha256:` prefix is unchanged. This is not a policy
|
||||
change, native authorization or reinterpretation of a previously served approval.
|
||||
Existing served approvals retain their original immutable request and digest;
|
||||
a mismatching request needs a new request/approval rather than rewriting evidence.
|
||||
|
||||
The consumer matches literal UTF-8 Unicode, JSON control-character escaping,
|
||||
Go's default HTML escaping of `<`, `>` and `&`, and unconditional escaping of
|
||||
U+2028/U+2029, including nested map keys. Invalid Unicode scalar strings fail
|
||||
closed. Struct fields retain their documented order; nested maps sort by key.
|
||||
Empty optional attribute maps and context are omitted, matching Go's struct tags.
|
||||
The optional top-level advisory `purpose` is hashed if present; this does not
|
||||
admit it on enforcement requests, whose Access endpoint rejects that field.
|
||||
|
||||
## Numeric profile
|
||||
|
||||
Access HTTP decoding puts JSON numbers in `map[string]any` into binary64
|
||||
`float64`; arbitrary Python JSON serialization is therefore not equivalent.
|
||||
This consumer admits finite binary64 floats using shortest round-trip digits,
|
||||
Go's fixed notation interval `[1e-6, 1e21)`, normalized scientific exponents,
|
||||
integer-valued float formatting and signed negative zero. Integers outside
|
||||
`[-(2^53-1), 2^53-1]` fail closed to prevent silently hashing the rounded value
|
||||
Go would decode. Non-finite values and unsupported non-JSON values fail closed.
|
||||
Fractional finite values and Unicode catalogue names remain supported. This
|
||||
profile uses the actual HTTP JSON decode path; arbitrary Go-native values such
|
||||
as `json.Number` or pre-HTTP big integers are not claimed to match.
|
||||
|
||||
## Separate fingerprints
|
||||
|
||||
`execution_contract.catalog_target_digest` remains the descriptor-local sorted
|
||||
JSON UTF-8 fingerprint without Go HTML escaping. Approval Engine's claim-binding
|
||||
digest and owner body hashes retain their distinct algorithms. Only full Access
|
||||
request and approval-free Access binding digest computation uses this profile.
|
||||
Removing `context.approval` reproduces the pre-claim request digest; a full
|
||||
claim-bearing request continues to hash the entire approval claim.
|
||||
|
||||
## Verification
|
||||
|
||||
The new tests build a real oracle against the installed Access Engine source
|
||||
and call its exported digest functions after actual `json.Decode`. They cover
|
||||
Unicode/HTML/separators, nested keys, exact approval joins, optional omission,
|
||||
supported integer boundaries and explicit unsupported-number refusals. Existing
|
||||
normal canonical/replay/approval/descriptor fixtures remain unmodified. No Go
|
||||
algorithm, policy package, credentials, default or deployment is changed.
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
{
|
||||
"schema": "source-preparation-receipt/v1",
|
||||
"record_id": "SECRETS-CANONICAL-20261011-R1",
|
||||
"completed_preparation_record_sha256": "c29911e5908ca757d5572530741660438c3d9d1b8962b2ae605cac399ca550f7",
|
||||
"formal_owner_review": false,
|
||||
"native_execution": false,
|
||||
"counterexample_source": "/tmp/access-platform-compiler-review-receipt/docs/evidence/2026-10-11-task-execution/compiler-canonical-counterexample.json",
|
||||
"counterexample_sha256": "eadd598e7ffceaf1431bb5ec3642afa8c230604da1422d324c21dba4d6411c45",
|
||||
"go_oracle_source_sha256": "21e754617c2b82d673cef31b7075f8d177e4367782b63b20b37125587bf9e300",
|
||||
"checks": {
|
||||
"go_oracle_tests": 34,
|
||||
"deterministic_float_samples": 256,
|
||||
"focused_final": 186,
|
||||
"original_fixture_files_modified": false
|
||||
},
|
||||
"prior_observations": [
|
||||
{
|
||||
"kind": "incomplete_stage",
|
||||
"result": "Initial collection errors / missing repo assets; full tracked asset copy corrected staging only."
|
||||
},
|
||||
{
|
||||
"kind": "sandbox_custody",
|
||||
"result": "Initial full staged suite 612 passed, 16 failed, 1 skipped before fractional-support revision. Initial receipt proved only one unchanged-source reproduction. Revision r2 now runs the exact same sixteen failures against unchanged authoritative source, all sixteen reproduce untrusted-owner failure in this sandbox. Full final revised suite is not claimed green."
|
||||
},
|
||||
{
|
||||
"kind": "superseded_preparation",
|
||||
"result": "Initial integer-only float profile superseded before frozen candidate; final admits finite fractional binary64 values."
|
||||
},
|
||||
{
|
||||
"kind": "outside-sandbox-baseline",
|
||||
"result": "Unmodified actual source reproduces TypeError for companion-lane stub missing auth_args; all other custody failures disappear outside sandbox. Exact baseline log retained. Revised test accepts auth_args and verifies identity while preserving each-lane denial/backend refusal test."
|
||||
}
|
||||
],
|
||||
"limits": [
|
||||
"No policy or Go global algorithm change",
|
||||
"No reinterpretation of prior served approvals",
|
||||
"Large Python integers outside safe binary64 interval fail closed",
|
||||
"No native proof or rollout",
|
||||
"Independent review remains required"
|
||||
],
|
||||
"revision": "r3-existing-test-stub-contract",
|
||||
"supersedes_receipt_sha256": "7776fd99177e97a3dfc167e8207d1cdeedecc7bb62ed3f9a53c379946dceb49d",
|
||||
"baseline_verification": {
|
||||
"unchanged_original_code_sha256": "0ec4c3f7304d68abd06a0d08dc459e4f1f7269da0e4258549662be9e57b39fd3",
|
||||
"same_sandbox_environment": true,
|
||||
"observed_tmp_owner_uid": 65534,
|
||||
"executing_uid": 1000,
|
||||
"tests_requested": 16,
|
||||
"same_failures_reproduced": 16,
|
||||
"per_test": [
|
||||
{
|
||||
"test": "tests/test_exec_owner.py::test_actual_bound_child_gets_only_selected_secret_and_fixed_environment",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner.py::test_config_changed_during_fetch_does_not_launch",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner.py::test_binding_changed_after_approval_refuses_before_fetch",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_companion_value_reaches_bound_child_and_is_redacted",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_companion_fetch_failure_starts_no_child",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_each_lane_is_gated_separately_before_backend",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_unresolvable_companion_refuses_before_any_gate",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[missing]",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[extra]",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[env]",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[field]",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[lane]",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[consent]",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[stage]",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_human_control.py::test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
},
|
||||
{
|
||||
"test": "tests/test_integration_companions.py::test_two_lanes_reach_one_bound_owner",
|
||||
"result": "failed",
|
||||
"observed_failure": "exec owner path has an untrusted owner"
|
||||
}
|
||||
],
|
||||
"baseline_observation_path": "/tmp/secrets-canonical-baseline-sixteen-observation.json",
|
||||
"baseline_observation_sha256": "652badb08358ae90ea01d7777bd9c26b8778d38b1dc78b97f1fb4ee36dfd40ab",
|
||||
"baseline_log_path": "/tmp/secrets-canonical-baseline-sixteen-tests.txt",
|
||||
"baseline_log_sha256": "fa88af7b7c14780c63a7609d9da329688a53146aecb98a3b2b1ec736d6baf2c6",
|
||||
"staged_full_log_path": "/tmp/secrets-canonical-go-final-tests.txt",
|
||||
"staged_full_log_sha256": "67339d0b2045081f4df67c61e2388df507413057f417d9dd2d77da207fc0bb3e",
|
||||
"full_final_revised_suite_green": true
|
||||
},
|
||||
"full_final_source_suite": {
|
||||
"exit_code": 0,
|
||||
"environment": "outside filesystem sandbox with explicit approved escalation",
|
||||
"collected": 637,
|
||||
"passed": 636,
|
||||
"skipped": 1,
|
||||
"log_path": "/tmp/secrets-canonical-v3-full-suite.txt",
|
||||
"log_sha256": "b563940c02dfa8b8509dacb48ece3f017c2c841690cda37f0a51e6c7ba2203e0",
|
||||
"collection_log_path": "/tmp/secrets-canonical-v3-collection.txt",
|
||||
"collection_log_sha256": "40689fd881e4b16fffcf693b54e05fa169073e7abd884b44642a252d0236bf77",
|
||||
"baseline_stub_failure_log_path": "/tmp/secrets-canonical-baseline-unsandboxed.txt",
|
||||
"baseline_stub_failure_log_sha256": "92fd5edb071e1b74a0971642226e532b8f2edc9284b411339335f370c3816f1b",
|
||||
"baseline_stub_exit_code": 1,
|
||||
"canonical_code_sha256_unchanged_from_r1": "d58e5fd01ca2470a5d11a6b4f7e9d262a0c1565637e07b35466faa30e4991d14",
|
||||
"native_estate_changes": false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"schema": "bounded-source-preparation/v1",
|
||||
"record_id": "SECRETS-CANONICAL-20261011-R1",
|
||||
"driver": "SECRETS-WP-0013-T01",
|
||||
"status": "completed",
|
||||
"scope": "Local canonical digest consumer repair and source tests only",
|
||||
"formal_owner_assent": false,
|
||||
"native_execution": false,
|
||||
"workplan_task_status_change": false
|
||||
}
|
||||
|
|
@ -9,9 +9,11 @@ from __future__ import annotations
|
|||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
|
@ -166,6 +168,8 @@ def _parse_time(value: object, name: str) -> datetime:
|
|||
def _sorted_map(value: object) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
return {}
|
||||
if not all(isinstance(key, str) for key in value):
|
||||
raise DecisionError("canonical request object keys must be strings")
|
||||
return {key: _canonical_map_value(value[key]) for key in sorted(value)}
|
||||
|
||||
|
||||
|
|
@ -184,7 +188,7 @@ def _subject_ref(value: object) -> dict[str, Any]:
|
|||
for name in ("type", "tenant"):
|
||||
if value.get(name):
|
||||
subject[name] = _required_text(value, name)
|
||||
if value.get("attributes") is not None:
|
||||
if value.get("attributes"):
|
||||
subject["attributes"] = _sorted_map(value.get("attributes"))
|
||||
return subject
|
||||
|
||||
|
|
@ -196,7 +200,7 @@ def _resource_ref(value: object) -> dict[str, Any]:
|
|||
for name in ("type", "system", "tenant"):
|
||||
if value.get(name):
|
||||
resource[name] = _required_text(value, name)
|
||||
if value.get("attributes") is not None:
|
||||
if value.get("attributes"):
|
||||
resource["attributes"] = _sorted_map(value.get("attributes"))
|
||||
return resource
|
||||
|
||||
|
|
@ -221,6 +225,8 @@ def canonical_check_request(request: object) -> dict[str, Any]:
|
|||
)
|
||||
if request.get("policy_version"):
|
||||
canonical["policy_version"] = _required_text(request, "policy_version")
|
||||
if request.get("purpose"):
|
||||
canonical["purpose"] = _required_text(request, "purpose")
|
||||
return canonical
|
||||
|
||||
|
||||
|
|
@ -248,12 +254,61 @@ def digest_material(request: object) -> dict[str, Any]:
|
|||
return material
|
||||
|
||||
|
||||
def _go_json_encode(value: Any) -> str:
|
||||
"""Go encoding/json wire profile, including decoded binary64 numbers.
|
||||
|
||||
Integers outside the exactly representable binary64-safe interval fail
|
||||
closed instead of silently hashing Go's rounded HTTP-decoded value.
|
||||
"""
|
||||
if value is None:
|
||||
return "null"
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value.encode("utf-8", errors="strict")
|
||||
except UnicodeEncodeError as error:
|
||||
raise DecisionError("canonical request requires valid Unicode scalars") from error
|
||||
encoded = json.dumps(value, ensure_ascii=False)
|
||||
for character, escaped in (("<", "\\u003c"), (">", "\\u003e"),
|
||||
("&", "\\u0026"), ("\u2028", "\\u2028"),
|
||||
("\u2029", "\\u2029")):
|
||||
encoded = encoded.replace(character, escaped)
|
||||
return encoded
|
||||
if isinstance(value, int):
|
||||
if abs(value) > 2**53 - 1:
|
||||
raise DecisionError("canonical request integer exceeds safe binary64 interval")
|
||||
return str(value)
|
||||
if isinstance(value, float):
|
||||
if not math.isfinite(value):
|
||||
raise DecisionError("canonical request number must be finite")
|
||||
if value == 0:
|
||||
return "-0" if math.copysign(1, value) < 0 else "0"
|
||||
# Both runtimes use shortest round-tripping binary64 digits. Go uses
|
||||
# fixed notation for [1e-6, 1e21), scientific notation otherwise.
|
||||
text = repr(value)
|
||||
magnitude = abs(value)
|
||||
if 1e-6 <= magnitude < 1e21:
|
||||
fixed = format(Decimal(text), "f")
|
||||
return fixed.rstrip("0").rstrip(".") if "." in fixed else fixed
|
||||
mantissa, exponent = text.lower().split("e")
|
||||
if mantissa.endswith(".0"):
|
||||
mantissa = mantissa[:-2]
|
||||
number = int(exponent)
|
||||
return mantissa + "e" + ("+" if number >= 0 else "-") + str(abs(number))
|
||||
if isinstance(value, list):
|
||||
return "[" + ",".join(_go_json_encode(item) for item in value) + "]"
|
||||
if isinstance(value, dict):
|
||||
if not all(isinstance(key, str) for key in value):
|
||||
raise DecisionError("canonical request object keys must be strings")
|
||||
return "{" + ",".join(_go_json_encode(key) + ":" + _go_json_encode(item)
|
||||
for key, item in value.items()) + "}"
|
||||
raise DecisionError("canonical request contains an unsupported JSON value")
|
||||
|
||||
|
||||
def request_digest(request: object) -> str:
|
||||
canonical = digest_material(request)
|
||||
encoded = json.dumps(
|
||||
canonical, ensure_ascii=False, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
||||
encoded = _go_json_encode(digest_material(request))
|
||||
return "sha256:" + hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
#: The context key an approval claim travels on, per flex-auth's
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ def test_each_lane_is_gated_separately_before_backend(bound, monkeypatch, tmp_pa
|
|||
table = {entry.id: entry, lane.id: lane}
|
||||
monkeypatch.setattr(cli, "get_entry", lambda _dir, cid: table[cid])
|
||||
gated = []
|
||||
def gate(cfg, e, action, evidence, *, fields=()):
|
||||
def gate(cfg, e, action, evidence, *, fields=(), auth_args=None):
|
||||
assert auth_args is args
|
||||
gated.append((e.id, action, fields))
|
||||
if e.id == "test-worker":
|
||||
raise DeliveryError("companion lane denied")
|
||||
|
|
@ -161,8 +162,9 @@ def test_each_lane_is_gated_separately_before_backend(bound, monkeypatch, tmp_pa
|
|||
monkeypatch.setattr(cli, "require_delivery_state", lambda *a: None)
|
||||
monkeypatch.setattr(cli, "_open_backend", lambda *a, **k: pytest.fail("no backend after a lane refusal"))
|
||||
command = data["delivery_config"]["exec_owner"]["command"]
|
||||
args = SimpleNamespace(field=None, catalog=entry.id, command=command, mode="exec-env")
|
||||
with pytest.raises(DeliveryError, match="companion lane denied"):
|
||||
cli.cmd_exec(_cfg(tmp_path), SimpleNamespace(field=None, catalog=entry.id, command=command, mode="exec-env"))
|
||||
cli.cmd_exec(_cfg(tmp_path), args)
|
||||
assert gated == [("test-lane", "exec", ("api_token",)), ("test-worker", "exec", ("worker_token",))]
|
||||
|
||||
|
||||
|
|
|
|||
109
tests/test_go_canonical_parity.py
Normal file
109
tests/test_go_canonical_parity.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Real Access Engine oracle, source-only; no synthetic reimplementation."""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from secrets_engine.authorization import request_digest, approval_binding_digest
|
||||
from secrets_engine.errors import DecisionError
|
||||
from secrets_engine.execution_contract import catalog_target_digest
|
||||
from tests.test_action_authorization import _request
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def go_oracle(tmp_path_factory):
|
||||
access = Path(os.environ.get("ACCESS_ENGINE_SOURCE", "/home/worsch/access-engine"))
|
||||
directory = tmp_path_factory.mktemp("go-canonical")
|
||||
source = directory / "main.go"
|
||||
source.write_text('''package main
|
||||
import ("encoding/json"; "os"; "github.com/netkingdom/access-engine/pkg/api")
|
||||
func main() {
|
||||
var request api.CheckRequest
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&request); err != nil { panic(err) }
|
||||
json.NewEncoder(os.Stdout).Encode([]string{api.RequestDigest(request), api.ApprovalBindingDigest(request)})
|
||||
}
|
||||
''')
|
||||
binary = directory / "oracle"
|
||||
subprocess.run(["go", "build", "-o", str(binary), str(source)], cwd=access, check=True, timeout=120)
|
||||
return binary
|
||||
|
||||
|
||||
def _oracle(binary, request):
|
||||
result = subprocess.run([str(binary)], input=json.dumps(request, ensure_ascii=False, allow_nan=False),
|
||||
text=True, capture_output=True, check=True, timeout=5)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("purpose", ["café<&>", "漢字😀", "separator\u2028paragraph\u2029", 'quoted"\\\n\t', "\\u003c", "ascii"])
|
||||
def test_real_go_unicode_and_approval_join(go_oracle, purpose):
|
||||
request = _request()
|
||||
request["context"]["purpose"] = purpose
|
||||
request["context"]["nested"] = {"ключ<&>": [purpose, None, True, False]}
|
||||
expected = _oracle(go_oracle, request)
|
||||
assert [request_digest(request), approval_binding_digest(request)] == expected
|
||||
before = request_digest(request)
|
||||
request["context"]["approval"] = {"id": "approval:é<&>", "binding": {"pdp_digest": before}}
|
||||
expected = _oracle(go_oracle, request)
|
||||
assert [request_digest(request), approval_binding_digest(request)] == expected
|
||||
assert expected[1] == before and expected[0] != before
|
||||
|
||||
|
||||
@pytest.mark.parametrize("number", [0, 1, -1, 1.0, 0.0, -0.0, 1.5, 1e-7, 1e-6, 1.2e-6, 1e20, 1e21, 1e23, 5e-324, 1.7976931348623157e308, 1000000000000000, 2**53-1, -(2**53-1)])
|
||||
def test_numeric_admitted_profile(go_oracle, number):
|
||||
request = _request()
|
||||
request["context"]["number"] = {"nested": [number]}
|
||||
assert request_digest(request) == _oracle(go_oracle, request)[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("number", [float("nan"), float("inf"), -float("inf"), 2**53, 2**53+1, 10**100])
|
||||
def test_numeric_outside_profile_fails_closed(number):
|
||||
request = _request()
|
||||
request["context"]["number"] = number
|
||||
with pytest.raises(DecisionError):
|
||||
request_digest(request)
|
||||
with pytest.raises(DecisionError):
|
||||
approval_binding_digest(request)
|
||||
|
||||
|
||||
def test_empty_optional_maps_and_approval_only(go_oracle):
|
||||
request = _request()
|
||||
request["subject"]["attributes"] = {}
|
||||
request["resource"]["attributes"] = {}
|
||||
request["context"] = {"approval": {"id": "test"}}
|
||||
assert [request_digest(request), approval_binding_digest(request)] == _oracle(go_oracle, request)
|
||||
request["purpose"] = "readback"
|
||||
assert [request_digest(request), approval_binding_digest(request)] == _oracle(go_oracle, request)
|
||||
|
||||
|
||||
def test_unicode_fingerprint_is_distinct_and_unchanged(go_oracle):
|
||||
request = _request()
|
||||
target = request["context"]["catalog_target"]
|
||||
target["org"] = "café<&>"
|
||||
import hashlib
|
||||
expected = "sha256:" + hashlib.sha256(json.dumps(target, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest()
|
||||
assert catalog_target_digest(target) == expected
|
||||
assert request_digest(request) == _oracle(go_oracle, request)[0]
|
||||
|
||||
|
||||
def test_invalid_unicode_fails_closed():
|
||||
request = _request()
|
||||
request["context"]["purpose"] = "bad\ud800"
|
||||
with pytest.raises(DecisionError):
|
||||
request_digest(request)
|
||||
|
||||
|
||||
def test_binary64_samples_against_real_go(go_oracle):
|
||||
import random
|
||||
import struct
|
||||
import math
|
||||
randomizer = random.Random(20261011)
|
||||
for _ in range(256):
|
||||
number = struct.unpack(">d", randomizer.getrandbits(64).to_bytes(8, "big"))[0]
|
||||
if not math.isfinite(number):
|
||||
continue
|
||||
request = _request()
|
||||
request["context"]["number"] = number
|
||||
assert request_digest(request) == _oracle(go_oracle, request)[0], repr(number)
|
||||
|
|
@ -94,3 +94,14 @@ and confirmed revocation/cleanup. No applied CCR38 replay or broad default flip.
|
|||
Native metadata proof does not certify provision/exec delivery or all T06 scope.
|
||||
|
||||
2026-10-11 source revision v2: qualified T03 dependencies point to the owning design and Platform execution-entitlement task. WP13 owns the missing task-scoped execution seam; WP8-T06 retains production adoption and its full workflow proof. Source publication and fixture checks do not imply native acceptance. Original v1 stage remains unchanged.
|
||||
|
||||
2026-10-11 completed bounded source preparation within T01: repaired the actual
|
||||
Python/Go canonical request digest mismatch for valid Unicode/HTML purpose and
|
||||
nested strings, optional maps and decoded numeric formatting. Full request and
|
||||
approval-free request bindings retain the opaque sha256 prefix; catalogue,
|
||||
claim-binding and body fingerprints remain distinct and unchanged. Real Access
|
||||
Go-oracle cases and unchanged canonical/replay/approval/descriptor fixtures pass.
|
||||
This is source preparation awaiting independent technical review/publication,
|
||||
not a formal owner assent, policy deployment or native execution milestone. T01
|
||||
remains progress; T03 and SECRETS-WP-0008-T06 remain open. Detailed limitations
|
||||
and retained failed test observations are in the source preparation receipt.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue