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/__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
|
+
]
|
aauth/agent/__init__.py
ADDED
|
@@ -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,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
|
+
|