-
-
Notifications
You must be signed in to change notification settings - Fork 40
tsk-5bup3r [OPEN] Registry tokens never expire: taOS mints JWTs with #2235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -263,6 +263,10 @@ def load_or_create_signing_keypair(data_dir: Path) -> tuple[bytes, bytes]: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Token minting (compact JWT-style - header.payload.signature, base64url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_REGISTRY_TOKEN_LIFETIME = 86400 # 24 hours: long enough for multi-turn agent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # tasks, short enough that a leaked bearer credential is not permanent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _b64url_encode(data: bytes) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import base64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -276,13 +280,41 @@ def _b64url_decode(s: str) -> bytes: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return base64.urlsafe_b64decode(s) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _verify_signature_only(token: str, public_key_pem: bytes) -> dict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Verify EdDSA signature and return payload without checking exp. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Internal helper for the renewal path, which must accept expired tokens | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| so an agent can rotate its credential without human intervention. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from cryptography.hazmat.primitives.serialization import load_pem_public_key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from cryptography.exceptions import InvalidSignature | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts = token.split(".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(parts) != 3: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("token must have three dot-separated parts") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header_b64, payload_b64, sig_b64 = parts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signing_input = f"{header_b64}.{payload_b64}".encode() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sig_bytes = _b64url_decode(sig_b64) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public_key = load_pem_public_key(public_key_pem) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public_key.verify(sig_bytes, signing_input) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except InvalidSignature: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("token signature verification failed") from None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = json.loads(_b64url_decode(payload_b64)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return payload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def mint_registry_token( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| canonical_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private_key_pem: bytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id: str = "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| framework: str = "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_id: Optional[str] = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetime_seconds: int | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Return a signed compact EdDSA JWT: <header>.<payload>.<signature> (base64url). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -294,6 +326,7 @@ def mint_registry_token( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sub - canonical_id (immutable agent identity) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| iss - "taos-registry" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| iat - unix timestamp of issuance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exp - unix timestamp of expiry (iat + lifetime_seconds) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id - owning user_id at registration time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| framework - agent framework at registration time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_id - project binding, present only when non-empty; absent means | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -308,10 +341,13 @@ def mint_registry_token( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header = _b64url_encode( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json.dumps({"alg": "EdDSA", "typ": "JWT"}, separators=(",", ":")).encode() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if lifetime_seconds is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetime_seconds = DEFAULT_REGISTRY_TOKEN_LIFETIME | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| claims: dict = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sub": canonical_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "iss": "taos-registry", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "iat": int(time.time()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "exp": int(time.time()) + lifetime_seconds, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "jti": uuid.uuid4().hex, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "user_id": user_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "framework": framework, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -326,31 +362,53 @@ def mint_registry_token( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f"{header}.{payload}.{signature}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def verify_registry_token(token: str, public_key_pem: bytes) -> dict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def verify_registry_token( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public_key_pem: bytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allow_no_exp_until: float | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> dict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Verify *token* against *public_key_pem*. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns the decoded payload dict on success. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Raises ``ValueError`` on invalid format or bad signature. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Raises ``ValueError`` on invalid format, bad signature, missing exp (after | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the migration window), or expired token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from cryptography.hazmat.primitives.serialization import load_pem_public_key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from cryptography.exceptions import InvalidSignature | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = _verify_signature_only(token, public_key_pem) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts = token.split(".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(parts) != 3: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("token must have three dot-separated parts") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exp = payload.get("exp") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if exp is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if now >= exp: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("token has expired") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif allow_no_exp_until is None or now >= allow_no_exp_until: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("token has no exp claim") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header_b64, payload_b64, sig_b64 = parts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signing_input = f"{header_b64}.{payload_b64}".encode() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sig_bytes = _b64url_decode(sig_b64) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return payload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public_key = load_pem_public_key(public_key_pem) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public_key.verify(sig_bytes, signing_input) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except InvalidSignature: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("token signature verification failed") from None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = json.loads(_b64url_decode(payload_b64)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return payload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def renew_registry_token( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public_key_pem: bytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private_key_pem: bytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetime_seconds: int | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Renew an existing registry token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Verifies the EdDSA signature (but does not require the token to be | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unexpired) and returns a freshly-minted token with the same claims and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| a new exp. This is the self-service path for agents whose token has | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expired without human re-minting. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = _verify_signature_only(token, public_key_pem) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return mint_registry_token( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload["sub"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private_key_pem, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id=payload.get("user_id", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| framework=payload.get("framework", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_id=payload.get("project_id"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetime_seconds=lifetime_seconds, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+389
to
+411
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Guard against a payload missing the
🛡️ Proposed fix payload = _verify_signature_only(token, public_key_pem)
+ canonical_id = payload.get("sub")
+ if not canonical_id:
+ raise ValueError("token missing sub claim")
return mint_registry_token(
- payload["sub"],
+ canonical_id,
private_key_pem,
user_id=payload.get("user_id", ""),
framework=payload.get("framework", ""),
project_id=payload.get("project_id"),
lifetime_seconds=lifetime_seconds,
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Locate file and relevant symbols"
if [ -f tinyagentos/agent_registry_store.py ]; then
wc -l tinyagentos/agent_registry_store.py
ast-grep outline tinyagentos/agent_registry_store.py --match renew_registry_token --view expanded || true
echo "--- relevant lines 240-430 ---"
sed -n '240,430p' tinyagentos/agent_registry_store.py | nl -ba -v240
else
fd -i 'agent_registry_store.py' .
fi
echo "--- search for token handling symbols ---"
rg -n "TOKEN_(LIFETIME|RENEW_|EXPIRED)?|lifetime_seconds|renew_registry_token|mint_registry_token|active|verify_signature|jti" tinyagentosRepository: jaylfc/taOS Length of output: 361 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "--- relevant lines 240-430 ---"
sed -n '240,430p' tinyagentos/agent_registry_store.py | sed 's/^/: /'
echo "--- search token-related symbols ---"
rg -n "TOKEN_(LIFETIME|RENEW_|EXPIRED)?|lifetime_seconds|renew_registry_token|mint_registry_token|active|verify_signature|jti" tinyagentos || true
echo "--- inspect registry state references around relevant functions ---"
rg -n "_verify_signature_only|mint_registry_token|renew_registry_token|def .*registry_token|lookup|active|update|delete|agent" tinyagentos/agent_registry_store.py | sed 's/^/: /'Repository: jaylfc/taOS Length of output: 50367 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "--- renew_registry_token_route ---"
sed -n '748,774p' tinyagentos/routes/agent_registry.py | sed 's/^/: /'
echo "--- agent_token_auth relevant lines ---"
sed -n '1,140p' tinyagentos/agent_token_auth.py | sed 's/^/: /'
echo "--- deterministic behavior probe ---"
python3 - <<'PY'
import json
from base64 import urlsafe_b64encode, urlsafe_b64decode
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption
def b64url(data): return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def b64url_decode(s): return urlsafe_b64decode(s + b"=" * (4 - len(s) % 4))
private = Ed25519PrivateKey.generate()
public = private.public_key()
priv_pem = private.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
pub_pem = public.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
def sign(payload):
header = b64url(json.dumps({"alg":"EdDSA","typ":"JWT"}, separators=(",", ":")).encode())
payload_b64 = b64url(json.dumps(payload).encode())
sig = b64url(private.sign(f"{header}.{payload_b64}".encode()))
return f"{header}.{payload_b64}.{sig}"
for days in [25, 36500]:
token = sign({"sub":"agent-a1b2","iss":"taos-registry","iat":0,"exp":days*86400,"jti":"old","user_id":"u"})
# Minimal implementation matching tinyagentos: signature checked, exp ignored, new exp iat+lifetime
payload = json.loads(b64url_decode(token.split(".")[1]))
new_token = sign({"sub":payload["sub"],"iss":"taos-registry","iat":1,\
"exp":1+86400,"jti":"new","user_id":payload.get("user_id",""),**{k:payload.get(k) for k in ["framework","project_id"]}})
print({
"days": days,
"old_exp": payload["exp"],
"old_expired_at_renewal_time": False,
"new_exp": json.loads(new_token.split(".")[1])["exp"],
"renewal_same_id": payload["sub"] == json.loads(new_token.split(".")[1])["sub"]
})
token = sign({"sub":"agent-a1b2","iss":"taos-registry","iat":100,"exp":200,"jti":"old"})
payload = json.loads(b64url_decode(token.split(".")[1]))
try:
public.verify(b64url_decode(token.split(".")[2]), f"{token.split('.')[0]}.{token.split('.')[1]}".encode())
print("signature_verifies_any_expired_payload", payload["sub"])
except Exception as exc:
print("signature_verification_failed", type(exc).__name__, str(exc))
PYRepository: jaylfc/taOS Length of output: 7356 Bound self-service token renewal.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,13 @@ def _get_store(request: Request): | |
| return store | ||
|
|
||
|
|
||
| def _get_migration_cutoff(request: Request) -> float | None: | ||
| config = getattr(request.app.state, "config", None) | ||
| if config is None: | ||
| return None | ||
| return getattr(config, "registry_token_migration_cutoff_ts", None) | ||
|
|
||
|
|
||
| def _get_grants_store(request: Request): | ||
| store = getattr(request.app.state, "agent_grants", None) | ||
| if store is None: | ||
|
|
@@ -96,7 +103,7 @@ async def _verify_agent_scope( | |
| # Verify the EdDSA signature using the registry public key. | ||
| _private_pem, public_pem = _get_keypair(request) | ||
| try: | ||
| payload = verify_registry_token(raw_token, public_pem) | ||
| payload = verify_registry_token(raw_token, public_pem, allow_no_exp_until=_get_migration_cutoff(request)) | ||
| except ValueError: | ||
|
Comment on lines
104
to
107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Legacy tokens break immediately check_agent_scope()/check_agent_identity() now enforce exp via verify_registry_token(), but the default registry_token_migration_cutoff_ts=None means any legacy registry token without exp is rejected (401) unless an operator explicitly configures a future cutoff. This is a backward-compatibility/availability regression on upgrade for deployments with pre-exp tokens. Agent Prompt
|
||
| raise HTTPException(status_code=401, detail="invalid or malformed registry token") | ||
|
|
||
|
|
@@ -178,7 +185,7 @@ async def check_agent_identity(request: Request) -> Optional[str]: | |
|
|
||
| _private_pem, public_pem = _get_keypair(request) | ||
| try: | ||
| payload = verify_registry_token(raw_token, public_pem) | ||
| payload = verify_registry_token(raw_token, public_pem, allow_no_exp_until=_get_migration_cutoff(request)) | ||
| except ValueError: | ||
| raise HTTPException(status_code=401, detail="invalid or malformed registry token") | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,8 @@ class AppConfig: | |
| memory_url: str = "http://localhost:7900" | ||
| wallhaven_api_key: str | None = None | ||
| github_app_id: str = "" | ||
| registry_token_lifetime_seconds: int = 86400 | ||
| registry_token_migration_cutoff_ts: float | None = None | ||
|
Comment on lines
+65
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 6. Token lifetime config unused registry_token_lifetime_seconds is added and parsed into AppConfig, but minting/renewal call sites never pass it, so configuring token lifetime has no effect. Tokens always use DEFAULT_REGISTRY_TOKEN_LIFETIME unless callers explicitly override lifetime_seconds. Agent Prompt
Comment on lines
+65
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -B2 -A15 'def save_config' tinyagentos/config.py
rg -n 'to_dict\(' tinyagentos/config.pyRepository: jaylfc/taOS Length of output: 1247 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== config.py outline relevant =="
ast-grep outline tinyagentos/config.py --match AppConfig --view expanded || true
echo "== AppConfig class region =="
sed -n '1,130p' tinyagentos/config.py
echo "== save/load/save_locked regions =="
sed -n '320,355p' tinyagentos/config.pyRepository: jaylfc/taOS Length of output: 8506 Persist the new registry-token fields when saving config.
Add explicit entries for these fields in 🤖 Prompt for AI Agents |
||
| config_path: Path | None = None | ||
|
|
||
| def to_dict(self) -> dict: | ||
|
|
@@ -194,6 +196,8 @@ def load_config(path: Path) -> AppConfig: | |
| github_app_id=str(data.get("github_app_id", "") or ""), | ||
| config_path=path, | ||
| wallhaven_api_key=wallhaven_api_key, | ||
| registry_token_lifetime_seconds=int(data.get("registry_token_lifetime_seconds", 86400)), | ||
| registry_token_migration_cutoff_ts=float(data["registry_token_migration_cutoff_ts"]) if "registry_token_migration_cutoff_ts" in data else None, | ||
|
Comment on lines
+199
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 7. Config fields not saved load_config() reads registry_token_lifetime_seconds and registry_token_migration_cutoff_ts, but AppConfig.to_dict() omits them, so any save_config() will silently drop these settings from config.yaml. This can unexpectedly revert a configured migration cutoff (and lifetime) after unrelated config-saving migrations. Agent Prompt
Comment on lines
+199
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Unguarded numeric coercion can crash config loading.
🤖 Prompt for AI Agents |
||
| ) | ||
| if "github_app_private_key" in data: | ||
| global _deprecation_warned_github_key | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5. Renew bypasses exp migration
🐞 Bug⛨ SecurityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools