Verify normalized task approval facts and preserve signed source types
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 17:28:03 +02:00
parent 5a2099f400
commit 4dd004a225
18 changed files with 756 additions and 60 deletions

View file

@ -0,0 +1,210 @@
[
{
"input_target_json": "{\"value\":true}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": true
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":true}}",
"native_digest": "sha256:e1f537daa849e5da023efb5f3d62cc33d94cce20f9ca9c5a708c76e225fd702f"
},
{
"input_target_json": "{\"value\":1}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 1
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1}}",
"native_digest": "sha256:3a3d2da6267e4b917505a7c2247e6ce36fd8ee074eef37251bcc66839b602332"
},
{
"input_target_json": "{\"value\":1.0}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 1.0
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1.0}}",
"native_digest": "sha256:21c2cdb9b161071ebfe361e50703511b2abf6c6f327f153282d247aa92c3898e"
},
{
"input_target_json": "{\"value\":false}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": false
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":false}}",
"native_digest": "sha256:278cbcd013f7923db5258dd860f911d688505a8259f15ccb77983b044f312c74"
},
{
"input_target_json": "{\"value\":0}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 0
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":0}}",
"native_digest": "sha256:e691b8fb55d4bf82a310dee37c045331100c8031e26ea6f51bdcceeeb3b1f20e"
},
{
"input_target_json": "{\"value\":-0.0}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": -0.0
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":-0.0}}",
"native_digest": "sha256:eccde458c686bff67506bb2d94c750e648a311bf23967cb22b37f11e84b6844f"
},
{
"input_target_json": "{\"value\":1e-7}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 1e-07
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-07}}",
"native_digest": "sha256:779042b6c0e675f6aacdcd3a02199aab66f534bb84dd410a95c18657afae563c"
},
{
"input_target_json": "{\"value\":1e-6}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 1e-06
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-06}}",
"native_digest": "sha256:d021f402231fcd59e764b31e6f08a5298ebb328dfd93da58ecde8feb11fde0b5"
},
{
"input_target_json": "{\"value\":1e20}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 1e+20
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e+20}}",
"native_digest": "sha256:14270ffca4ab10e9d9302d52e988b4aa3e2facd494c12a60e2edafef70277a20"
},
{
"input_target_json": "{\"value\":1e21}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 1e+21
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e+21}}",
"native_digest": "sha256:75a60a595b2578430b0be466b6bf9a46b62ddc99bbbcebd7fa0e8d22e3078c0a"
},
{
"input_target_json": "{\"value\":2.5e-7}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 2.5e-07
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":2.5e-07}}",
"native_digest": "sha256:50af37ff75a3e7a22629e746f814995c0c2cd96adb7f1ed5b26999b71d0ac498"
},
{
"input_target_json": "{\"value\":9007199254740993}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"value": 9007199254740993
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":9007199254740993}}",
"native_digest": "sha256:765360dc5a1d949bd114c6b749d83069123d8b4b6b1d939f831428a800f50dab"
},
{
"input_target_json": "{\"html\":\"<>&\", \"unicode\":\"é雪😀\", \"separator\":\"\\u2028\\u2029\"}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"html": "<>&",
"unicode": "é雪😀",
"separator": "

"
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"html\":\"<>&\",\"separator\":\"

\",\"unicode\":\"é雪😀\"}}",
"native_digest": "sha256:c203d934743e2954342038a3746b6c37a0f3101e886ef855b1c6ad824adf5dea"
},
{
"input_target_json": "{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}",
"binding": {
"action": "secrets.kv.destroy",
"target": {
"b": [
true,
1,
1.0,
null,
{
"z": "x",
"a": "y"
}
],
"a": {}
},
"actor": "agt-secrets-engine",
"principal": "bernd",
"purpose": "rotate-exposed-key"
},
"canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"a\":{},\"b\":[true,1,1.0,null,{\"a\":\"y\",\"z\":\"x\"}]}}",
"native_digest": "sha256:56306505c882af6edfae5f3d2a5b42e92737a0859236fe5574a21d1874655d51"
}
]

View file

