Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
*.egg-info/
dist/
72 changes: 72 additions & 0 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Open Agent Trust Registry — Python SDK

Python port of the TypeScript SDK (`sdk/typescript/`). Verifies agent
attestation tokens locally against the registry manifest — no network calls
per request, pure local computation, target `<1ms`.

## Installation

```bash
pip install open-agent-trust
```

## Quick Start

```python
import json
from open_agent_trust import verify_attestation, RegistryManifest, RevocationList

# Load the manifest and revocations you fetched and cached locally
with open("manifest.json") as f:
manifest = RegistryManifest.from_dict(json.load(f))

with open("revocations.json") as f:
revocations = RevocationList.from_dict(json.load(f))

# Verify an incoming attestation token
result = verify_attestation(
attestation_jws=token,
manifest=manifest,
revocations=revocations,
expected_audience="https://your-api.com",
)

if result.valid:
# Safe to use result.claims.scope / result.claims.constraints
print("Agent verified:", result.issuer.issuer_id)
print("Scopes:", result.claims.scope)
else:
print("Rejected:", result.reason)
```

## Verification Protocol

Implements the 14-step protocol from `spec/03-verification.md`:

1. Parse JWS and extract `iss` / `kid` from protected header
2. Fast-reject against the revocation list (O(n) scan, expected empty)
3. Look up the issuer in the manifest
4. Reject unknown issuer
5. Reject suspended / revoked issuer
6. Locate key by `kid` in the issuer's `public_keys` array
7. Reject unknown key
8. Reject revoked key
9. Enforce 90-day grace period for deprecated keys
10. Reject expired registry public key
11–12. Verify Ed25519 signature cryptographically
13. Check `aud`, `exp`, and optional `nonce` claims
14. Return `VerificationResult(valid=True, issuer=..., claims=...)`

## Requirements

- Python 3.10+
- `cryptography >= 41` (Ed25519 support)
- `PyJWT >= 2.8` (EdDSA algorithm)

## Running Tests

```bash
cd sdk/python
pip install -e ".[dev]"
pytest -v
```
24 changes: 24 additions & 0 deletions sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "open-agent-trust"
version = "0.1.0"
description = "Python SDK for the Open Agent Trust Registry — verify agent attestation tokens locally"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.10"
dependencies = [
"cryptography>=41.0",
"PyJWT>=2.8",
]

[project.optional-dependencies]
dev = ["pytest>=7.0", "hatchling"]

[tool.hatch.build.targets.wheel]
packages = ["src/open_agent_trust"]

[tool.pytest.ini_options]
testpaths = ["tests"]
49 changes: 49 additions & 0 deletions sdk/python/src/open_agent_trust/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Open Agent Trust Registry — Python SDK

Verify agent attestation tokens locally against the registry manifest.
No network calls required per verification (pure local computation, <1ms).

Usage::

from open_agent_trust import verify_attestation, RegistryManifest, RevocationList

manifest = RegistryManifest.from_dict(manifest_json)
revocations = RevocationList.from_dict(revocations_json)

result = verify_attestation(token, manifest, revocations, audience="https://your-api.com")
if result.valid:
scopes = result.claims.scope
"""

from .types import (
AttestationClaims,
IssuerCapabilities,
IssuerEndpoints,
IssuerEntry,
PublicKey,
RegistryManifest,
RegistrySignature,
RevocationList,
RevokedIssuer,
RevokedKey,
VerificationResult,
)
from .verify import verify_attestation

__all__ = [
"verify_attestation",
"VerificationResult",
"AttestationClaims",
"RegistryManifest",
"RevocationList",
"IssuerEntry",
"PublicKey",
"IssuerCapabilities",
"IssuerEndpoints",
"RegistrySignature",
"RevokedKey",
"RevokedIssuer",
]

__version__ = "0.1.0"
229 changes: 229 additions & 0 deletions sdk/python/src/open_agent_trust/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
"""
Type definitions for the Open Agent Trust Registry Python SDK.

Mirrors sdk/typescript/src/types/ for API parity.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Literal, Optional


# --- Registry types ---

KeyAlgorithm = Literal["Ed25519", "ECDSA-P256"]
KeyStatus = Literal["active", "deprecated", "revoked"]
IssuerStatus = Literal["active", "suspended", "revoked"]

VerificationReasonCode = Literal[
"unknown_issuer",
"revoked_issuer",
"suspended_issuer",
"unknown_key",
"revoked_key",
"grace_period_expired",
"expired_attestation",
"invalid_signature",
"audience_mismatch",
"nonce_mismatch",
]


@dataclass
class PublicKey:
kid: str
algorithm: KeyAlgorithm
public_key: str # Base64url-encoded Ed25519 x coordinate
status: KeyStatus
issued_at: str
expires_at: str
deprecated_at: Optional[str]
revoked_at: Optional[str]

