agentauthoritychain 0.5.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.
- agentauthoritychain-0.5.0/PKG-INFO +18 -0
- agentauthoritychain-0.5.0/README.md +15 -0
- agentauthoritychain-0.5.0/aac_client.py +133 -0
- agentauthoritychain-0.5.0/aac_public_test.py +59 -0
- agentauthoritychain-0.5.0/aac_receipt_verifier.py +27 -0
- agentauthoritychain-0.5.0/agentauthoritychain.egg-info/PKG-INFO +18 -0
- agentauthoritychain-0.5.0/agentauthoritychain.egg-info/SOURCES.txt +10 -0
- agentauthoritychain-0.5.0/agentauthoritychain.egg-info/dependency_links.txt +1 -0
- agentauthoritychain-0.5.0/agentauthoritychain.egg-info/requires.txt +2 -0
- agentauthoritychain-0.5.0/agentauthoritychain.egg-info/top_level.txt +3 -0
- agentauthoritychain-0.5.0/pyproject.toml +28 -0
- agentauthoritychain-0.5.0/setup.cfg +4 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentauthoritychain
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Fail-closed AI-agent authority verification, delegated capability enforcement, and signed decision receipts
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://agentauthoritychain.com/
|
|
7
|
+
Project-URL: Documentation, https://agentauthoritychain.com/quickstart/
|
|
8
|
+
Project-URL: Repository, https://github.com/AgentAuthorityChain/aac
|
|
9
|
+
Project-URL: Security, https://agentauthoritychain.com/.well-known/security.txt
|
|
10
|
+
Keywords: ai-agents,authority,delegation,authorization,capabilities,mcp,fail-closed
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: cryptography>=43
|
|
18
|
+
Requires-Dist: truststore>=0.10.4
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# AAC Python client
|
|
2
|
+
|
|
3
|
+
Requires Python 3.10+. The client fails closed and verifies AAC's Ed25519 decision-receipt signature before returning an exact `permit`.
|
|
4
|
+
|
|
5
|
+
```powershell
|
|
6
|
+
python -m venv .venv
|
|
7
|
+
.\.venv\Scripts\python.exe -m pip install .
|
|
8
|
+
.\.venv\Scripts\python.exe -m aac_public_test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
All eight credential-free AAC/AACP cases must print `PASS`. Then supply the once-shown organization credential through a secret manager or `AAC_API_CREDENTIAL`; never commit it. Use `AACClient.from_env()`, `AACClient.verify(signed_scenario)` or `AACClient.enforce(...)`. Async applications use the API-compatible `AsyncAACClient`; it preserves the same fail-closed receipt verification and supports sync or async protected operations. Private signing keys stay outside AAC.
|
|
12
|
+
|
|
13
|
+
Denial is distinct from an unreliable verification: `AACDenied` means AAC returned a signed denial, while configuration, authentication, transport, protocol and receipt-signature failures use their corresponding `AAC*Error` classes. None of these errors includes the credential or authority proof.
|
|
14
|
+
|
|
15
|
+
The signed-in organization console provides a domain-bound First Permit Kit for generating a local Ed25519 key and completing the first real authenticated permit.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Fail-closed AAC verification client using the declared cryptography dependency."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
import base64
|
|
10
|
+
try:
|
|
11
|
+
import truststore
|
|
12
|
+
truststore.inject_into_ssl()
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AACError(Exception):
|
|
19
|
+
"""Base class that never includes credentials or request bodies in its message."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AACConfigurationError(AACError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AACTransportError(AACError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AACAuthenticationError(AACError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AACProtocolError(AACError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AACReceiptVerificationError(AACProtocolError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AACDenied(PermissionError, AACError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AACClient:
|
|
47
|
+
def __init__(self, credential, base_url="https://agentauthoritychain.com", timeout=8):
|
|
48
|
+
if not isinstance(credential, str) or not credential.strip():
|
|
49
|
+
raise AACConfigurationError("AAC credential is required.")
|
|
50
|
+
if not isinstance(base_url, str) or not base_url.startswith("https://"):
|
|
51
|
+
raise AACConfigurationError("AAC base URL must use HTTPS.")
|
|
52
|
+
self.credential = credential
|
|
53
|
+
self.base_url = base_url.rstrip("/")
|
|
54
|
+
self.endpoint = self.base_url + "/api/v1/verify"
|
|
55
|
+
self.key_endpoint = self.base_url + "/.well-known/aac-receipt-key.json"
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_env(cls):
|
|
60
|
+
return cls(
|
|
61
|
+
os.environ.get("AAC_API_CREDENTIAL", ""),
|
|
62
|
+
os.environ.get("AAC_BASE_URL", "https://agentauthoritychain.com"),
|
|
63
|
+
float(os.environ.get("AAC_TIMEOUT_SECONDS", "8")),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def verify(self, signed_scenario):
|
|
67
|
+
body = json.dumps({"input_type": "signed", "signed_scenario": signed_scenario}).encode()
|
|
68
|
+
request = urllib.request.Request(self.endpoint, data=body, method="POST", headers={
|
|
69
|
+
"Authorization": f"Bearer {self.credential}", "Content-Type": "application/json",
|
|
70
|
+
"X-AAC-Timestamp": str(int(time.time())), "X-AAC-Nonce": secrets.token_urlsafe(24),
|
|
71
|
+
})
|
|
72
|
+
try:
|
|
73
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
74
|
+
payload = json.load(response)
|
|
75
|
+
except urllib.error.HTTPError as exc:
|
|
76
|
+
if exc.code in (401, 403):
|
|
77
|
+
raise AACAuthenticationError("AAC rejected the client authentication.") from exc
|
|
78
|
+
raise AACTransportError("AAC verification request failed.") from exc
|
|
79
|
+
except OSError as exc:
|
|
80
|
+
raise AACTransportError("AAC verification was unavailable.") from exc
|
|
81
|
+
except (TypeError, ValueError) as exc:
|
|
82
|
+
raise AACProtocolError("AAC returned malformed JSON.") from exc
|
|
83
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("result"), str):
|
|
84
|
+
raise AACProtocolError("AAC returned a malformed decision.")
|
|
85
|
+
receipt = payload.get("decision_receipt") if isinstance(payload, dict) else None
|
|
86
|
+
if not isinstance(receipt, dict):
|
|
87
|
+
raise AACProtocolError("AAC returned no decision receipt.")
|
|
88
|
+
if not self._valid_signature(receipt):
|
|
89
|
+
raise AACReceiptVerificationError("AAC receipt verification failed.")
|
|
90
|
+
if payload["result"] != "valid" or receipt.get("decision") != "permit":
|
|
91
|
+
reason = receipt.get("reason", {})
|
|
92
|
+
code = reason.get("code", "authority_denied") if isinstance(reason, dict) else "authority_denied"
|
|
93
|
+
raise AACDenied(f"AAC denied the operation: {code}.")
|
|
94
|
+
return receipt
|
|
95
|
+
|
|
96
|
+
def _valid_signature(self, receipt):
|
|
97
|
+
try:
|
|
98
|
+
signature = receipt["signature"]
|
|
99
|
+
if (receipt.get("receipt_version") != "aac-decision-receipt/1"
|
|
100
|
+
or receipt.get("verifier") != self.base_url
|
|
101
|
+
or signature.get("verification_key") != self.key_endpoint):
|
|
102
|
+
return False
|
|
103
|
+
with urllib.request.urlopen(self.key_endpoint, timeout=self.timeout) as response:
|
|
104
|
+
if hasattr(response, "geturl") and response.geturl() != self.key_endpoint:
|
|
105
|
+
return False
|
|
106
|
+
key = json.load(response)
|
|
107
|
+
if key["kid"] != signature["kid"] or key["algorithm"] != "Ed25519":
|
|
108
|
+
return False
|
|
109
|
+
decode = lambda value: base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
|
110
|
+
unsigned = {name: value for name, value in receipt.items() if name != "signature"}
|
|
111
|
+
canonical = json.dumps(unsigned, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
|
112
|
+
Ed25519PublicKey.from_public_bytes(decode(key["public_key"])).verify(decode(signature["value"]), canonical)
|
|
113
|
+
return True
|
|
114
|
+
except Exception:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def enforce(self, signed_scenario, protected_operation, *args, **kwargs):
|
|
118
|
+
receipt = self.verify(signed_scenario)
|
|
119
|
+
return protected_operation(*args, **kwargs), receipt
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AsyncAACClient(AACClient):
|
|
123
|
+
"""Async facade over the dependency-minimal sync transport."""
|
|
124
|
+
|
|
125
|
+
async def verify(self, signed_scenario):
|
|
126
|
+
return await asyncio.to_thread(super().verify, signed_scenario)
|
|
127
|
+
|
|
128
|
+
async def enforce(self, signed_scenario, protected_operation, *args, **kwargs):
|
|
129
|
+
receipt = await self.verify(signed_scenario)
|
|
130
|
+
result = protected_operation(*args, **kwargs)
|
|
131
|
+
if hasattr(result, "__await__"):
|
|
132
|
+
result = await result
|
|
133
|
+
return result, receipt
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Credential-free AAC/AACP public conformance runner."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.request
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import truststore
|
|
10
|
+
truststore.inject_into_ssl()
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CASES = (
|
|
16
|
+
("/api/v1/evaluate?case=valid", {"result": "valid", "decision": "permit"}),
|
|
17
|
+
("/api/v1/evaluate?case=widened", {"result": "invalid", "decision": "deny"}),
|
|
18
|
+
("/api/v1/evaluate?case=expired", {"result": "invalid", "decision": "deny"}),
|
|
19
|
+
("/api/aacp/v1/evaluate?case=valid", {"valid": True}),
|
|
20
|
+
("/api/aacp/v1/evaluate?case=invalid_signature", {"valid": False}),
|
|
21
|
+
("/api/aacp/v1/evaluate?case=stale_acceptance", {"valid": False}),
|
|
22
|
+
("/api/aacp/v1/evaluate?case=broken_event_chain", {"valid": False}),
|
|
23
|
+
("/api/aacp/v1/evaluate?case=evidence_substitution", {"valid": False}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run_case(base_url, opener, case):
|
|
28
|
+
path, expected = case
|
|
29
|
+
try:
|
|
30
|
+
request = urllib.request.Request(base_url.rstrip("/") + path, headers={"Accept": "application/json", "User-Agent": "AAC-Official-Conformance/1.0"})
|
|
31
|
+
with opener(request, timeout=30) as response:
|
|
32
|
+
payload = json.load(response)
|
|
33
|
+
if response.status != 200:
|
|
34
|
+
raise RuntimeError(f"HTTP {response.status}")
|
|
35
|
+
actual = {"valid": payload.get("valid")} if "valid" in expected else {"result": payload.get("result"), "decision": payload.get("decision_receipt", {}).get("decision")}
|
|
36
|
+
passed = actual == expected
|
|
37
|
+
except (OSError, ValueError, urllib.error.HTTPError, KeyError) as exc:
|
|
38
|
+
reason = getattr(exc, "reason", exc)
|
|
39
|
+
detail = str(reason).encode(sys.stdout.encoding or "utf-8", errors="backslashreplace").decode(sys.stdout.encoding or "utf-8", errors="replace")
|
|
40
|
+
actual, passed = {"error": type(exc).__name__, "detail": detail[:240]}, False
|
|
41
|
+
return {"path": path, "expected": expected, "actual": actual, "passed": passed}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run(base_url="https://agentauthoritychain.com", opener=urllib.request.urlopen):
|
|
45
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
46
|
+
return list(executor.map(lambda case: _run_case(base_url, opener, case), CASES))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
arguments = [item for item in sys.argv[1:] if item != "--direct"]
|
|
51
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({})).open if "--direct" in sys.argv[1:] else urllib.request.urlopen
|
|
52
|
+
results = run(arguments[0] if arguments else "https://agentauthoritychain.com", opener=opener)
|
|
53
|
+
for item in results:
|
|
54
|
+
print(("PASS" if item["passed"] else "FAIL"), item["path"], "" if item["passed"] else item["actual"])
|
|
55
|
+
return 0 if all(item["passed"] for item in results) else 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Independent offline AAC decision-receipt verifier."""
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _decode(value): return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify_receipt(receipt, key_document, request=None, authority_chain=None):
|
|
13
|
+
signature = receipt.get("signature", {})
|
|
14
|
+
if key_document.get("schema") == "aac-receipt-key-set/1":
|
|
15
|
+
matching = [key for key in key_document.get("keys", []) if key.get("kid") == signature.get("kid")]
|
|
16
|
+
if len(matching) != 1: return {"valid": False, "reason": "KEY_OR_ALGORITHM_MISMATCH"}
|
|
17
|
+
key_document = matching[0]
|
|
18
|
+
if signature.get("algorithm") != "Ed25519" or signature.get("kid") != key_document.get("kid"): return {"valid": False, "reason": "KEY_OR_ALGORITHM_MISMATCH"}
|
|
19
|
+
unsigned = {key: value for key, value in receipt.items() if key != "signature"}
|
|
20
|
+
canonical = json.dumps(unsigned, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
|
21
|
+
try: Ed25519PublicKey.from_public_bytes(_decode(key_document["public_key"])).verify(_decode(signature["value"]), canonical)
|
|
22
|
+
except Exception: return {"valid": False, "reason": "INVALID_SIGNATURE"}
|
|
23
|
+
if request is not None and receipt.get("request_hash") != "sha256:" + hashlib.sha256(request).hexdigest(): return {"valid": False, "reason": "REQUEST_HASH_MISMATCH"}
|
|
24
|
+
if authority_chain is not None:
|
|
25
|
+
expected = "sha256:" + hashlib.sha256(authority_chain).hexdigest()
|
|
26
|
+
if receipt.get("authority_chain_hash") != expected: return {"valid": False, "reason": "AUTHORITY_CHAIN_HASH_MISMATCH"}
|
|
27
|
+
return {"valid": True, "decision": receipt.get("decision"), "kid": signature["kid"], "request_hash": receipt.get("request_hash"), "authority_chain_hash": receipt.get("authority_chain_hash")}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentauthoritychain
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Fail-closed AI-agent authority verification, delegated capability enforcement, and signed decision receipts
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://agentauthoritychain.com/
|
|
7
|
+
Project-URL: Documentation, https://agentauthoritychain.com/quickstart/
|
|
8
|
+
Project-URL: Repository, https://github.com/AgentAuthorityChain/aac
|
|
9
|
+
Project-URL: Security, https://agentauthoritychain.com/.well-known/security.txt
|
|
10
|
+
Keywords: ai-agents,authority,delegation,authorization,capabilities,mcp,fail-closed
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: cryptography>=43
|
|
18
|
+
Requires-Dist: truststore>=0.10.4
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
aac_client.py
|
|
3
|
+
aac_public_test.py
|
|
4
|
+
aac_receipt_verifier.py
|
|
5
|
+
pyproject.toml
|
|
6
|
+
agentauthoritychain.egg-info/PKG-INFO
|
|
7
|
+
agentauthoritychain.egg-info/SOURCES.txt
|
|
8
|
+
agentauthoritychain.egg-info/dependency_links.txt
|
|
9
|
+
agentauthoritychain.egg-info/requires.txt
|
|
10
|
+
agentauthoritychain.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentauthoritychain"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Fail-closed AI-agent authority verification, delegated capability enforcement, and signed decision receipts"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["cryptography>=43", "truststore>=0.10.4"]
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
keywords = ["ai-agents", "authority", "delegation", "authorization", "capabilities", "mcp", "fail-closed"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Security",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://agentauthoritychain.com/"
|
|
23
|
+
Documentation = "https://agentauthoritychain.com/quickstart/"
|
|
24
|
+
Repository = "https://github.com/AgentAuthorityChain/aac"
|
|
25
|
+
Security = "https://agentauthoritychain.com/.well-known/security.txt"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
py-modules = ["aac_client", "aac_receipt_verifier", "aac_public_test"]
|