@ -133,7 +133,7 @@ def test_closed_schema_refuses(mutation):
@pytest.mark.parametrize('action', ['apply','rotate','exec','execute-ccr'])
def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action):
e=entry(); cfg=_Cfg(_token(tmp_path)); calls=[]
e=entry(); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
def fetch(*args,**kwargs): calls.append('claim'); raise AssertionError
contract=None if action=='execute-ccr' else ExecutionContract.parse(descriptor(e))
with pytest.raises(DecisionError):
@ -143,7 +143,7 @@ def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action)
def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
e=entry(); typed=ExecutionContract.parse(descriptor(e)); e=replace(e,path='changed/path')
cfg=_Cfg(_token(tmp_path)); calls=[]
cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='catalog'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=typed,opener=lambda *a,**k:calls.append('claim'))
assert not calls
@ -154,7 +154,7 @@ def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
def test_expired_contract_refuses_before_claim(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat()
cfg=_Cfg(_token(tmp_path)); calls=[]
cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='expired'):
authorize_action(cfg,e,'execute-ccr',execution_contract=ExecutionContract.parse(raw),opener=lambda *a,**k:calls.append('claim'))
assert not calls
@ -200,3 +200,22 @@ def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools
def test_execution_service_declaration_is_fixed_before_issue():
e=entry();contract=ExecutionContract.parse(descriptor(e))
built=request(e,contract)
assert built['context']['service_actor_binding']=={
'version':'proposed-service-task-caller/v1',
'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform'}
assert built['subject']=={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
assert built['resource']['tenant']=='tenant:platform'
assert 'service_actor_binding' not in build_action_request(e,'apply',subject_id='secrets-engine',subject_type='service',purpose='metadata-test')['context']
@pytest.mark.parametrize('subject_id,subject_type',[('other','service'),('secrets-engine','Human'),('user:alice','Human')])
def test_execution_declaration_cannot_assert_foreign_service(subject_id,subject_type):
e=entry();contract=ExecutionContract.parse(descriptor(e))
with pytest.raises(DecisionError,match='service subject'):
build_action_request(e,'execute-ccr',subject_id=subject_id,subject_type=subject_type,purpose='metadata-test',execution_contract=contract)

View file

@ -0,0 +1,89 @@
"""Signed DENY supplies preapproval facts; it never supplies execution authority."""
import copy
import pytest
from secrets_engine.authorization import request_digest
from secrets_engine.errors import DecisionError
from secrets_engine.task_execution_handoff import prepare_task_approval,consume_task_execution
from tests.test_task_execution_handoff import lane,prepared
from tests.signature_support import signature_tools
def normalize(envelope):
envelope['effect']='deny'
envelope['provenance']['registry_snapshot_digest']='sha256:'+'4'*64
envelope['binding']['subject']['attributes']={'roles':['Operator'],'display_name':'registry service'}
envelope['binding']['request_digest']=request_digest(envelope['binding'])
envelope['binding']['approval_binding_digest']=envelope['binding']['request_digest']
def test_signed_registry_normalization_is_detached_and_cannot_consume(lane):
cfg,e,contract,state=lane
state['decision_change']=normalize
result=prepare_task_approval(cfg,e,execution_contract=contract)
output=result.as_owner_handoff()
assert state['calls']==['check']
assert 'approval' not in output['original_request']['context']
assert output['approval_binding_digest']!=output['submitted_request_digest']
assert 'attributes' not in output['original_request']['subject']
assert output['execution_contract']==contract.as_dict()
assert output['native_binding']['target']==output['original_request']['resource']
output['original_request']['context']['purpose']='mutated'
assert result.as_owner_handoff()['original_request']['context']['purpose']!='mutated'
with pytest.raises(DecisionError):consume_task_execution(cfg,result)
assert state['calls']==['check']
@pytest.mark.parametrize('case',['allow','context','descriptor-type','submitted','evaluated','approval-digest','caller','native','future-time','policy'])
def test_signed_normalization_refuses_changed_correspondence(lane,case):
cfg,e,contract,state=lane
def change(envelope):
normalize(envelope)
if case=='allow':envelope['effect']='allow'
elif case=='context':envelope['binding']['context']['purpose']='other'
elif case=='descriptor-type':envelope['binding']['context']['execution_contract']['backend_budget']['ttl_seconds']=120.0
elif case=='submitted':envelope['binding']['submitted_request_digest']='sha256:'+'0'*64
elif case=='evaluated':envelope['binding']['request_digest']='sha256:'+'0'*64
elif case=='approval-digest':envelope['binding']['approval_binding_digest']='sha256:'+'0'*64
elif case=='caller':envelope['provenance']['caller']['mode']='disabled'
elif case=='native':envelope['binding']['resource']['system']='other'
elif case=='future-time':envelope['provenance']['decision_time']='2099-01-01T00:00:00Z'
else:envelope['provenance']['policy_version']='other'
state['decision_change']=change
with pytest.raises(DecisionError):prepare_task_approval(cfg,e,execution_contract=contract)
assert 'claim' not in state['calls'] and 'consume' not in state['calls']
def test_signed_context_numeric_kind_cannot_change(lane):
cfg,e,contract,state=lane
state['decision_change']=lambda envelope:envelope['binding']['context']['execution_contract']['backend_budget'].__setitem__('ttl_seconds',120.0)
with pytest.raises(DecisionError):prepared(lane)
assert 'consume' not in state['calls']
@pytest.mark.parametrize('original,served',[(1,True),(1,1.0),(1.0,1),(False,0)])
def test_shared_original_target_subset_is_type_sensitive(lane,original,served):
from datetime import datetime,timedelta,timezone
from secrets_engine.task_execution_handoff import _task_identity
from secrets_engine.approval_consume import _expected_request
cfg,e,contract,state=lane
request=_expected_request(cfg,e,'execute-ccr',execution_contract=contract)
request['resource']['attributes']['requested_number']=original
binding=copy.deepcopy(request);binding['resource']['attributes']['requested_number']=served
envelope={'binding':binding,'subject':binding['subject'],'resource':binding['resource'],
'provenance':{'caller':{'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine',
'audience':'flex-auth','not_after':(datetime.now(timezone.utc)+timedelta(minutes=20)).isoformat()}}}
with pytest.raises(DecisionError):_task_identity(request,envelope,contract.as_dict(),datetime.now(timezone.utc))
binding['resource']['attributes']['requested_number']=original
binding['resource']['attributes']['legitimate_extra_registry_fact']=1.0
_task_identity(request,envelope,contract.as_dict(),datetime.now(timezone.utc))
def test_old_normalization_record_has_no_permission_window(lane):
cfg,e,contract,state=lane
def old(envelope):
normalize(envelope)
envelope['provenance']['decision_time']='2000-01-01T00:00:00Z'
state['decision_change']=old
normalization=prepare_task_approval(cfg,e,execution_contract=contract)
with pytest.raises(DecisionError):consume_task_execution(cfg,normalization)
assert state['calls']==['check']

View file

@ -0,0 +1,80 @@
"""Owner signature interoperability, independent synthetic admission only."""
import copy
import json
from pathlib import Path
import subprocess
import pytest
from secrets_engine.decision_signature import authenticate_response,load_trust
from secrets_engine.errors import DecisionError
from tests.test_decision_signature import FIXTURE,trust_file,verifier
from tests.signature_support import signature_tools
from tests.test_task_execution_handoff import lane,prepared,consume_task_execution
VECTORS=json.loads((Path(__file__).parent/'fixtures/pdp-signature/approval-native-vectors.json').read_text())
@pytest.mark.parametrize('vector',VECTORS)
def test_fourteen_owner_native_types_retain_signed_bytes(vector,trust_file,signature_tools):
env=json.loads(FIXTURE.read_text());env.pop('signature',None)
env['diagnostics']={'native_vector':'REPLACE_RAW_TARGET'}
raw=json.dumps(env).replace('"REPLACE_RAW_TARGET"',vector['input_target_json']).encode()
signed=subprocess.run([str(signature_tools/'sign')],input=raw,capture_output=True,check=True).stdout
result=authenticate_response(signed,trust=load_trust(trust_file),verifier=signature_tools/'verify')
again=authenticate_response(result.authenticated_json.encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
assert again['diagnostics']['native_vector']==json.loads(vector['input_target_json'])
tampered=signed.replace(b'"action":"rotate"',b'"action":"destroy"')
assert tampered!=signed
with pytest.raises(DecisionError):authenticate_response(tampered,trust=load_trust(trust_file),verifier=signature_tools/'verify')
def test_unknown_provenance_refuses(trust_file,signature_tools):
env=json.loads(FIXTURE.read_text());env['provenance']['unknown_source_authority']=True
with pytest.raises(DecisionError):
authenticate_response(json.dumps(env).encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
def test_exact_original_numeric_lexeme_survives_strict_cas(lane,monkeypatch,signature_tools):
import tests.test_task_execution_handoff as fixtures
cfg,e,contract,state=lane
def raw_sign(env,tools):
env['diagnostics']={'numeric':'LEXEME'}
raw=json.dumps(env).replace('"LEXEME"','1e-7').encode()
return subprocess.run([str(tools/'sign')],input=raw,capture_output=True,check=True).stdout
monkeypatch.setattr(fixtures,'sign_envelope',raw_sign)
authorization=prepared(lane)
assert '1e-7' in authorization._action.artifacts
assert consume_task_execution(cfg,authorization).idempotent is False
def source_evidence(envelope,claim):
from datetime import datetime,timezone
return {'source_id':'synthetic-source','source_revision':'synthetic-revision',
'configuration_digest':'sha256:'+'2'*64,'claim_digest':'sha256:'+'3'*64,
'native_binding_digest':claim['binding']['digest'],
'observed_at':datetime.now(timezone.utc).isoformat(),
'validated_at':datetime.now(timezone.utc).isoformat(),
'not_after':envelope['lifetime']['expires_at']}
def test_signed_source_evidence_keeps_original_context_and_cas(lane):
cfg,e,contract,state=lane
state['decision_change']=lambda env:env['provenance'].__setitem__('approval_observation',source_evidence(env,state['claim']))
authorization=prepared(lane)
snapshot=json.loads(authorization._action.artifacts)
assert snapshot['envelope']['binding']['context']==snapshot['request']['context']
assert snapshot['request']['context']['approval']==state['claim']
assert consume_task_execution(cfg,authorization).idempotent is False
@pytest.mark.parametrize('field,value',[
('source_id',''),('source_revision',''),('configuration_digest','wrong'),('claim_digest','wrong'),
('native_binding_digest','sha256:'+'0'*64),('observed_at','2099-01-01T00:00:00Z'),
('validated_at','2099-01-01T00:00:00Z'),('not_after','2000-01-01T00:00:00Z')])
def test_signed_invalid_source_evidence_refuses_before_cas(lane,field,value):
cfg,e,contract,state=lane
def change(env):
evidence=source_evidence(env,state['claim']);evidence[field]=value
env['provenance']['approval_observation']=evidence
state['decision_change']=change
with pytest.raises(DecisionError):prepared(lane)
assert 'consume' not in state['calls']