verigate-verify 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rudimentall1
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: verigate-verify
3
+ Version: 0.1.0
4
+ Summary: Offline verifier for Verigate authority proofs: check a portable proof with only the issuer's public key.
5
+ Author: rudimentall1
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rudimentall1/verigate
8
+ Project-URL: Source, https://github.com/rudimentall1/verigate/tree/main/packaging/verigate-verify
9
+ Project-URL: Issues, https://github.com/rudimentall1/verigate/issues
10
+ Project-URL: Proof format, https://github.com/rudimentall1/verigate/blob/main/docs/verigate-authority-proof-v1.md
11
+ Project-URL: Threat model, https://github.com/rudimentall1/verigate/blob/main/docs/threat-model.md
12
+ Keywords: ai-agents,authorization,verification,ed25519,audit,x402
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: cryptography<51,>=48.0.1
21
+ Dynamic: license-file
22
+
23
+ # verigate-verify
24
+
25
+ Offline verifier for [Verigate](https://github.com/rudimentall1/verigate) authority proofs.
26
+
27
+ Verigate decides, per action, whether an AI agent had the authority to act, issues a one-time signed
28
+ authorization, and exports a portable proof. This package is the standalone verifier for that proof:
29
+ **you need only the proof file and the issuer's Ed25519 public key.** No Verigate server, database or API.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install verigate-verify
35
+ ```
36
+
37
+ ## Use
38
+
39
+ ```bash
40
+ verigate-verify proof.json --public-key issuer.pub
41
+ verigate-verify proof.json --public-key issuer.pub --format json
42
+ # or
43
+ python -m verigate_verify proof.json --public-key issuer.pub
44
+ ```
45
+
46
+ The result is `VALID` or `INVALID` with a reason (for example `proof package digest mismatch` if the
47
+ evidence was altered). Exit code is 0 for a valid proof and non-zero otherwise.
48
+
49
+ A sample proof and issuer key are in the repository under `examples/authority-proof/`.
50
+
51
+ ## What it does and does not tell you
52
+
53
+ It checks the package digest, the evidence manifest, the issuer signature against the public key you trust,
54
+ the evidence graph and the authority protocol assertions. It does not decide whether the issuer's key is the
55
+ right one to trust (you must obtain the public key through a channel you trust), and it does not prove that no
56
+ other actions were taken. See the [threat model](https://github.com/rudimentall1/verigate/blob/main/docs/threat-model.md).
57
+
58
+ ## Source
59
+
60
+ This is the same code as `tools/verigate_proof_verifier.py` in the main repository. MIT licensed.
@@ -0,0 +1,38 @@
1
+ # verigate-verify
2
+
3
+ Offline verifier for [Verigate](https://github.com/rudimentall1/verigate) authority proofs.
4
+
5
+ Verigate decides, per action, whether an AI agent had the authority to act, issues a one-time signed
6
+ authorization, and exports a portable proof. This package is the standalone verifier for that proof:
7
+ **you need only the proof file and the issuer's Ed25519 public key.** No Verigate server, database or API.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install verigate-verify
13
+ ```
14
+
15
+ ## Use
16
+
17
+ ```bash
18
+ verigate-verify proof.json --public-key issuer.pub
19
+ verigate-verify proof.json --public-key issuer.pub --format json
20
+ # or
21
+ python -m verigate_verify proof.json --public-key issuer.pub
22
+ ```
23
+
24
+ The result is `VALID` or `INVALID` with a reason (for example `proof package digest mismatch` if the
25
+ evidence was altered). Exit code is 0 for a valid proof and non-zero otherwise.
26
+
27
+ A sample proof and issuer key are in the repository under `examples/authority-proof/`.
28
+
29
+ ## What it does and does not tell you
30
+
31
+ It checks the package digest, the evidence manifest, the issuer signature against the public key you trust,
32
+ the evidence graph and the authority protocol assertions. It does not decide whether the issuer's key is the
33
+ right one to trust (you must obtain the public key through a channel you trust), and it does not prove that no
34
+ other actions were taken. See the [threat model](https://github.com/rudimentall1/verigate/blob/main/docs/threat-model.md).
35
+
36
+ ## Source
37
+
38
+ This is the same code as `tools/verigate_proof_verifier.py` in the main repository. MIT licensed.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "verigate-verify"
7
+ version = "0.1.0"
8
+ description = "Offline verifier for Verigate authority proofs: check a portable proof with only the issuer's public key."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "rudimentall1" }]
14
+ keywords = ["ai-agents", "authorization", "verification", "ed25519", "audit", "x402"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Security :: Cryptography",
20
+ ]
21
+ dependencies = ["cryptography>=48.0.1,<51"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/rudimentall1/verigate"
25
+ Source = "https://github.com/rudimentall1/verigate/tree/main/packaging/verigate-verify"
26
+ Issues = "https://github.com/rudimentall1/verigate/issues"
27
+ "Proof format" = "https://github.com/rudimentall1/verigate/blob/main/docs/verigate-authority-proof-v1.md"
28
+ "Threat model" = "https://github.com/rudimentall1/verigate/blob/main/docs/threat-model.md"
29
+
30
+ [project.scripts]
31
+ verigate-verify = "verigate_verify._verifier:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """Offline verifier for Verigate authority proofs."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from ._verifier import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+ import argparse,base64,hashlib,json
4
+ from pathlib import Path
5
+ from cryptography.exceptions import InvalidSignature
6
+ from cryptography.hazmat.primitives import serialization
7
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
8
+ PROTOCOL='verigate-authority-proof-v1'; PKG='verigate-proof-package-v1'; MEDIA='application/vnd.verigate.proof-package+json'; ZERO='0'*64
9
+ REQUIRED_AUTHORITY_TYPES=('action_intent','execution_authorization','execution_receipt','outcome_claim','observed_effect','outcome_attestation','authority_event','authority_state','authority_state_after','authority_ledger','genesis_authority','capability')
10
+ def canon(x): return json.dumps(x,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode()
11
+ def sha(x): return hashlib.sha256(canon(x)).hexdigest()
12
+ def b64(x): return base64.b64decode(x,validate=True)
13
+ def ref(n): return f"{n['type']}:{n['id']}"
14
+ def node(p,k):
15
+ x=[n for n in p.get('nodes',[]) if n.get('type')==k]; return x[0] if len(x)==1 else None
16
+ def edge(p,a,r,b): return any(e.get('from')==a and e.get('relation')==r and e.get('to')==b for e in p.get('edges',[]))
17
+ def merkle(xs):
18
+ if not xs:return hashlib.sha256(b'VERIGATE-EVIDENCE-MERKLE:v1:empty').hexdigest()
19
+ xs=sorted(xs)
20
+ while len(xs)>1:
21
+ if len(xs)%2:xs.append(xs[-1])
22
+ xs=[hashlib.sha256(('pair:'+xs[i]+':'+xs[i+1]).encode()).hexdigest() for i in range(0,len(xs),2)]
23
+ return xs[0]
24
+ def root(p):
25
+ xs=[hashlib.sha256(('node:'+sha(n['data'])).encode()).hexdigest() for n in p.get('nodes',[])]
26
+ xs += [hashlib.sha256(('edge:'+sha(e)).encode()).hexdigest() for e in p.get('edges',[])]
27
+ return merkle(xs)
28
+ def bad(r,c): return {'valid':False,'protocol':PROTOCOL,'reason':r,'checks':c}
29
+ def assertions(m):
30
+ p=m['payload']; kinds=['identity','capability','action_intent','authority_state','genesis_authority','decision','execution_authorization','execution_receipt','outcome_claim','observed_effect','outcome_attestation','authority_event','authority_state_after','authority_ledger']; n={k:node(p,k) for k in kinds}
31
+ if any(v is None for v in n.values()): return False,'authority protocol evidence incomplete'
32
+ R={k:ref(v) for k,v in n.items()}; ap=n['execution_authorization']['data']['payload']; rp=n['execution_receipt']['data']['payload']; cp=n['outcome_claim']['data']; oe=n['observed_effect']['data']; ep=n['authority_event']['data']; sp=n['authority_state_after']['data']; ent=n['authority_ledger']['data'].get('entries',[])
33
+ def A(i,k,s,pr,o,e):
34
+ x={'id':i,'kind':k,'subject':s,'predicate':pr,'object':o,'evidence':e}; x['sha256']=hashlib.sha256(canon(x)).hexdigest(); return x
35
+ exp=[A('A1','identity',R['identity'],'possessed_capability',R['capability'],[R['identity'],R['capability']]),A('A2','intent',R['action_intent'],'proposed_by',n['identity']['data'].get('agent_id',p.get('agent_id')),[R['action_intent'],R['identity']]),A('A3','authority',R['authority_state'],'permitted',R['action_intent'],[R['authority_state'],R['action_intent'],R['capability']]),A('A4','authorization',R['execution_authorization'],'derived_from',R['authority_state'],[R['decision'],R['execution_authorization'],R['authority_state'],R['genesis_authority']]),A('A5','execution',R['execution_receipt'],'consumed_authorization',R['execution_authorization'],[R['execution_authorization'],R['execution_receipt']]),A('A6','observation',R['outcome_claim'],'observes',R['execution_receipt'],[R['execution_receipt'],R['outcome_claim'],R['observed_effect'],R['outcome_attestation']]),A('A7','learning',R['authority_event'],'justified_by',R['outcome_claim'],[R['outcome_claim'],R['authority_event']]),A('A8','learning',R['authority_state_after'],'produced_by',R['authority_event'],[R['authority_event'],R['authority_state_after']]),A('A9','integrity',R['authority_state_after'],'ledger_bound',R['authority_ledger'],[R['authority_state_after'],R['authority_ledger']])]
36
+ astate=n['authority_state']['data']; capdata=n['capability']['data']; gd=n['genesis_authority']['data']
37
+ authority_digest=sha({'authority_id':f"{ap.get('agent_id')}:{n['capability']['id']}:{astate.get('ledger_head_hash') or sha(astate)}",'agent_id':ap.get('agent_id'),'identity_id':ap.get('identity_id',''),'capability_id':n['capability']['id'],'capability_version':capdata.get('version',1),'capability_sha256':ap.get('capability_sha256',''),'state':astate.get('state'),'multiplier':float(astate.get('multiplier')),'epoch':astate.get('epoch',0),'ledger_head_hash':astate.get('ledger_head_hash',''),'effective_from':float(astate.get('evaluated_at',0.0)),'expires_at':None,'parent_authority_id':None,'evidence_refs':(),'metadata':{'source':'core.authority_state.AuthoritySnapshot','successes':astate.get('successes',0),'adverse_events':astate.get('adverse_events',0),'critical_events':astate.get('critical_events',0),'reason':astate.get('reason','')}})
38
+ checks=[edge(p,R['identity'],'AUTHENTICATES',R['action_intent']),edge(p,R['capability'],'AUTHORIZES',R['action_intent']),ap.get('authority_state_sha256')==sha(n['authority_state']['data']),gd.get('authority_digest')==authority_digest,edge(p,R['decision'],'MINTS',R['execution_authorization']),edge(p,R['execution_authorization'],'PRODUCES',R['execution_receipt']),rp.get('authorization_id')==ap.get('authorization_id'),edge(p,R['execution_receipt'],'OBSERVED_BY',R['outcome_claim']),edge(p,R['outcome_claim'],'SUPPORTED_BY',R['observed_effect']),oe.get('authorization_id')==ap.get('authorization_id'),oe.get('action_sha256')==cp.get('action_sha256'),oe.get('evidence_ref')==cp.get('evidence_ref'),oe.get('result_sha256')==cp.get('result_sha256'),cp.get('observed_effect_sha256')==sha(oe),oe.get('observation_sha256')==sha({'verifier_type':oe.get('verifier_type'),'evidence_kind':oe.get('evidence_kind'),'effect_status':oe.get('effect_status'),'authorization_id':oe.get('authorization_id'),'action_sha256':oe.get('action_sha256'),'observed_at':oe.get('observed_at'),'evidence_ref':oe.get('evidence_ref'),'result_sha256':oe.get('result_sha256'),'observation':oe.get('observation')}),edge(p,R['outcome_attestation'],'ATTESTS',R['outcome_claim']),edge(p,R['outcome_claim'],'INFORMS',R['authority_event']),edge(p,R['authority_event'],'TRANSITIONS_TO',R['authority_state_after']),ep.get('evidence_ref')==cp.get('claim_id'),sp.get('source_event_id')==ep.get('event_id'),len([x for x in ent if x.get('event_id')==ep.get('event_id')])==1 and sp.get('ledger_head_hash')==next((x.get('event_hash') for x in ent if x.get('event_id')==ep.get('event_id')),None)]
39
+ return (exp==p.get('authority_assertions') and all(checks) and sha(exp)==p.get('authority_assertions_sha256'),'signed authority assertions do not match evidence' if exp!=p.get('authority_assertions') else 'authority protocol preconditions failed')
40
+ def load_trusted_key(path):
41
+ raw=Path(path).read_bytes()
42
+ public_key=serialization.load_pem_public_key(raw)
43
+ if not isinstance(public_key,Ed25519PublicKey): raise ValueError('issuer public key must be Ed25519')
44
+ return base64.b64encode(public_key.public_bytes(serialization.Encoding.Raw,serialization.PublicFormat.Raw)).decode()
45
+
46
+ def verify_historical(snapshot,entries,policy,expected_head):
47
+ try:
48
+ eval_at=float(snapshot['evaluated_at']); hist=float(snapshot['history_start_at']); win=float(policy['window_seconds']); p=int(policy['probation_successes']); s=int(policy['standard_successes']); l=int(policy['limited_adverse_events']); c=int(policy['suspension_critical_events']); mult={'PROBATION':float(policy['probation_multiplier']),'LIMITED':float(policy['limited_multiplier']),'STANDARD':float(policy['standard_multiplier']),'ELEVATED':float(policy['elevated_multiplier']),'SUSPENDED':0.0}
49
+ except Exception:return False,'historical authority policy or snapshot is malformed'
50
+ if hist!=eval_at-win:return False,'historical authority history boundary is not policy-derived'
51
+ if snapshot.get('authority_policy_sha256')!=sha(policy):return False,'historical authority policy binding mismatch'
52
+ committed=[]
53
+ if expected_head!='0'*64:
54
+ for x in entries:
55
+ committed.append(x)
56
+ if x.get('event_hash')==expected_head:break
57
+ if not committed or committed[-1].get('event_hash')!=expected_head:return False,'historical authority ledger head is not present'
58
+ for x in entries[len(committed):]:
59
+ e=x.get('event') if isinstance(x,dict) else None
60
+ if not isinstance(e,dict):return False,'historical authority future event is malformed'
61
+ t=e.get('occurred_at')
62
+ if not isinstance(t,(int,float)) or isinstance(t,bool):return False,'historical authority future event timestamp is invalid'
63
+ if float(t)<=eval_at:return False,'historical authority head is inconsistent with evaluation time'
64
+ succ=adv=crit=0; advset={'EXECUTION_FAILED','AUTHORIZATION_REJECTED','POLICY_VIOLATION','TAMPER_DETECTED'}; critset={'TAMPER_DETECTED','POLICY_VIOLATION'}
65
+ for row in committed:
66
+ e=row.get('event') if isinstance(row,dict) else None
67
+ if not isinstance(e,dict):return False,'historical authority ledger contains malformed event'
68
+ t=e.get('occurred_at')
69
+ if not isinstance(t,(int,float)) or isinstance(t,bool):return False,'historical authority event timestamp is invalid'
70
+ if hist<=float(t)<=eval_at:
71
+ succ+=1 if e.get('event_type')=='EXECUTION_CONFIRMED' else 0; adv+=1 if e.get('event_type') in advset else 0; crit+=1 if e.get('event_type') in critset else 0
72
+ if (snapshot.get('successes'),snapshot.get('adverse_events'),snapshot.get('critical_events'))!=(succ,adv,crit):return False,'historical authority counters do not match ledger history'
73
+ if crit>=c:state='SUSPENDED';reason='critical authority event observed'
74
+ elif adv>=l:state='LIMITED';reason='adverse outcome threshold reached'
75
+ elif succ>=s and adv<=1:state='ELEVATED';reason='verified success threshold reached with bounded adverse history'
76
+ elif succ>=p and adv<=1:state='STANDARD';reason='probation success threshold reached'
77
+ else:state='PROBATION';reason='insufficient verified history for broader authority'
78
+ if snapshot.get('state')!=state:return False,'historical authority state is not deterministically justified'
79
+ if snapshot.get('multiplier')!=mult[state]:return False,'historical authority multiplier is inconsistent'
80
+ if snapshot.get('reason')!=reason:return False,'historical authority reason is inconsistent'
81
+ return True,'valid historical authority'
82
+
83
+ def verify_learning(claim,att,ev,before,after,entries,policy,expected_head):
84
+ if claim.get('status')=='SUCCEEDED': et='EXECUTION_CONFIRMED'
85
+ elif claim.get('status')=='FAILED': et='EXECUTION_FAILED'
86
+ else: return False,'authority lifecycle learning requires a terminal independent outcome'
87
+ agent=claim.get('agent_id'); cap=before.get('capability_id'); cid=claim.get('claim_id')
88
+ if ev.get('agent_id')!=agent or ev.get('capability_id')!=cap or ev.get('event_type')!=et or ev.get('evidence_ref')!=cid:return False,'authority event does not match canonical outcome'
89
+ if ev.get('event_id')!=hashlib.sha256(f"{agent}:{cap}:{et}:{cid}".encode()).hexdigest():return False,'authority event id is not deterministic'
90
+ meta=ev.get('metadata') if isinstance(ev.get('metadata'),dict) else {}
91
+ if 'outcome_attestation_id' in meta and meta.get('outcome_attestation_id')!=att.get('attestation_id'):return False,'authority event is not bound to outcome attestation'
92
+ if 'attestation_type' in meta and meta.get('attestation_type')!=att.get('attestor_type'):return False,'authority event attestation type is inconsistent'
93
+ try:
94
+ eval_at=float(after['evaluated_at']); hist=float(after['history_start_at']); win=float(policy['window_seconds'])
95
+ thresholds=(int(policy['probation_successes']),int(policy['standard_successes']),int(policy['limited_adverse_events']),int(policy['suspension_critical_events']))
96
+ mult={'PROBATION':float(policy['probation_multiplier']),'LIMITED':float(policy['limited_multiplier']),'STANDARD':float(policy['standard_multiplier']),'ELEVATED':float(policy['elevated_multiplier']),'SUSPENDED':0.0}
97
+ except Exception:return False,'authority policy or post-learning snapshot is malformed'
98
+ if hist!=eval_at-win:return False,'post-learning authority history boundary is not policy-derived'
99
+ if after.get('authority_policy_sha256')!=sha(policy):return False,'post-learning authority policy binding mismatch'
100
+ if after.get('agent_id')!=agent or after.get('capability_id')!=cap:return False,'post-learning authority snapshot identity mismatch'
101
+ eh=sha(ev); matches=[x for x in entries if isinstance(x,dict) and x.get('event_id')==ev.get('event_id')]
102
+ if len(matches)!=1 or matches[0].get('event_hash')!=eh or matches[0].get('event')!=ev:return False,'authority ledger event is not exact canonical learning evidence'
103
+ if after.get('ledger_head_hash')!=eh:return False,'post-learning authority is not bound to the learning event hash'
104
+ try: event_at=float(ev['occurred_at']); before_at=float(before['evaluated_at'])
105
+ except Exception:return False,'authority learning timestamps are malformed'
106
+ if event_at>eval_at or event_at<hist:return False,'learning event is outside the post-learning authority evaluation window'
107
+ if before_at>eval_at:return False,'post-learning authority is evaluated before the historical snapshot'
108
+ if before.get('ledger_head_hash')!=expected_head:return False,'historical authority snapshot is not bound to authorization head'
109
+ prior=next((x.get('sequence') for x in entries if x.get('event_hash')==expected_head),0)
110
+ seq=matches[0].get('sequence')
111
+ if expected_head!='0'*64 and prior==0:return False,'authorization ledger head is not present in authority ledger'
112
+ if not isinstance(seq,int) or seq<=prior:return False,'learning event does not extend authorization ledger head'
113
+ for x in entries:
114
+ if isinstance(x,dict) and isinstance(x.get('sequence'),int) and x['sequence']>seq:
115
+ fe=x.get('event')
116
+ if isinstance(fe,dict) and float(fe.get('occurred_at',eval_at+1))<=eval_at:return False,'post-learning snapshot is not bound to ledger head at evaluation time'
117
+ successes=adverse=critical=0; adv={'EXECUTION_FAILED','AUTHORIZATION_REJECTED','POLICY_VIOLATION','TAMPER_DETECTED'}; crit={'TAMPER_DETECTED','POLICY_VIOLATION'}
118
+ for row in entries:
119
+ e=row.get('event') if isinstance(row,dict) else None
120
+ if not isinstance(e,dict):return False,'authority ledger contains malformed event'
121
+ t=e.get('occurred_at')
122
+ if not isinstance(t,(int,float)) or isinstance(t,bool):return False,'authority event timestamp is invalid'
123
+ if hist<=float(t)<=eval_at:
124
+ successes+=1 if e.get('event_type')=='EXECUTION_CONFIRMED' else 0; adverse+=1 if e.get('event_type') in adv else 0; critical+=1 if e.get('event_type') in crit else 0
125
+ if (after.get('successes'),after.get('adverse_events'),after.get('critical_events'))!=(successes,adverse,critical):return False,'post-learning authority counters do not match ledger history'
126
+ p,s,l,c=thresholds; prev=before.get('state')
127
+ if prev=='SUSPENDED': state='SUSPENDED'; reason='suspended until an explicit authority reset'
128
+ elif critical>=c: state='SUSPENDED'; reason='critical authority event observed'
129
+ elif adverse>=l: state='LIMITED'; reason='adverse outcome threshold reached'
130
+ elif successes>=s and adverse<=1: state='ELEVATED'; reason='verified success threshold reached with bounded adverse history'
131
+ elif successes>=p and adverse<=1: state='STANDARD'; reason='probation success threshold reached'
132
+ else: state='PROBATION'; reason='insufficient verified history for broader authority'
133
+ if after.get('state')!=state:return False,'post-learning authority state is not deterministically justified'
134
+ if after.get('multiplier')!=mult[state]:return False,'post-learning authority multiplier is inconsistent'
135
+ if after.get('reason')!=reason:return False,'post-learning authority reason is inconsistent'
136
+ return True,'valid deterministic learning transition'
137
+
138
+ def verify(proof,key):
139
+ c={}; d=json.loads(Path(proof).read_text(encoding='utf-8')); trusted=load_trusted_key(key); pkg=d.get('package'); ph=d.get('package_sha256')
140
+ c['package_integrity']={'valid':isinstance(pkg,dict) and sha(pkg)==ph}
141
+ if not c['package_integrity']['valid']:return bad('package digest mismatch',c)
142
+ m=pkg.get('manifest'); p=m.get('payload') if isinstance(m,dict) else None
143
+ c['manifest_integrity']={'valid':isinstance(m,dict) and pkg.get('manifest_sha256')==sha(m)}
144
+ if not c['manifest_integrity']['valid']:return bad('manifest digest mismatch',c)
145
+ if pkg.get('protocol')!=PKG or pkg.get('media_type')!=MEDIA or not isinstance(p,dict) or p.get('proof_profile')!='authority_lifecycle':return bad('unsupported proof package',c)
146
+ if pkg.get('proof_protocol')!=PROTOCOL or pkg.get('protocol')!=PKG: return bad('package protocol metadata mismatch',c)
147
+ if not pkg.get('self_contained') or pkg.get('node_count')!=len(p.get('nodes',[])) or pkg.get('edge_count')!=len(p.get('edges',[])): return bad('package graph metadata mismatch',c)
148
+ inventory=[{'type':n.get('type'),'id':n.get('id'),'sha256':n.get('sha256')} for n in p.get('nodes',[])]
149
+ if pkg.get('node_inventory')!=inventory or pkg.get('graph_root_digest')!=p.get('root_digest'): return bad('package graph inventory mismatch',c)
150
+ if pkg.get('assertion_set_sha256')!=sha(pkg.get('assertions',[])): return bad('package assertion-set digest mismatch',c)
151
+ if p.get('proof_profile')=='authority_lifecycle' and pkg.get('required_artifact_types')!=list(REQUIRED_AUTHORITY_TYPES): return bad('authority lifecycle required artifact metadata mismatch',c)
152
+ if p.get('proof_profile')=='authority_lifecycle' and (pkg.get('assertions')!=p.get('authority_assertions') or pkg.get('assertion_set_sha256')!=p.get('authority_assertions_sha256')): return bad('package assertions are not bound to signed manifest',c)
153
+ c['trust_anchor']={'valid':m.get('issuer_public_key_b64')==trusted}
154
+ if not c['trust_anchor']['valid']:return bad('issuer public key is not trusted',c)
155
+ try: pub=Ed25519PublicKey.from_public_bytes(b64(trusted)); pub.verify(b64(m['signature']),canon(p)); sig=True
156
+ except Exception:sig=False
157
+ c['issuer_signature']={'valid':sig}
158
+ if not sig:return bad('invalid manifest signature',c)
159
+ refs=set(); good=True
160
+ for n in p.get('nodes',[]):
161
+ if not isinstance(n,dict) or not all(k in n for k in ('type','id','sha256','data')) or sha(n['data'])!=n['sha256'] or ref(n) in refs:good=False;break
162
+ refs.add(ref(n))
163
+ good=good and all(isinstance(e,dict) and all(k in e for k in ('from','relation','to')) and e['from'] in refs and e['to'] in refs for e in p.get('edges',[])) and root(p)==p.get('root_digest')
164
+ c['graph_integrity']={'valid':good}
165
+ if not good:return bad('graph integrity mismatch',c)
166
+ ok,why=assertions(m); c['authority_protocol']={'valid':ok}
167
+ if not ok:return bad(why,c)
168
+ a=node(p,'execution_authorization'); r=node(p,'execution_receipt'); ap=a['data']['payload']; rp=r['data']['payload']; t=rp.get('executed_at')
169
+ identity_node=node(p,'identity'); capability_node=node(p,'capability'); intent_node=node(p,'action_intent'); authority_node=node(p,'authority_state'); genesis_node=node(p,'genesis_authority')
170
+ if any(x is None for x in (identity_node,capability_node,intent_node,authority_node,genesis_node)): return bad('authority binding evidence incomplete',c)
171
+ if ap.get('agent_id') != p.get('agent_id') or ap.get('agent_id') != identity_node['data'].get('agent_id') or ap.get('identity_id') != identity_node['id'] or ap.get('capability_id') != capability_node['id'] or ap.get('intent_id') != intent_node['id']:
172
+ return bad('authorization identity/capability/intent binding failed',c)
173
+ if ap.get('action') != intent_node['data']:
174
+ return bad('execution authorization action payload differs from canonical intent',c)
175
+ if rp.get('authorization_sha256') != sha(ap):
176
+ return bad('execution receipt is not bound to the exact execution authorization payload',c)
177
+ external_state_required=bool(ap.get('external_state_required'))
178
+ external_state_scope=rp.get('execution_external_state_scope')
179
+ external_state=ap.get('external_state')
180
+ requirement=ap.get('external_state_requirement')
181
+ if not external_state_required:
182
+ if external_state_scope not in (None,'atomic'):
183
+ return bad('unsupported execution external-state scope',c)
184
+ else:
185
+ if external_state_scope != 'atomic':
186
+ return bad('required external state execution is not atomically enforced',c)
187
+ if not isinstance(external_state,dict) or not external_state:
188
+ return bad('required external state binding is missing',c)
189
+ if not isinstance(requirement,dict):
190
+ return bad('required external state policy requirement is missing',c)
191
+ metadata=intent_node['data'].get('metadata')
192
+ canonical_state=metadata.get('external_state') if isinstance(metadata,dict) else None
193
+ if not isinstance(canonical_state,dict) or external_state != canonical_state:
194
+ return bad('external state binding differs from canonical intent',c)
195
+ if ap.get('external_state_sha256') != sha(external_state):
196
+ return bad('external state fingerprint mismatch',c)
197
+ if ap.get('external_state_requirement_sha256') != sha(requirement):
198
+ return bad('external state requirement fingerprint mismatch',c)
199
+ if external_state.get('kind') != requirement.get('kind'):
200
+ return bad('external state kind does not satisfy policy requirement',c)
201
+ if not isinstance(external_state.get('reference'),str) or not external_state['reference'].strip():
202
+ return bad('external state binding is missing reference',c)
203
+ dv=external_state.get('digest')
204
+ if not isinstance(dv,str) or len(dv)!=64:
205
+ return bad('external state binding has invalid digest',c)
206
+ try: int(dv,16)
207
+ except Exception: return bad('external state binding digest is not hexadecimal',c)
208
+ authority_data=authority_node['data']; authority_sha=sha(authority_data)
209
+ if ap.get('authority_state_sha256') != authority_sha: return bad('historical authority snapshot digest mismatch',c)
210
+ capability_data=capability_node['data']
211
+ authority_obj={'authority_id':f"{ap['agent_id']}:{capability_node['id']}:{authority_data.get('ledger_head_hash') or sha(authority_data)}",'agent_id':ap['agent_id'],'identity_id':ap.get('identity_id',''),'capability_id':capability_node['id'],'capability_version':capability_data.get('version',1),'capability_sha256':ap.get('capability_sha256',''),'state':authority_data['state'],'multiplier':float(authority_data['multiplier']),'epoch':0,'ledger_head_hash':authority_data.get('ledger_head_hash',''),'effective_from':float(authority_data.get('evaluated_at',0.0)),'expires_at':None,'parent_authority_id':None,'evidence_refs':[],'metadata':{'source':'core.authority_state.AuthoritySnapshot','successes':authority_data.get('successes',0),'adverse_events':authority_data.get('adverse_events',0),'critical_events':authority_data.get('critical_events',0),'reason':authority_data.get('reason','')}}
212
+ if genesis_node['data'].get('authority_digest') != sha(authority_obj): return bad('Genesis authority does not bind historical authority',c)
213
+ try:pub.verify(b64(a['data']['signature']),canon(ap)); asig=True
214
+ except Exception:asig=False
215
+ c['authorization']={'valid':asig and ap.get('expires_at',-1)>=t>=ap.get('issued_at',10**30),'historical_verification_time':t}
216
+ if not c['authorization']['valid']:return bad('execution authorization verification failed',c)
217
+ try:pub.verify(b64(r['data']['signature']),canon(rp)); rsig=True
218
+ except Exception:rsig=False
219
+ c['execution']={'valid':rsig and rp.get('authorization_id')==ap.get('authorization_id') and rp.get('intent_id')==ap.get('intent_id')}
220
+ if not c['execution']['valid']:return bad('execution receipt verification failed',c)
221
+ l=node(p,'authority_ledger')['data'].get('entries',[]); prev=ZERO; lok=True
222
+ for i,x in enumerate(l,1):
223
+ if x.get('sequence')!=i or x.get('prev_event_hash')!=prev or hashlib.sha256(canon(x.get('event'))).hexdigest()!=x.get('event_hash'):lok=False;break
224
+ prev=x.get('event_hash')
225
+ head=ap.get('authority_ledger_head_hash'); c['historical_authority']={'valid':lok and (head=='0'*64 or any(x.get('event_hash')==head for x in l)),'ledger_sequence':next((i for i,x in enumerate(l,1) if x.get('event_hash')==head),0),'live_ledger_sequence':len(l)}
226
+ if not c['historical_authority']['valid']:return bad('historical authority binding failed',c)
227
+ h_ok,h_reason=verify_historical(authority_data,l,ap.get('authority_policy') or {},head); c['historical_authority_semantics']={'valid':h_ok,'reason':h_reason}
228
+ if not h_ok:return bad('historical authority semantics failed',c)
229
+ cl=node(p,'outcome_claim'); oe=node(p,'observed_effect'); at=node(p,'outcome_attestation'); ev=node(p,'authority_event'); af=node(p,'authority_state_after'); claim_data=cl['data']; observed_data=oe['data']; att_payload=at['data'].get('payload',{}); out=edge(p,ref(r),'OBSERVED_BY',ref(cl)) and edge(p,ref(cl),'SUPPORTED_BY',ref(oe)) and claim_data.get('execution_receipt_sha256')==sha(r['data']) and claim_data.get('observed_effect_sha256')==sha(observed_data) and oe['id']==claim_data.get('observed_effect_sha256') and observed_data.get('authorization_id')==ap.get('authorization_id') and observed_data.get('action_sha256')==claim_data.get('action_sha256') and observed_data.get('evidence_ref')==claim_data.get('evidence_ref') and observed_data.get('result_sha256')==claim_data.get('result_sha256') and observed_data.get('observation_sha256')==sha({'verifier_type':observed_data.get('verifier_type'),'evidence_kind':observed_data.get('evidence_kind'),'effect_status':observed_data.get('effect_status'),'authorization_id':observed_data.get('authorization_id'),'action_sha256':observed_data.get('action_sha256'),'observed_at':observed_data.get('observed_at'),'evidence_ref':observed_data.get('evidence_ref'),'result_sha256':observed_data.get('result_sha256'),'observation':observed_data.get('observation')}) and edge(p,ref(at),'ATTESTS',ref(cl)) and att_payload.get('claim_sha256')==sha(claim_data) and att_payload.get('claim',{}).get('claim_id')==claim_data.get('claim_id'); c['outcome']={'valid':out}; learn_ok,learn_reason=verify_learning(claim_data,att_payload,ev['data'],authority_node['data'],af['data'],l,ap.get('authority_policy') or {},ap.get('authority_ledger_head_hash','')); learn=learn_ok; c['learning']={'valid':learn,'reason':learn_reason}
230
+ if not out or not learn:return bad('outcome or learning binding failed',c)
231
+ eh=next((x.get('event_hash') for x in l if x.get('event_id')==ev['data'].get('event_id')),None); c['post_learning_authority']={'valid':eh is not None and af['data'].get('ledger_head_hash')==eh}
232
+ if not c['post_learning_authority']['valid']:return bad('post-learning authority is not ledger-bound',c)
233
+ return {'valid':True,'protocol':PROTOCOL,'root_digest':p['root_digest'],'package_sha256':ph,'assertion_set_sha256':pkg.get('assertion_set_sha256'),'checks':c}
234
+ def main():
235
+ x=argparse.ArgumentParser();x.add_argument('proof');x.add_argument('--public-key',required=True);x.add_argument('--format',choices=['text','json'],default='text');a=x.parse_args()
236
+ try:
237
+ r=verify(a.proof,a.public_key)
238
+ except Exception as exc:
239
+ r=bad('verification failed closed: '+str(exc),{})
240
+ if a.format=='json':print(json.dumps(r,indent=2,sort_keys=True))
241
+ else:
242
+ print('VERIGATE AUTHORITY PROOF - STANDALONE');print('Protocol:',r.get('protocol',PROTOCOL));[print(f"{k.upper():28} {'PASS' if v.get('valid') else 'FAIL'}") for k,v in r.get('checks',{}).items()];print('RESULT:', 'VALID' if r.get('valid') else 'INVALID'); print('Reason:',r['reason']) if not r.get('valid') else None
243
+ return 0 if r.get('valid') else 1
244
+ if __name__=='__main__':raise SystemExit(main())
245
+
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: verigate-verify
3
+ Version: 0.1.0
4
+ Summary: Offline verifier for Verigate authority proofs: check a portable proof with only the issuer's public key.
5
+ Author: rudimentall1
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rudimentall1/verigate
8
+ Project-URL: Source, https://github.com/rudimentall1/verigate/tree/main/packaging/verigate-verify
9
+ Project-URL: Issues, https://github.com/rudimentall1/verigate/issues
10
+ Project-URL: Proof format, https://github.com/rudimentall1/verigate/blob/main/docs/verigate-authority-proof-v1.md
11
+ Project-URL: Threat model, https://github.com/rudimentall1/verigate/blob/main/docs/threat-model.md
12
+ Keywords: ai-agents,authorization,verification,ed25519,audit,x402
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: cryptography<51,>=48.0.1
21
+ Dynamic: license-file
22
+
23
+ # verigate-verify
24
+
25
+ Offline verifier for [Verigate](https://github.com/rudimentall1/verigate) authority proofs.
26
+
27
+ Verigate decides, per action, whether an AI agent had the authority to act, issues a one-time signed
28
+ authorization, and exports a portable proof. This package is the standalone verifier for that proof:
29
+ **you need only the proof file and the issuer's Ed25519 public key.** No Verigate server, database or API.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install verigate-verify
35
+ ```
36
+
37
+ ## Use
38
+
39
+ ```bash
40
+ verigate-verify proof.json --public-key issuer.pub
41
+ verigate-verify proof.json --public-key issuer.pub --format json
42
+ # or
43
+ python -m verigate_verify proof.json --public-key issuer.pub
44
+ ```
45
+
46
+ The result is `VALID` or `INVALID` with a reason (for example `proof package digest mismatch` if the
47
+ evidence was altered). Exit code is 0 for a valid proof and non-zero otherwise.
48
+
49
+ A sample proof and issuer key are in the repository under `examples/authority-proof/`.
50
+
51
+ ## What it does and does not tell you
52
+
53
+ It checks the package digest, the evidence manifest, the issuer signature against the public key you trust,
54
+ the evidence graph and the authority protocol assertions. It does not decide whether the issuer's key is the
55
+ right one to trust (you must obtain the public key through a channel you trust), and it does not prove that no
56
+ other actions were taken. See the [threat model](https://github.com/rudimentall1/verigate/blob/main/docs/threat-model.md).
57
+
58
+ ## Source
59
+
60
+ This is the same code as `tools/verigate_proof_verifier.py` in the main repository. MIT licensed.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/verigate_verify/__init__.py
5
+ src/verigate_verify/__main__.py
6
+ src/verigate_verify/_verifier.py
7
+ src/verigate_verify.egg-info/PKG-INFO
8
+ src/verigate_verify.egg-info/SOURCES.txt
9
+ src/verigate_verify.egg-info/dependency_links.txt
10
+ src/verigate_verify.egg-info/entry_points.txt
11
+ src/verigate_verify.egg-info/requires.txt
12
+ src/verigate_verify.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ verigate-verify = verigate_verify._verifier:main
@@ -0,0 +1 @@
1
+ cryptography<51,>=48.0.1
@@ -0,0 +1 @@
1
+ verigate_verify