veefore-py 1.1.0__py3-none-any.whl
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.
- veefore/__init__.py +73 -0
- veefore/assertion.py +119 -0
- veefore/client.py +639 -0
- veefore/errors.py +146 -0
- veefore/guard.py +232 -0
- veefore/models.py +131 -0
- veefore/protect.py +151 -0
- veefore/py.typed +0 -0
- veefore_py-1.1.0.dist-info/METADATA +57 -0
- veefore_py-1.1.0.dist-info/RECORD +12 -0
- veefore_py-1.1.0.dist-info/WHEEL +5 -0
- veefore_py-1.1.0.dist-info/top_level.txt +1 -0
veefore/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Veefore SDK for Python.
|
|
2
|
+
|
|
3
|
+
The SDK is a convenience layer, never a security boundary. Every protected
|
|
4
|
+
action is decided server-side by the gateway on every call. Nothing in this
|
|
5
|
+
package can grant an agent authority it does not already have, and nothing here
|
|
6
|
+
caches a permission.
|
|
7
|
+
|
|
8
|
+
from veefore import protect
|
|
9
|
+
|
|
10
|
+
agent = protect(my_agent)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .assertion import (
|
|
16
|
+
ASSERTION_TTL_SECONDS,
|
|
17
|
+
canonical_json,
|
|
18
|
+
create_assertion,
|
|
19
|
+
generate_nonce,
|
|
20
|
+
)
|
|
21
|
+
from .client import SDK_NAME, SDK_VERSION, VeeforeClient
|
|
22
|
+
from .errors import (
|
|
23
|
+
VeeforeApprovalDeniedError,
|
|
24
|
+
VeeforeApprovalRequiredError,
|
|
25
|
+
VeeforeAuthenticationError,
|
|
26
|
+
VeeforeBlockedError,
|
|
27
|
+
VeeforeConfigurationError,
|
|
28
|
+
VeeforeConnectionError,
|
|
29
|
+
VeeforeError,
|
|
30
|
+
VeeforeRateLimitError,
|
|
31
|
+
)
|
|
32
|
+
from .models import (
|
|
33
|
+
ActionDecision,
|
|
34
|
+
ActionRequest,
|
|
35
|
+
ApprovalStatus,
|
|
36
|
+
HandshakeResult,
|
|
37
|
+
StageResult,
|
|
38
|
+
)
|
|
39
|
+
from .guard import guard, guard_client, reset_guard_client
|
|
40
|
+
from .protect import ProtectedAgent, config_from_env, create_client, protect
|
|
41
|
+
|
|
42
|
+
__version__ = SDK_VERSION
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"protect",
|
|
46
|
+
"guard",
|
|
47
|
+
"guard_client",
|
|
48
|
+
"reset_guard_client",
|
|
49
|
+
"create_client",
|
|
50
|
+
"config_from_env",
|
|
51
|
+
"ProtectedAgent",
|
|
52
|
+
"VeeforeClient",
|
|
53
|
+
"ActionRequest",
|
|
54
|
+
"ActionDecision",
|
|
55
|
+
"ApprovalStatus",
|
|
56
|
+
"HandshakeResult",
|
|
57
|
+
"StageResult",
|
|
58
|
+
"VeeforeError",
|
|
59
|
+
"VeeforeAuthenticationError",
|
|
60
|
+
"VeeforeBlockedError",
|
|
61
|
+
"VeeforeApprovalRequiredError",
|
|
62
|
+
"VeeforeApprovalDeniedError",
|
|
63
|
+
"VeeforeConnectionError",
|
|
64
|
+
"VeeforeConfigurationError",
|
|
65
|
+
"VeeforeRateLimitError",
|
|
66
|
+
"create_assertion",
|
|
67
|
+
"generate_nonce",
|
|
68
|
+
"canonical_json",
|
|
69
|
+
"ASSERTION_TTL_SECONDS",
|
|
70
|
+
"SDK_NAME",
|
|
71
|
+
"SDK_VERSION",
|
|
72
|
+
"__version__",
|
|
73
|
+
]
|
veefore/assertion.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Agent identity assertions.
|
|
2
|
+
|
|
3
|
+
A compact JWS (EdDSA / Ed25519) proving possession of the agent's private
|
|
4
|
+
identity key, bound to the agent id, the gateway audience, a single-use nonce
|
|
5
|
+
and a short expiry.
|
|
6
|
+
|
|
7
|
+
The private key never leaves this module: it is not logged, not placed in a
|
|
8
|
+
request body, and not written to disk.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import json
|
|
15
|
+
import secrets
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
from typing import Any, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from cryptography.hazmat.primitives import serialization
|
|
20
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
21
|
+
|
|
22
|
+
from .errors import VeeforeConfigurationError
|
|
23
|
+
|
|
24
|
+
__all__ = ["ASSERTION_TTL_SECONDS", "canonical_json", "create_assertion", "generate_nonce"]
|
|
25
|
+
|
|
26
|
+
ASSERTION_TTL_SECONDS = 120
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _b64url(data: bytes) -> str:
|
|
30
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def canonical_json(value: Any) -> str:
|
|
34
|
+
"""RFC 8785-style canonical JSON: sorted keys, no insignificant whitespace.
|
|
35
|
+
|
|
36
|
+
Must match the server's canonicalisation byte for byte or signatures fail.
|
|
37
|
+
"""
|
|
38
|
+
if value is None:
|
|
39
|
+
return "null"
|
|
40
|
+
if isinstance(value, bool):
|
|
41
|
+
return "true" if value else "false"
|
|
42
|
+
if isinstance(value, (int, float)):
|
|
43
|
+
return json.dumps(value)
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
return json.dumps(value, ensure_ascii=False)
|
|
46
|
+
if isinstance(value, (list, tuple)):
|
|
47
|
+
return "[" + ",".join(canonical_json(item) for item in value) + "]"
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
items = sorted((k, v) for k, v in value.items() if v is not None)
|
|
50
|
+
return (
|
|
51
|
+
"{"
|
|
52
|
+
+ ",".join(
|
|
53
|
+
f"{json.dumps(k, ensure_ascii=False)}:{canonical_json(v)}" for k, v in items
|
|
54
|
+
)
|
|
55
|
+
+ "}"
|
|
56
|
+
)
|
|
57
|
+
return "null"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def generate_nonce() -> str:
|
|
61
|
+
return secrets.token_urlsafe(18)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_private_key(base64_pkcs8: str) -> Ed25519PrivateKey:
|
|
65
|
+
try:
|
|
66
|
+
der = base64.b64decode(base64_pkcs8, validate=False)
|
|
67
|
+
key = serialization.load_der_private_key(der, password=None)
|
|
68
|
+
except Exception as error: # noqa: BLE001 - surfaced as a config error
|
|
69
|
+
raise VeeforeConfigurationError(
|
|
70
|
+
"The agent private key could not be parsed. "
|
|
71
|
+
"Expected base64-encoded PKCS#8 Ed25519 material."
|
|
72
|
+
) from error
|
|
73
|
+
if not isinstance(key, Ed25519PrivateKey):
|
|
74
|
+
raise VeeforeConfigurationError(
|
|
75
|
+
"The agent private key is not an Ed25519 key. Veefore agent identities use Ed25519."
|
|
76
|
+
)
|
|
77
|
+
return key
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_assertion(
|
|
81
|
+
*,
|
|
82
|
+
agent_id: str,
|
|
83
|
+
audience: str,
|
|
84
|
+
private_key: str,
|
|
85
|
+
key_id: str,
|
|
86
|
+
purpose: str = "agent_authentication",
|
|
87
|
+
nonce: Optional[str] = None,
|
|
88
|
+
ttl_seconds: int = ASSERTION_TTL_SECONDS,
|
|
89
|
+
now: Optional[datetime] = None,
|
|
90
|
+
) -> Tuple[str, str]:
|
|
91
|
+
"""Returns ``(assertion, nonce)``."""
|
|
92
|
+
issued_at = now or datetime.now(timezone.utc)
|
|
93
|
+
actual_nonce = nonce or generate_nonce()
|
|
94
|
+
|
|
95
|
+
header = {"typ": "veefore-assertion+jwt", "alg": "EdDSA", "kid": key_id}
|
|
96
|
+
payload = {
|
|
97
|
+
"sub": agent_id,
|
|
98
|
+
"aud": audience,
|
|
99
|
+
"purpose": purpose,
|
|
100
|
+
"nonce": actual_nonce,
|
|
101
|
+
"iat": _iso(issued_at),
|
|
102
|
+
"exp": _iso(issued_at + timedelta(seconds=ttl_seconds)),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
signing_input = (
|
|
106
|
+
_b64url(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
|
107
|
+
+ "."
|
|
108
|
+
+ _b64url(canonical_json(payload).encode("utf-8"))
|
|
109
|
+
)
|
|
110
|
+
key = _load_private_key(private_key)
|
|
111
|
+
signature = key.sign(signing_input.encode("ascii"))
|
|
112
|
+
return f"{signing_input}.{_b64url(signature)}", actual_nonce
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _iso(value: datetime) -> str:
|
|
116
|
+
"""ISO-8601 with a trailing ``Z``, matching the server's format."""
|
|
117
|
+
return value.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace(
|
|
118
|
+
"+00:00", "Z"
|
|
119
|
+
)
|