aauth 0.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.
- aauth/__init__.py +137 -0
- aauth/agent/__init__.py +2 -0
- aauth/agent/challenge_handler.py +61 -0
- aauth/agent/signer.py +78 -0
- aauth/errors.py +51 -0
- aauth/headers/__init__.py +2 -0
- aauth/headers/agent_auth.py +146 -0
- aauth/headers/signature.py +53 -0
- aauth/headers/signature_input.py +65 -0
- aauth/headers/signature_key.py +99 -0
- aauth/http/__init__.py +2 -0
- aauth/http/request.py +140 -0
- aauth/http/response.py +106 -0
- aauth/keys/__init__.py +2 -0
- aauth/keys/jwk.py +123 -0
- aauth/keys/jwks.py +191 -0
- aauth/keys/keypair.py +16 -0
- aauth/metadata/__init__.py +2 -0
- aauth/metadata/agent.py +20 -0
- aauth/metadata/auth_server.py +114 -0
- aauth/metadata/resource.py +42 -0
- aauth/resource/__init__.py +2 -0
- aauth/resource/challenge_builder.py +87 -0
- aauth/resource/token_issuer.py +74 -0
- aauth/resource/verifier.py +170 -0
- aauth/signing/__init__.py +2 -0
- aauth/signing/algorithms.py +35 -0
- aauth/signing/signature_base.py +193 -0
- aauth/signing/signer.py +138 -0
- aauth/signing/verifier.py +323 -0
- aauth/tokens/__init__.py +2 -0
- aauth/tokens/agent_token.py +202 -0
- aauth/tokens/auth_token.py +227 -0
- aauth/tokens/resource_token.py +68 -0
- aauth-0.1.0.dist-info/METADATA +584 -0
- aauth-0.1.0.dist-info/RECORD +39 -0
- aauth-0.1.0.dist-info/WHEEL +5 -0
- aauth-0.1.0.dist-info/licenses/LICENSE +21 -0
- aauth-0.1.0.dist-info/top_level.txt +1 -0
aauth/metadata/agent.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Agent metadata handling for AAuth."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_agent_metadata(agent_id: str, jwks_uri: str) -> Dict[str, Any]:
|
|
7
|
+
"""Generate agent metadata JSON per AAuth spec Section 8.1.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
agent_id: Agent identifier (HTTPS URL)
|
|
11
|
+
jwks_uri: URL to agent's JSON Web Key Set
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Agent metadata dictionary with required fields
|
|
15
|
+
"""
|
|
16
|
+
return {
|
|
17
|
+
"agent": agent_id,
|
|
18
|
+
"jwks_uri": jwks_uri
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Auth server metadata handling for AAuth."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_auth_metadata(
|
|
7
|
+
auth_id: str,
|
|
8
|
+
jwks_uri: str,
|
|
9
|
+
token_endpoint: str,
|
|
10
|
+
auth_endpoint: str,
|
|
11
|
+
signing_algs_supported: Optional[list[str]] = None,
|
|
12
|
+
request_types_supported: Optional[list[str]] = None,
|
|
13
|
+
scopes_supported: Optional[list[str]] = None
|
|
14
|
+
) -> Dict[str, Any]:
|
|
15
|
+
"""Generate auth server metadata JSON per AAuth spec Section 8.2.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
auth_id: Auth server identifier (HTTPS URL)
|
|
19
|
+
jwks_uri: URL to auth server's JSON Web Key Set
|
|
20
|
+
token_endpoint: Endpoint for auth requests, code exchange, token exchange, and refresh
|
|
21
|
+
auth_endpoint: Endpoint for user authentication and consent flow
|
|
22
|
+
signing_algs_supported: Optional list of supported HTTPSig algorithms
|
|
23
|
+
request_types_supported: Optional list of supported request_type values
|
|
24
|
+
scopes_supported: Optional list of supported scopes
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Auth server metadata dictionary with required fields
|
|
28
|
+
"""
|
|
29
|
+
metadata = {
|
|
30
|
+
"issuer": auth_id,
|
|
31
|
+
"jwks_uri": jwks_uri,
|
|
32
|
+
"agent_token_endpoint": token_endpoint,
|
|
33
|
+
"agent_auth_endpoint": auth_endpoint
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if signing_algs_supported:
|
|
37
|
+
metadata["agent_signing_algs_supported"] = signing_algs_supported
|
|
38
|
+
else:
|
|
39
|
+
# Default to Ed25519
|
|
40
|
+
metadata["agent_signing_algs_supported"] = ["ed25519"]
|
|
41
|
+
|
|
42
|
+
if request_types_supported:
|
|
43
|
+
metadata["request_types_supported"] = request_types_supported
|
|
44
|
+
else:
|
|
45
|
+
# Default to auth, code, exchange, refresh
|
|
46
|
+
metadata["request_types_supported"] = ["auth", "code", "exchange", "refresh"]
|
|
47
|
+
|
|
48
|
+
if scopes_supported:
|
|
49
|
+
metadata["scopes_supported"] = scopes_supported
|
|
50
|
+
|
|
51
|
+
return metadata
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fetch_metadata(url: str) -> Dict[str, Any]:
|
|
55
|
+
"""Fetch metadata document from URL via HTTPS (sync version for backward compatibility).
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
url: HTTPS URL to metadata document (HTTP allowed for localhost development)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Parsed metadata dictionary
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If URL is not HTTPS (except localhost for development)
|
|
65
|
+
MetadataError: If HTTP request fails
|
|
66
|
+
"""
|
|
67
|
+
import httpx
|
|
68
|
+
from ..errors import MetadataError
|
|
69
|
+
|
|
70
|
+
# Verify HTTPS (allow HTTP for localhost development)
|
|
71
|
+
if not url.startswith("https://"):
|
|
72
|
+
# Allow HTTP for localhost/127.0.0.1 for development
|
|
73
|
+
parsed = httpx.URL(url)
|
|
74
|
+
if parsed.host not in ("localhost", "127.0.0.1", "::1"):
|
|
75
|
+
raise ValueError(f"Metadata URL must use HTTPS (except localhost): {url}")
|
|
76
|
+
|
|
77
|
+
# Fetch metadata
|
|
78
|
+
try:
|
|
79
|
+
response = httpx.get(url, timeout=10.0)
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
return response.json()
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise MetadataError(
|
|
84
|
+
f"Failed to fetch metadata from {url}: {e}",
|
|
85
|
+
metadata_url=url
|
|
86
|
+
) from e
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def fetch_auth_metadata(url: str, http_client=None) -> Dict[str, Any]:
|
|
90
|
+
"""Fetch auth server metadata from URL (async version).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
url: URL to auth server metadata document (e.g., https://auth.example/.well-known/aauth-issuer)
|
|
94
|
+
http_client: Optional HTTP client (default: uses DefaultHTTPClient from keys.jwks)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Parsed auth server metadata dictionary
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
MetadataError: If fetch fails
|
|
101
|
+
"""
|
|
102
|
+
from ..keys.jwks import DefaultHTTPClient
|
|
103
|
+
from ..errors import MetadataError
|
|
104
|
+
|
|
105
|
+
client = http_client or DefaultHTTPClient()
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
return await client.fetch_json(url)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise MetadataError(
|
|
111
|
+
f"Failed to fetch auth server metadata from {url}: {e}",
|
|
112
|
+
metadata_url=url
|
|
113
|
+
)
|
|
114
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Resource metadata handling for AAuth."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_resource_metadata(
|
|
7
|
+
resource_id: str,
|
|
8
|
+
jwks_uri: str,
|
|
9
|
+
resource_token_endpoint: str,
|
|
10
|
+
supported_scopes: Optional[list[str]] = None,
|
|
11
|
+
scope_descriptions: Optional[Dict[str, str]] = None
|
|
12
|
+
) -> Dict[str, Any]:
|
|
13
|
+
"""Generate resource metadata JSON per AAuth spec Section 8.3.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
resource_id: Resource identifier (HTTPS URL)
|
|
17
|
+
jwks_uri: URL to resource's JSON Web Key Set (REQUIRED)
|
|
18
|
+
resource_token_endpoint: Endpoint where agents request resource tokens (REQUIRED per spec)
|
|
19
|
+
supported_scopes: Optional list of supported scope values
|
|
20
|
+
scope_descriptions: Optional dictionary mapping scope names to descriptions
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Resource metadata dictionary with required fields
|
|
24
|
+
|
|
25
|
+
Note:
|
|
26
|
+
Per SPEC.md Section 8.3, resource_token_endpoint is REQUIRED.
|
|
27
|
+
auth_server is NOT in resource metadata - it's only provided in Agent-Auth challenge headers.
|
|
28
|
+
"""
|
|
29
|
+
metadata = {
|
|
30
|
+
"resource": resource_id,
|
|
31
|
+
"jwks_uri": jwks_uri,
|
|
32
|
+
"resource_token_endpoint": resource_token_endpoint
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if supported_scopes:
|
|
36
|
+
metadata["supported_scopes"] = supported_scopes
|
|
37
|
+
|
|
38
|
+
if scope_descriptions:
|
|
39
|
+
metadata["scope_descriptions"] = scope_descriptions
|
|
40
|
+
|
|
41
|
+
return metadata
|
|
42
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Agent-Auth challenge building for resource role."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from ..headers.agent_auth import build_agent_auth_challenge
|
|
5
|
+
from ..tokens.resource_token import create_resource_token
|
|
6
|
+
from ..keys.jwk import calculate_jwk_thumbprint, public_key_to_jwk
|
|
7
|
+
from ..errors import ChallengeError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChallengeBuilder:
|
|
11
|
+
"""Builds Agent-Auth challenges for resources."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
resource_id: str,
|
|
16
|
+
resource_private_key,
|
|
17
|
+
resource_kid: str,
|
|
18
|
+
auth_server: str
|
|
19
|
+
):
|
|
20
|
+
"""Initialize challenge builder.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
resource_id: Resource identifier (HTTPS URL)
|
|
24
|
+
resource_private_key: Resource's private key for signing resource tokens
|
|
25
|
+
resource_kid: Resource's key ID
|
|
26
|
+
auth_server: Auth server identifier (HTTPS URL)
|
|
27
|
+
"""
|
|
28
|
+
self.resource_id = resource_id
|
|
29
|
+
self.resource_private_key = resource_private_key
|
|
30
|
+
self.resource_kid = resource_kid
|
|
31
|
+
self.auth_server = auth_server
|
|
32
|
+
|
|
33
|
+
def build_challenge(
|
|
34
|
+
self,
|
|
35
|
+
require_signature: bool = True,
|
|
36
|
+
require_identity: bool = False,
|
|
37
|
+
require_auth_token: bool = False,
|
|
38
|
+
agent_id: Optional[str] = None,
|
|
39
|
+
agent_public_key=None,
|
|
40
|
+
scope: Optional[str] = None
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Build Agent-Auth challenge header.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
require_signature: Require HTTP signature
|
|
46
|
+
require_identity: Require agent identity
|
|
47
|
+
require_auth_token: Require authorization token
|
|
48
|
+
agent_id: Agent identifier (for resource token)
|
|
49
|
+
agent_public_key: Agent's public key (for resource token)
|
|
50
|
+
scope: Required scope (for resource token)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Agent-Auth header value
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ChallengeError: If challenge cannot be built
|
|
57
|
+
"""
|
|
58
|
+
resource_token = None
|
|
59
|
+
|
|
60
|
+
if require_auth_token:
|
|
61
|
+
if not agent_id or not agent_public_key or not scope:
|
|
62
|
+
raise ChallengeError(
|
|
63
|
+
"agent_id, agent_public_key, and scope required for auth-token challenge"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Create resource token
|
|
67
|
+
agent_jwk = public_key_to_jwk(agent_public_key)
|
|
68
|
+
agent_jkt = calculate_jwk_thumbprint(agent_jwk)
|
|
69
|
+
|
|
70
|
+
resource_token = create_resource_token(
|
|
71
|
+
iss=self.resource_id,
|
|
72
|
+
aud=self.auth_server,
|
|
73
|
+
agent=agent_id,
|
|
74
|
+
agent_jkt=agent_jkt,
|
|
75
|
+
scope=scope,
|
|
76
|
+
private_key=self.resource_private_key,
|
|
77
|
+
kid=self.resource_kid
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return build_agent_auth_challenge(
|
|
81
|
+
require_signature=require_signature,
|
|
82
|
+
require_identity=require_identity,
|
|
83
|
+
require_auth_token=require_auth_token,
|
|
84
|
+
resource_token=resource_token,
|
|
85
|
+
auth_server=self.auth_server if require_auth_token else None
|
|
86
|
+
)
|
|
87
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Resource token issuance for resource role."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from ..tokens.resource_token import create_resource_token
|
|
5
|
+
from ..keys.jwk import calculate_jwk_thumbprint, public_key_to_jwk
|
|
6
|
+
from ..errors import TokenError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResourceTokenIssuer:
|
|
10
|
+
"""Issues resource tokens for agents."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
resource_id: str,
|
|
15
|
+
resource_private_key,
|
|
16
|
+
resource_kid: str,
|
|
17
|
+
auth_server: str
|
|
18
|
+
):
|
|
19
|
+
"""Initialize resource token issuer.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
resource_id: Resource identifier (HTTPS URL)
|
|
23
|
+
resource_private_key: Resource's private key for signing
|
|
24
|
+
resource_kid: Resource's key ID
|
|
25
|
+
auth_server: Auth server identifier (HTTPS URL)
|
|
26
|
+
"""
|
|
27
|
+
self.resource_id = resource_id
|
|
28
|
+
self.resource_private_key = resource_private_key
|
|
29
|
+
self.resource_kid = resource_kid
|
|
30
|
+
self.auth_server = auth_server
|
|
31
|
+
|
|
32
|
+
def issue_token(
|
|
33
|
+
self,
|
|
34
|
+
agent_id: str,
|
|
35
|
+
agent_public_key,
|
|
36
|
+
scope: str,
|
|
37
|
+
exp: Optional[int] = None
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Issue a resource token.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
agent_id: Agent identifier (HTTPS URL)
|
|
43
|
+
agent_public_key: Agent's public signing key
|
|
44
|
+
scope: Space-separated scope values
|
|
45
|
+
exp: Optional expiration timestamp
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Resource token (JWT string)
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
TokenError: If token creation fails
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
# Calculate agent JWK thumbprint
|
|
55
|
+
agent_jwk = public_key_to_jwk(agent_public_key)
|
|
56
|
+
agent_jkt = calculate_jwk_thumbprint(agent_jwk)
|
|
57
|
+
|
|
58
|
+
# Create resource token
|
|
59
|
+
return create_resource_token(
|
|
60
|
+
iss=self.resource_id,
|
|
61
|
+
aud=self.auth_server,
|
|
62
|
+
agent=agent_id,
|
|
63
|
+
agent_jkt=agent_jkt,
|
|
64
|
+
scope=scope,
|
|
65
|
+
private_key=self.resource_private_key,
|
|
66
|
+
kid=self.resource_kid,
|
|
67
|
+
exp=exp
|
|
68
|
+
)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise TokenError(
|
|
71
|
+
f"Failed to issue resource token: {e}",
|
|
72
|
+
token_type="resource+jwt"
|
|
73
|
+
) from e
|
|
74
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Request verification for resource role."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
4
|
+
from ..signing.verifier import verify_signature
|
|
5
|
+
from ..headers.signature_key import parse_signature_key
|
|
6
|
+
from ..headers.signature_input import parse_signature_input
|
|
7
|
+
from ..errors import SignatureError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RequestVerifier:
|
|
11
|
+
"""Verifies incoming requests for resources."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
canonical_authorities: List[str],
|
|
16
|
+
jwks_fetcher: Optional[Callable] = None,
|
|
17
|
+
trusted_auth_servers: Optional[List[str]] = None
|
|
18
|
+
):
|
|
19
|
+
"""Initialize request verifier.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
canonical_authorities: List of canonical authorities (host:port) per SPEC 10.3.1
|
|
23
|
+
jwks_fetcher: Optional JWKS fetcher function
|
|
24
|
+
trusted_auth_servers: Optional list of trusted auth server identifiers
|
|
25
|
+
"""
|
|
26
|
+
self.canonical_authorities = canonical_authorities
|
|
27
|
+
self.jwks_fetcher = jwks_fetcher
|
|
28
|
+
self.trusted_auth_servers = trusted_auth_servers or []
|
|
29
|
+
|
|
30
|
+
def verify_request(
|
|
31
|
+
self,
|
|
32
|
+
method: str,
|
|
33
|
+
target_uri: str,
|
|
34
|
+
headers: Dict[str, str],
|
|
35
|
+
body: Optional[bytes],
|
|
36
|
+
require_identity: bool = False,
|
|
37
|
+
require_auth_token: bool = False
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""Verify incoming request.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
method: HTTP method
|
|
43
|
+
target_uri: Target URI
|
|
44
|
+
headers: Request headers
|
|
45
|
+
body: Request body bytes
|
|
46
|
+
require_identity: Whether agent identity is required
|
|
47
|
+
require_auth_token: Whether auth token is required
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary with verification result:
|
|
51
|
+
- valid: bool
|
|
52
|
+
- agent_id: Optional[str]
|
|
53
|
+
- agent_delegate: Optional[str]
|
|
54
|
+
- user_sub: Optional[str]
|
|
55
|
+
- scopes: Optional[List[str]]
|
|
56
|
+
- error: Optional[str]
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
SignatureError: If verification fails due to invalid format
|
|
60
|
+
"""
|
|
61
|
+
# Extract signature headers
|
|
62
|
+
signature_input_header = headers.get("signature-input") or headers.get("Signature-Input")
|
|
63
|
+
signature_header = headers.get("signature") or headers.get("Signature")
|
|
64
|
+
signature_key_header = headers.get("signature-key") or headers.get("Signature-Key")
|
|
65
|
+
|
|
66
|
+
if not (signature_input_header and signature_header and signature_key_header):
|
|
67
|
+
return {
|
|
68
|
+
"valid": False,
|
|
69
|
+
"error": "Missing signature headers"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Parse Signature-Key to determine scheme
|
|
73
|
+
try:
|
|
74
|
+
parsed_key = parse_signature_key(signature_key_header)
|
|
75
|
+
scheme = parsed_key["scheme"]
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return {
|
|
78
|
+
"valid": False,
|
|
79
|
+
"error": f"Invalid Signature-Key: {e}"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Check canonical authority
|
|
83
|
+
from urllib.parse import urlparse
|
|
84
|
+
parsed_uri = urlparse(target_uri)
|
|
85
|
+
request_authority = parsed_uri.netloc
|
|
86
|
+
|
|
87
|
+
if request_authority not in self.canonical_authorities:
|
|
88
|
+
return {
|
|
89
|
+
"valid": False,
|
|
90
|
+
"error": f"Request authority {request_authority} not in canonical authorities"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Verify signature
|
|
94
|
+
try:
|
|
95
|
+
is_valid = verify_signature(
|
|
96
|
+
method=method,
|
|
97
|
+
target_uri=target_uri,
|
|
98
|
+
headers=headers,
|
|
99
|
+
body=body,
|
|
100
|
+
signature_input_header=signature_input_header,
|
|
101
|
+
signature_header=signature_header,
|
|
102
|
+
signature_key_header=signature_key_header,
|
|
103
|
+
jwks_fetcher=self.jwks_fetcher
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if not is_valid:
|
|
107
|
+
return {
|
|
108
|
+
"valid": False,
|
|
109
|
+
"error": "Signature verification failed"
|
|
110
|
+
}
|
|
111
|
+
except SignatureError as e:
|
|
112
|
+
return {
|
|
113
|
+
"valid": False,
|
|
114
|
+
"error": str(e)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Extract identity/authorization info based on scheme
|
|
118
|
+
result = {
|
|
119
|
+
"valid": True,
|
|
120
|
+
"agent_id": None,
|
|
121
|
+
"agent_delegate": None,
|
|
122
|
+
"user_sub": None,
|
|
123
|
+
"scopes": None
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if scheme == "jwks":
|
|
127
|
+
# Extract agent ID from Signature-Key
|
|
128
|
+
params = parsed_key["params"]
|
|
129
|
+
result["agent_id"] = params.get("id")
|
|
130
|
+
|
|
131
|
+
elif scheme == "jwt":
|
|
132
|
+
# Extract from JWT token
|
|
133
|
+
jwt_token = parsed_key["params"].get("jwt")
|
|
134
|
+
if jwt_token:
|
|
135
|
+
try:
|
|
136
|
+
import jwt as pyjwt
|
|
137
|
+
payload = pyjwt.decode(jwt_token, options={"verify_signature": False})
|
|
138
|
+
|
|
139
|
+
# Determine token type
|
|
140
|
+
header = pyjwt.get_unverified_header(jwt_token)
|
|
141
|
+
typ = header.get("typ")
|
|
142
|
+
|
|
143
|
+
if typ == "agent+jwt":
|
|
144
|
+
result["agent_id"] = payload.get("iss")
|
|
145
|
+
result["agent_delegate"] = payload.get("sub")
|
|
146
|
+
elif typ == "auth+jwt":
|
|
147
|
+
result["agent_id"] = payload.get("agent")
|
|
148
|
+
result["agent_delegate"] = payload.get("agent_delegate")
|
|
149
|
+
result["user_sub"] = payload.get("sub")
|
|
150
|
+
scope_str = payload.get("scope")
|
|
151
|
+
if scope_str:
|
|
152
|
+
result["scopes"] = scope_str.split()
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
# Check requirements
|
|
157
|
+
if require_identity and not result["agent_id"]:
|
|
158
|
+
return {
|
|
159
|
+
"valid": False,
|
|
160
|
+
"error": "Agent identity required but not present"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if require_auth_token and not result.get("scopes"):
|
|
164
|
+
return {
|
|
165
|
+
"valid": False,
|
|
166
|
+
"error": "Auth token required but not present"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Signature algorithm support for AAuth."""
|
|
2
|
+
|
|
3
|
+
# Supported algorithms per AAuth spec Section 10.2
|
|
4
|
+
# MUST support ed25519, MAY support others
|
|
5
|
+
|
|
6
|
+
ED25519 = "ed25519"
|
|
7
|
+
RSA_PSS_SHA512 = "rsa-pss-sha512"
|
|
8
|
+
RSA_PSS_SHA256 = "rsa-pss-sha256"
|
|
9
|
+
ECDSA_P256_SHA256 = "ecdsa-p256-sha256"
|
|
10
|
+
ECDSA_P384_SHA384 = "ecdsa-p384-sha384"
|
|
11
|
+
|
|
12
|
+
# Required algorithm
|
|
13
|
+
REQUIRED_ALGORITHM = ED25519
|
|
14
|
+
|
|
15
|
+
# All supported algorithms
|
|
16
|
+
SUPPORTED_ALGORITHMS = [
|
|
17
|
+
ED25519,
|
|
18
|
+
RSA_PSS_SHA512,
|
|
19
|
+
RSA_PSS_SHA256,
|
|
20
|
+
ECDSA_P256_SHA256,
|
|
21
|
+
ECDSA_P384_SHA384,
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_supported(algorithm: str) -> bool:
|
|
26
|
+
"""Check if algorithm is supported.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
algorithm: Algorithm name
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if supported, False otherwise
|
|
33
|
+
"""
|
|
34
|
+
return algorithm.lower() in [alg.lower() for alg in SUPPORTED_ALGORITHMS]
|
|
35
|
+
|