Verify normalized task approval facts and preserve signed source types
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
5a2099f400
commit
4dd004a225
18 changed files with 756 additions and 60 deletions
210
tests/fixtures/pdp-signature/approval-native-vectors.json
vendored
Normal file
210
tests/fixtures/pdp-signature/approval-native-vectors.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
89
tests/test_task_approval_preparation.py
Normal file
89
tests/test_task_approval_preparation.py
Normal 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']
|
||||
80
tests/test_trusted_successor.py
Normal file
80
tests/test_trusted_successor.py
Normal 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']
|
||||
Loading…
Add table
Add a link
Reference in a new issue