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 ADDED
@@ -0,0 +1,137 @@
1
+ """AAuth - Agent Authentication Protocol implementation for Python."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Errors
6
+ from .errors import (
7
+ AAuthError,
8
+ SignatureError,
9
+ TokenError,
10
+ ChallengeError,
11
+ MetadataError,
12
+ JWKSError
13
+ )
14
+
15
+ # HTTP abstraction
16
+ from .http.request import AAuthRequest
17
+ from .http.response import AAuthResponse
18
+
19
+ # Key management
20
+ from .keys.keypair import generate_ed25519_keypair
21
+ from .keys.jwk import (
22
+ public_key_to_jwk,
23
+ jwk_to_public_key,
24
+ calculate_jwk_thumbprint,
25
+ generate_jwks
26
+ )
27
+ from .keys.jwks import JWKSFetcher, JWKSCache, DefaultHTTPClient
28
+
29
+ # HTTP Message Signing
30
+ from .signing.signer import sign_request
31
+ from .signing.verifier import verify_signature
32
+ from .signing.algorithms import (
33
+ ED25519,
34
+ RSA_PSS_SHA512,
35
+ RSA_PSS_SHA256,
36
+ ECDSA_P256_SHA256,
37
+ ECDSA_P384_SHA384,
38
+ SUPPORTED_ALGORITHMS,
39
+ is_supported
40
+ )
41
+
42
+ # Token handling
43
+ from .tokens.agent_token import create_agent_token, verify_agent_token
44
+ from .tokens.auth_token import create_auth_token, parse_token_claims, verify_token
45
+ from .tokens.resource_token import create_resource_token
46
+
47
+ # Header handling
48
+ from .headers.signature_key import build_signature_key_header, parse_signature_key
49
+ from .headers.signature_input import build_signature_input_header, parse_signature_input
50
+ from .headers.signature import build_signature_header, parse_signature
51
+ from .headers.agent_auth import parse_agent_auth_header, build_agent_auth_challenge
52
+
53
+ # Metadata
54
+ from .metadata.agent import generate_agent_metadata
55
+ from .metadata.resource import generate_resource_metadata
56
+ from .metadata.auth_server import generate_auth_metadata, fetch_auth_metadata, fetch_metadata
57
+
58
+ # Agent role
59
+ from .agent.signer import AgentRequestSigner
60
+ from .agent.challenge_handler import ChallengeHandler
61
+
62
+ # Resource role
63
+ from .resource.verifier import RequestVerifier
64
+ from .resource.challenge_builder import ChallengeBuilder
65
+ from .resource.token_issuer import ResourceTokenIssuer
66
+
67
+ __all__ = [
68
+ # Version
69
+ "__version__",
70
+
71
+ # Errors
72
+ "AAuthError",
73
+ "SignatureError",
74
+ "TokenError",
75
+ "ChallengeError",
76
+ "MetadataError",
77
+ "JWKSError",
78
+
79
+ # HTTP abstraction
80
+ "AAuthRequest",
81
+ "AAuthResponse",
82
+
83
+ # Key management
84
+ "generate_ed25519_keypair",
85
+ "public_key_to_jwk",
86
+ "jwk_to_public_key",
87
+ "calculate_jwk_thumbprint",
88
+ "generate_jwks",
89
+ "JWKSFetcher",
90
+ "JWKSCache",
91
+ "DefaultHTTPClient",
92
+
93
+ # HTTP Message Signing
94
+ "sign_request",
95
+ "verify_signature",
96
+ "ED25519",
97
+ "RSA_PSS_SHA512",
98
+ "RSA_PSS_SHA256",
99
+ "ECDSA_P256_SHA256",
100
+ "ECDSA_P384_SHA384",
101
+ "SUPPORTED_ALGORITHMS",
102
+ "is_supported",
103
+
104
+ # Token handling
105
+ "create_agent_token",
106
+ "verify_agent_token",
107
+ "create_auth_token",
108
+ "parse_token_claims",
109
+ "verify_token",
110
+ "create_resource_token",
111
+
112
+ # Header handling
113
+ "build_signature_key_header",
114
+ "parse_signature_key",
115
+ "build_signature_input_header",
116
+ "parse_signature_input",
117
+ "build_signature_header",
118
+ "parse_signature",
119
+ "parse_agent_auth_header",
120
+ "build_agent_auth_challenge",
121
+
122
+ # Metadata
123
+ "generate_agent_metadata",
124
+ "generate_resource_metadata",
125
+ "generate_auth_metadata",
126
+ "fetch_auth_metadata",
127
+ "fetch_metadata",
128
+
129
+ # Agent role
130
+ "AgentRequestSigner",
131
+ "ChallengeHandler",
132
+
133
+ # Resource role
134
+ "RequestVerifier",
135
+ "ChallengeBuilder",
136
+ "ResourceTokenIssuer",
137
+ ]
@@ -0,0 +1,2 @@
1
+ """Agent role implementation for AAuth."""
2
+
@@ -0,0 +1,61 @@
1
+ """Agent-Auth challenge handling for agent role."""
2
+
3
+ from typing import Dict, Any, Optional
4
+ from ..headers.agent_auth import parse_agent_auth_header
5
+ from ..errors import ChallengeError
6
+
7
+
8
+ class ChallengeHandler:
9
+ """Handles Agent-Auth challenges from resources."""
10
+
11
+ def parse_challenge(self, agent_auth_header: str) -> Dict[str, Any]:
12
+ """Parse Agent-Auth challenge header.
13
+
14
+ Args:
15
+ agent_auth_header: Agent-Auth header value
16
+
17
+ Returns:
18
+ Parsed challenge parameters
19
+
20
+ Raises:
21
+ ChallengeError: If parsing fails
22
+ """
23
+ return parse_agent_auth_header(agent_auth_header)
24
+
25
+ def determine_response_scheme(
26
+ self,
27
+ challenge: Dict[str, Any],
28
+ has_agent_token: bool = False,
29
+ has_auth_token: bool = False
30
+ ) -> str:
31
+ """Determine which signature scheme to use in response to challenge.
32
+
33
+ Args:
34
+ challenge: Parsed challenge parameters
35
+ has_agent_token: Whether agent has an agent token
36
+ has_auth_token: Whether agent has an auth token
37
+
38
+ Returns:
39
+ Signature scheme to use ("hwk", "jwks", or "jwt")
40
+
41
+ Raises:
42
+ ChallengeError: If challenge cannot be satisfied
43
+ """
44
+ if challenge.get("auth_token"):
45
+ if has_auth_token:
46
+ return "jwt"
47
+ else:
48
+ raise ChallengeError(
49
+ "Challenge requires auth token but agent doesn't have one",
50
+ challenge_type="auth-token"
51
+ )
52
+
53
+ if challenge.get("identity"):
54
+ if has_agent_token:
55
+ return "jwt" # Use agent token
56
+ else:
57
+ return "jwks" # Use agent server identity
58
+
59
+ # Just signature required
60
+ return "hwk"
61
+
aauth/agent/signer.py ADDED
@@ -0,0 +1,78 @@
1
+ """High-level request signing for agent role."""
2
+
3
+ from typing import Dict, Any, Optional
4
+ from urllib.parse import urlparse
5
+ from ..signing.signer import sign_request
6
+ from ..headers.signature_key import build_signature_key_header
7
+ from ..errors import SignatureError
8
+
9
+
10
+ class AgentRequestSigner:
11
+ """High-level request signer for agents."""
12
+
13
+ def __init__(
14
+ self,
15
+ private_key,
16
+ agent_id: Optional[str] = None,
17
+ agent_token: Optional[str] = None,
18
+ kid: str = "key-1"
19
+ ):
20
+ """Initialize agent request signer.
21
+
22
+ Args:
23
+ private_key: Agent's private signing key
24
+ agent_id: Agent identifier (HTTPS URL) - required for jwks scheme
25
+ agent_token: Agent token (JWT) - required for jwt scheme
26
+ kid: Key ID for jwks scheme
27
+ """
28
+ self.private_key = private_key
29
+ self.agent_id = agent_id
30
+ self.agent_token = agent_token
31
+ self.kid = kid
32
+
33
+ def sign_request(
34
+ self,
35
+ method: str,
36
+ target_uri: str,
37
+ headers: Dict[str, str],
38
+ body: Optional[bytes] = None,
39
+ sig_scheme: str = "hwk"
40
+ ) -> Dict[str, str]:
41
+ """Sign an HTTP request.
42
+
43
+ Args:
44
+ method: HTTP method
45
+ target_uri: Target URI
46
+ headers: Request headers (will be modified)
47
+ body: Request body bytes
48
+ sig_scheme: Signature scheme ("hwk", "jwks", or "jwt")
49
+
50
+ Returns:
51
+ Dictionary with signature headers
52
+
53
+ Raises:
54
+ SignatureError: If signing fails
55
+ """
56
+ kwargs = {}
57
+
58
+ if sig_scheme == "jwks":
59
+ if not self.agent_id:
60
+ raise SignatureError("agent_id required for jwks scheme")
61
+ kwargs["id"] = self.agent_id
62
+ kwargs["kid"] = self.kid
63
+
64
+ elif sig_scheme == "jwt":
65
+ if not self.agent_token:
66
+ raise SignatureError("agent_token required for jwt scheme")
67
+ kwargs["jwt"] = self.agent_token
68
+
69
+ return sign_request(
70
+ method=method,
71
+ target_uri=target_uri,
72
+ headers=headers,
73
+ body=body,
74
+ private_key=self.private_key,
75
+ sig_scheme=sig_scheme,
76
+ **kwargs
77
+ )
78
+
aauth/errors.py ADDED
@@ -0,0 +1,51 @@
1
+ """Custom exceptions for AAuth."""
2
+
3
+
4
+ class AAuthError(Exception):
5
+ """Base exception for all AAuth errors."""
6
+ pass
7
+
8
+
9
+ class SignatureError(AAuthError):
10
+ """HTTP signature validation or creation error."""
11
+
12
+ def __init__(self, message: str, details: dict = None):
13
+ super().__init__(message)
14
+ self.details = details or {}
15
+
16
+
17
+ class TokenError(AAuthError):
18
+ """Token validation or creation error."""
19
+
20
+ def __init__(self, message: str, token_type: str = None, details: dict = None):
21
+ super().__init__(message)
22
+ self.token_type = token_type
23
+ self.details = details or {}
24
+
25
+
26
+ class ChallengeError(AAuthError):
27
+ """Agent-Auth challenge parsing or building error."""
28
+
29
+ def __init__(self, message: str, challenge_type: str = None, details: dict = None):
30
+ super().__init__(message)
31
+ self.challenge_type = challenge_type
32
+ self.details = details or {}
33
+
34
+
35
+ class MetadataError(AAuthError):
36
+ """Metadata discovery or parsing error."""
37
+
38
+ def __init__(self, message: str, metadata_url: str = None, details: dict = None):
39
+ super().__init__(message)
40
+ self.metadata_url = metadata_url
41
+ self.details = details or {}
42
+
43
+
44
+ class JWKSError(AAuthError):
45
+ """JWKS fetching or parsing error."""
46
+
47
+ def __init__(self, message: str, jwks_uri: str = None, details: dict = None):
48
+ super().__init__(message)
49
+ self.jwks_uri = jwks_uri
50
+ self.details = details or {}
51
+
@@ -0,0 +1,2 @@
1
+ """HTTP header parsing and building for AAuth."""
2
+
@@ -0,0 +1,146 @@
1
+ """Agent-Auth header parsing and building for AAuth."""
2
+
3
+ import re
4
+ from typing import Dict, Any, Optional, List
5
+ from ..errors import ChallengeError
6
+
7
+
8
+ def parse_agent_auth_header(header_value: str) -> Dict[str, Any]:
9
+ """Parse Agent-Auth header per AAuth spec Section 4.
10
+
11
+ Agent-Auth header uses RFC 8941 structured fields format:
12
+ - httpsig (required)
13
+ - identity=?1 (optional boolean)
14
+ - auth-token (optional bare token)
15
+ - resource_token="..." (optional string)
16
+ - auth_server="..." (optional string)
17
+ - user_interaction="..." (optional string)
18
+ - algs=("ed25519" "rsa-pss-sha512") (optional inner list)
19
+
20
+ Args:
21
+ header_value: Agent-Auth header value
22
+
23
+ Returns:
24
+ Dictionary with parsed parameters:
25
+ - httpsig: bool (always True)
26
+ - identity: Optional[bool]
27
+ - auth_token: Optional[bool]
28
+ - resource_token: Optional[str]
29
+ - auth_server: Optional[str]
30
+ - user_interaction: Optional[str]
31
+ - algs: Optional[List[str]]
32
+
33
+ Raises:
34
+ ChallengeError: If header format is invalid
35
+ """
36
+ try:
37
+ result = {
38
+ "httpsig": True,
39
+ "identity": None,
40
+ "auth_token": False,
41
+ "resource_token": None,
42
+ "auth_server": None,
43
+ "user_interaction": None,
44
+ "algs": None
45
+ }
46
+
47
+ # Parse structured fields format
48
+ # Basic format: httpsig; identity=?1; auth-token; resource_token="..."; auth_server="..."
49
+
50
+ # Check for httpsig (required)
51
+ if "httpsig" not in header_value:
52
+ raise ChallengeError("Agent-Auth header must include 'httpsig'")
53
+
54
+ # Parse identity parameter (?1 = true)
55
+ if "identity=?1" in header_value or "identity=?1;" in header_value:
56
+ result["identity"] = True
57
+ elif "identity=" in header_value:
58
+ # Could be ?0 or other value
59
+ match = re.search(r'identity=(\?[01])', header_value)
60
+ if match:
61
+ result["identity"] = match.group(1) == "?1"
62
+
63
+ # Parse auth-token (bare token, no value)
64
+ if re.search(r'\bauth-token\b', header_value):
65
+ result["auth_token"] = True
66
+
67
+ # Parse resource_token="..."
68
+ resource_token_match = re.search(r'resource_token="([^"]+)"', header_value)
69
+ if resource_token_match:
70
+ result["resource_token"] = resource_token_match.group(1)
71
+
72
+ # Parse auth_server="..."
73
+ auth_server_match = re.search(r'auth_server="([^"]+)"', header_value)
74
+ if auth_server_match:
75
+ result["auth_server"] = auth_server_match.group(1)
76
+
77
+ # Parse user_interaction="..."
78
+ user_interaction_match = re.search(r'user_interaction="([^"]+)"', header_value)
79
+ if user_interaction_match:
80
+ result["user_interaction"] = user_interaction_match.group(1)
81
+
82
+ # Parse algs=("ed25519" "rsa-pss-sha512") (inner list)
83
+ algs_match = re.search(r'algs=\("([^"]+)"(?:\s+"([^"]+)")*\)', header_value)
84
+ if algs_match:
85
+ # Extract all quoted strings
86
+ algs = re.findall(r'"([^"]+)"', algs_match.group(0))
87
+ result["algs"] = algs
88
+
89
+ return result
90
+
91
+ except ChallengeError:
92
+ raise
93
+ except Exception as e:
94
+ raise ChallengeError(f"Failed to parse Agent-Auth header: {e}") from e
95
+
96
+
97
+ def build_agent_auth_challenge(
98
+ require_signature: bool = True,
99
+ require_identity: bool = False,
100
+ require_auth_token: bool = False,
101
+ resource_token: Optional[str] = None,
102
+ auth_server: Optional[str] = None,
103
+ user_interaction: Optional[str] = None,
104
+ algs: Optional[List[str]] = None
105
+ ) -> str:
106
+ """Build Agent-Auth challenge header per AAuth spec Section 4.
107
+
108
+ Args:
109
+ require_signature: Require HTTP signature (default: True)
110
+ require_identity: Require agent identity verification
111
+ require_auth_token: Require authorization token
112
+ resource_token: Resource token (required if require_auth_token=True)
113
+ auth_server: Auth server URL (required if require_auth_token=True)
114
+ user_interaction: User interaction URL
115
+ algs: List of supported algorithms
116
+
117
+ Returns:
118
+ Agent-Auth header value
119
+
120
+ Raises:
121
+ ChallengeError: If parameters are invalid
122
+ """
123
+ if not require_signature:
124
+ raise ChallengeError("Agent-Auth header must require httpsig")
125
+
126
+ parts = ["httpsig"]
127
+
128
+ if require_identity:
129
+ parts.append("identity=?1")
130
+
131
+ if require_auth_token:
132
+ parts.append("auth-token")
133
+ if resource_token:
134
+ parts.append(f'resource_token="{resource_token}"')
135
+ if auth_server:
136
+ parts.append(f'auth_server="{auth_server}"')
137
+
138
+ if user_interaction:
139
+ parts.append(f'user_interaction="{user_interaction}"')
140
+
141
+ if algs:
142
+ algs_str = ' '.join([f'"{alg}"' for alg in algs])
143
+ parts.append(f'algs=({algs_str})')
144
+
145
+ return "; ".join(parts)
146
+
@@ -0,0 +1,53 @@
1
+ """Signature header parsing and building for AAuth."""
2
+
3
+ import re
4
+ import base64
5
+ from typing import Optional
6
+
7
+
8
+ def build_signature_header(signature_bytes: bytes, label: str = "sig1") -> str:
9
+ """Build Signature header per RFC 9421 Section 4.2.
10
+
11
+ Args:
12
+ signature_bytes: Raw signature bytes
13
+ label: Signature label (default: "sig1")
14
+
15
+ Returns:
16
+ Signature header value
17
+ """
18
+ signature_b64 = base64.urlsafe_b64encode(signature_bytes).decode('utf-8').rstrip('=')
19
+ return f'{label}=:{signature_b64}:'
20
+
21
+
22
+ def parse_signature(header_value: str, label: Optional[str] = None) -> bytes:
23
+ """Parse Signature header to extract signature bytes.
24
+
25
+ Args:
26
+ header_value: Signature header value
27
+ label: Optional expected label (for validation)
28
+
29
+ Returns:
30
+ Signature bytes
31
+
32
+ Raises:
33
+ ValueError: If header format is invalid or label doesn't match
34
+ """
35
+ match = re.search(r'(\w+)=:([A-Za-z0-9_-]+):', header_value)
36
+ if not match:
37
+ raise ValueError(f"Invalid Signature format: {header_value}")
38
+
39
+ found_label = match.group(1)
40
+ signature_b64 = match.group(2)
41
+
42
+ if label and found_label != label:
43
+ raise ValueError(f"Label mismatch: expected {label}, got {found_label}")
44
+
45
+ # Add padding if needed
46
+ signature_b64 += '=' * (4 - len(signature_b64) % 4)
47
+
48
+ try:
49
+ signature_bytes = base64.urlsafe_b64decode(signature_b64)
50
+ return signature_bytes
51
+ except Exception as e:
52
+ raise ValueError(f"Failed to decode signature: {e}")
53
+
@@ -0,0 +1,65 @@
1
+ """Signature-Input header parsing and building for AAuth."""
2
+
3
+ import re
4
+ import time
5
+ from typing import List, Dict, Any, Optional
6
+
7
+
8
+ def build_signature_input_header(
9
+ covered_components: List[str],
10
+ label: str = "sig1",
11
+ created: Optional[int] = None
12
+ ) -> str:
13
+ """Build Signature-Input header per RFC 9421 Section 4.1.
14
+
15
+ Args:
16
+ covered_components: List of component names to cover
17
+ label: Signature label (default: "sig1")
18
+ created: Creation timestamp (Unix time). If None, uses current time.
19
+
20
+ Returns:
21
+ Signature-Input header value
22
+ """
23
+ if created is None:
24
+ created = int(time.time())
25
+
26
+ # Format components: "@method" "@authority" "content-type"
27
+ component_list = ' '.join([
28
+ f'"{comp}"' for comp in covered_components
29
+ ])
30
+
31
+ return f'{label}=({component_list});created={created}'
32
+
33
+
34
+ def parse_signature_input(header_value: str) -> tuple[List[str], Dict[str, Any]]:
35
+ """Parse Signature-Input header to extract covered components and parameters.
36
+
37
+ Args:
38
+ header_value: Signature-Input header value
39
+
40
+ Returns:
41
+ Tuple of (components list, parameters dict)
42
+
43
+ Raises:
44
+ ValueError: If header format is invalid
45
+ """
46
+ match = re.match(r'(\w+)=\((.*)\)(?:;(.*))?', header_value)
47
+ if not match:
48
+ raise ValueError(f"Invalid Signature-Input format: {header_value}")
49
+
50
+ label = match.group(1)
51
+ components_str = match.group(2)
52
+ params_str = match.group(3) or ""
53
+
54
+ components = []
55
+ for match in re.finditer(r'"([^"]+)"', components_str):
56
+ components.append(match.group(1))
57
+
58
+ params = {}
59
+ for match in re.finditer(r'(\w+)=([^\s;]+)', params_str):
60
+ key = match.group(1)
61
+ value = match.group(2).strip('"')
62
+ params[key] = value
63
+
64
+ return components, params
65
+