@classmethod
def from_dict(cls, d: dict) -> "PublicKey":
return cls(
kid=d["kid"],
algorithm=d["algorithm"],
public_key=d["public_key"],
status=d["status"],
issued_at=d["issued_at"],
expires_at=d["expires_at"],
deprecated_at=d.get("deprecated_at"),
revoked_at=d.get("revoked_at"),
)


@dataclass
class IssuerCapabilities:
supervision_model: str
audit_logging: bool
immutable_audit: bool
attestation_format: str
max_attestation_ttl_seconds: int
capabilities_verified: bool = False

@classmethod
def from_dict(cls, d: dict) -> "IssuerCapabilities":
return cls(
supervision_model=d["supervision_model"],
audit_logging=d["audit_logging"],
immutable_audit=d["immutable_audit"],
attestation_format=d["attestation_format"],
max_attestation_ttl_seconds=d["max_attestation_ttl_seconds"],
capabilities_verified=d.get("capabilities_verified", False),
)


@dataclass
class IssuerEndpoints:
attestation_verify: Optional[str] = None
revocation_list: Optional[str] = None

@classmethod
def from_dict(cls, d: dict) -> "IssuerEndpoints":
return cls(
attestation_verify=d.get("attestation_verify"),
revocation_list=d.get("revocation_list"),
)


@dataclass
class IssuerEntry:
issuer_id: str
display_name: str
website: str
security_contact: str
status: IssuerStatus
added_at: str
last_verified: str
public_keys: list[PublicKey]
capabilities: IssuerCapabilities
endpoints: Optional[IssuerEndpoints] = None

@classmethod
def from_dict(cls, d: dict) -> "IssuerEntry":
return cls(
issuer_id=d["issuer_id"],
display_name=d["display_name"],
website=d["website"],
security_contact=d["security_contact"],
status=d["status"],
added_at=d["added_at"],
last_verified=d["last_verified"],
public_keys=[PublicKey.from_dict(k) for k in d["public_keys"]],
capabilities=IssuerCapabilities.from_dict(d["capabilities"]),
endpoints=IssuerEndpoints.from_dict(d["endpoints"]) if d.get("endpoints") else None,
)


@dataclass
class RegistrySignature:
algorithm: KeyAlgorithm
kid: str
value: str

@classmethod
def from_dict(cls, d: dict) -> "RegistrySignature":
return cls(algorithm=d["algorithm"], kid=d["kid"], value=d["value"])


@dataclass
class RegistryManifest:
schema_version: str
registry_id: str
generated_at: str
expires_at: str
entries: list[IssuerEntry]
signature: RegistrySignature

@classmethod
def from_dict(cls, d: dict) -> "RegistryManifest":
return cls(
schema_version=d["schema_version"],
registry_id=d["registry_id"],
generated_at=d["generated_at"],
expires_at=d["expires_at"],
entries=[IssuerEntry.from_dict(e) for e in d["entries"]],
signature=RegistrySignature.from_dict(d["signature"]),
)


@dataclass
class RevokedKey:
issuer_id: str
kid: str
revoked_at: str
reason: str

@classmethod
def from_dict(cls, d: dict) -> "RevokedKey":
return cls(
issuer_id=d["issuer_id"],
kid=d["kid"],
revoked_at=d["revoked_at"],
reason=d["reason"],
)


@dataclass
class RevokedIssuer:
issuer_id: str
revoked_at: str
reason: str

@classmethod
def from_dict(cls, d: dict) -> "RevokedIssuer":
return cls(
issuer_id=d["issuer_id"],
revoked_at=d["revoked_at"],
reason=d["reason"],
)


@dataclass
class RevocationList:
schema_version: str
generated_at: str
expires_at: str
revoked_keys: list[RevokedKey]
revoked_issuers: list[RevokedIssuer]
signature: RegistrySignature

@classmethod
def from_dict(cls, d: dict) -> "RevocationList":
return cls(
schema_version=d["schema_version"],
generated_at=d["generated_at"],
expires_at=d["expires_at"],
revoked_keys=[RevokedKey.from_dict(k) for k in d["revoked_keys"]],
revoked_issuers=[RevokedIssuer.from_dict(i) for i in d["revoked_issuers"]],
signature=RegistrySignature.from_dict(d["signature"]),
)


# --- Attestation types ---

AgnosticConstraints = dict[str, Any]


@dataclass
class AttestationClaims:
sub: str
aud: str
iat: int
exp: int
scope: list[str]
constraints: AgnosticConstraints
user_pseudonym: str
runtime_version: str
nonce: Optional[str] = None


@dataclass
class VerificationResult:
valid: bool
reason: Optional[VerificationReasonCode] = None
issuer: Optional[IssuerEntry] = None
claims: Optional[AttestationClaims] = None
Loading