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.
@@ -0,0 +1,193 @@
1
+ """Signature base construction for HTTP Message Signing (RFC 9421)."""
2
+
3
+ from typing import List, Tuple, Dict, Optional
4
+ from urllib.parse import urlparse
5
+ import base64
6
+ import hashlib
7
+
8
+
9
+ def build_signature_base(
10
+ method: str,
11
+ authority: str,
12
+ path: str,
13
+ query: Optional[str],
14
+ headers: Dict[str, str],
15
+ body: Optional[bytes],
16
+ signature_key_header: str,
17
+ covered_components: Optional[List[str]] = None,
18
+ signature_params: Optional[str] = None
19
+ ) -> str:
20
+ """Build signature base string per RFC 9421 Section 2.5.
21
+
22
+ Args:
23
+ method: HTTP method
24
+ authority: Canonical authority (host:port)
25
+ path: Request path
26
+ query: Query string (without leading "?")
27
+ headers: Request headers
28
+ body: Request body bytes (None if no body)
29
+ signature_key_header: Signature-Key header value
30
+ covered_components: Optional list of components to cover (auto-detected if None)
31
+ signature_params: Signature-Input header value (required for @signature-params line)
32
+
33
+ Returns:
34
+ Signature base string
35
+ """
36
+ # Auto-detect covered components if not provided
37
+ if covered_components is None:
38
+ covered_components = _determine_covered_components(query, body, additional_components=None)
39
+
40
+ # Build component list
41
+ components: List[Tuple[str, str]] = []
42
+
43
+ for component_name in covered_components:
44
+ if component_name == "@method":
45
+ components.append(("@method", method))
46
+ elif component_name == "@authority":
47
+ components.append(("@authority", authority))
48
+ elif component_name == "@path":
49
+ components.append(("@path", path))
50
+ elif component_name == "@query":
51
+ if query:
52
+ # RFC 9421 Section 2.2.7: @query value MUST include leading ?
53
+ components.append(("@query", f"?{query}"))
54
+ else:
55
+ raise ValueError("@query component specified but no query string present")
56
+ elif component_name == "content-type":
57
+ if body:
58
+ content_type = _get_header(headers, "content-type")
59
+ if content_type:
60
+ components.append(("content-type", content_type))
61
+ else:
62
+ raise ValueError("content-type component required but header missing")
63
+ else:
64
+ raise ValueError("content-type component specified but no body present")
65
+ elif component_name == "content-digest":
66
+ if body:
67
+ content_digest = _get_header(headers, "content-digest")
68
+ if content_digest:
69
+ components.append(("content-digest", content_digest))
70
+ else:
71
+ raise ValueError("content-digest component required but header missing")
72
+ else:
73
+ raise ValueError("content-digest component specified but no body present")
74
+ elif component_name == "signature-key":
75
+ components.append(("signature-key", signature_key_header))
76
+ elif component_name == "nonce":
77
+ nonce_value = _get_header(headers, "nonce")
78
+ if nonce_value:
79
+ components.append(("nonce", nonce_value))
80
+ else:
81
+ raise ValueError("nonce component required but Nonce header missing")
82
+ else:
83
+ raise ValueError(f"Unknown component: {component_name}")
84
+
85
+ # Build signature base (RFC 9421 Section 2.5)
86
+ signature_base_parts = []
87
+ for component_name, component_value in components:
88
+ if component_name.startswith("@"):
89
+ signature_base_parts.append(f'"{component_name}": {component_value}')
90
+ else:
91
+ header_name = component_name.lower()
92
+ signature_base_parts.append(f'"{header_name}": {component_value}')
93
+
94
+ # Add @signature-params as the FINAL line (RFC 9421 Section 2.5 requirement)
95
+ # The @signature-params line contains the Signature-Input header value (without the label prefix)
96
+ # RFC 9421 Section 2.5: @signature-params MUST be present
97
+ if not signature_params:
98
+ raise ValueError("signature_params is required for valid signature base")
99
+ signature_base_parts.append(f'"@signature-params": {signature_params}')
100
+
101
+ signature_base = "\n".join(signature_base_parts)
102
+
103
+ return signature_base
104
+
105
+
106
+ def _determine_covered_components(
107
+ query: Optional[str],
108
+ body: Optional[bytes],
109
+ additional_components: Optional[List[str]] = None
110
+ ) -> List[str]:
111
+ """Determine covered components based on request structure.
112
+
113
+ Per AAuth spec Section 10.3:
114
+ - Always: @method, @authority, @path, signature-key
115
+ - If query present: @query
116
+ - Body components (content-type, content-digest) are opt-in via additional_components
117
+
118
+ Args:
119
+ query: Query string (None if no query)
120
+ body: Request body (None if no body)
121
+ additional_components: Optional list of additional components to include (e.g., ["content-type", "content-digest", "nonce"])
122
+
123
+ Returns:
124
+ List of component names
125
+ """
126
+ components = ["@method", "@authority", "@path"]
127
+
128
+ if query:
129
+ components.append("@query")
130
+
131
+ # Body components are opt-in via additional_components
132
+ # Only add if explicitly requested
133
+ if additional_components:
134
+ components.extend(additional_components)
135
+
136
+ # signature-key MUST always be included
137
+ components.append("signature-key")
138
+
139
+ return components
140
+
141
+
142
+ def _get_header(headers: Dict[str, str], name: str) -> Optional[str]:
143
+ """Get header value (case-insensitive).
144
+
145
+ Args:
146
+ headers: Headers dictionary
147
+ name: Header name (case-insensitive)
148
+
149
+ Returns:
150
+ Header value or None
151
+ """
152
+ name_lower = name.lower()
153
+ for key, value in headers.items():
154
+ if key.lower() == name_lower:
155
+ return value
156
+ return None
157
+
158
+
159
+ def build_signature_params(
160
+ covered_components: List[str],
161
+ created: int,
162
+ keyid: Optional[str] = None
163
+ ) -> str:
164
+ """Build the Signature-Input value (the part after the label).
165
+
166
+ Args:
167
+ covered_components: List of component names
168
+ created: Creation timestamp (Unix time)
169
+ keyid: Optional key identifier
170
+
171
+ Returns:
172
+ Signature params string: ("@method" "@authority" ...);created=1234567890
173
+ """
174
+ components_str = " ".join(f'"{c}"' for c in covered_components)
175
+ params = f"({components_str});created={created}"
176
+ if keyid:
177
+ params += f';keyid="{keyid}"'
178
+ return params
179
+
180
+
181
+ def calculate_content_digest(body: bytes) -> str:
182
+ """Calculate Content-Digest header value per RFC 9530.
183
+
184
+ Args:
185
+ body: Request body bytes
186
+
187
+ Returns:
188
+ Content-Digest header value (e.g., "sha-256=:...:")
189
+ """
190
+ digest = hashlib.sha256(body).digest()
191
+ digest_b64 = base64.b64encode(digest).decode('ascii')
192
+ return f"sha-256=:{digest_b64}:"
193
+
@@ -0,0 +1,138 @@
1
+ """HTTP request signing for AAuth."""
2
+
3
+ from typing import Dict, Any, Optional, List
4
+ from urllib.parse import urlparse
5
+ import time
6
+ from ..headers.signature_key import build_signature_key_header
7
+ from ..headers.signature_input import build_signature_input_header
8
+ from ..headers.signature import build_signature_header
9
+ from ..signing.signature_base import build_signature_base, calculate_content_digest, build_signature_params
10
+ from ..errors import SignatureError
11
+
12
+
13
+ def sign_request(
14
+ method: str,
15
+ target_uri: str,
16
+ headers: Dict[str, str],
17
+ body: Optional[bytes],
18
+ private_key,
19
+ sig_scheme: str = "hwk",
20
+ additional_signature_components: Optional[List[str]] = None,
21
+ **kwargs
22
+ ) -> Dict[str, str]:
23
+ """Sign an HTTP request using HTTP Message Signatures (RFC 9421).
24
+
25
+ Args:
26
+ method: HTTP method (GET, POST, etc.)
27
+ target_uri: Target URI
28
+ headers: Request headers dictionary (will be modified)
29
+ body: Request body bytes (None if no body)
30
+ private_key: Ed25519 private key
31
+ sig_scheme: Signature scheme - "hwk", "jwks", or "jwt"
32
+ **kwargs: Additional parameters for signature schemes:
33
+ - For "jwks": id (required), kid (required), well-known (optional)
34
+ - For "jwt": jwt (required)
35
+
36
+ Returns:
37
+ Dictionary with Signature-Input, Signature, and Signature-Key headers
38
+
39
+ Raises:
40
+ SignatureError: If signing fails
41
+ """
42
+ try:
43
+ # Parse URI for derived components
44
+ parsed_uri = urlparse(target_uri)
45
+ authority = parsed_uri.netloc
46
+ path = parsed_uri.path or "/"
47
+ query_string = parsed_uri.query if parsed_uri.query else None
48
+
49
+ # Build Signature-Key header first (needed for signature-key component)
50
+ # Use label "sig1" to match Signature-Input and Signature headers
51
+ signature_key_header = build_signature_key_header(
52
+ sig_scheme=sig_scheme,
53
+ private_key=private_key,
54
+ label="sig1",
55
+ **kwargs
56
+ )
57
+
58
+ # Add Signature-Key to headers (needed for signature-key component)
59
+ headers["Signature-Key"] = signature_key_header
60
+
61
+ # Determine body components to include (opt-in only, per SPEC_NOTES.md)
62
+ # Body components are NOT automatic - only included if explicitly requested
63
+ # via additional_signature_components parameter (typically from server metadata)
64
+ body_components = []
65
+ if body and additional_signature_components:
66
+ # Only add if explicitly requested via additional_signature_components
67
+ for comp in additional_signature_components:
68
+ if comp in ("content-type", "content-digest"):
69
+ body_components.append(comp)
70
+
71
+ # Add Content-Digest header if needed and not already present (RFC 9530)
72
+ # Only calculate Content-Digest if it's in body_components but not in headers
73
+ if "content-digest" in body_components and "Content-Digest" not in headers:
74
+ content_digest = calculate_content_digest(body)
75
+ headers["Content-Digest"] = content_digest
76
+
77
+ # Add content-type header if needed and not already present
78
+ if "content-type" in body_components and "Content-Type" not in headers:
79
+ headers["Content-Type"] = "application/octet-stream"
80
+
81
+ # Check for Nonce header (per SPEC.md Section 10.5)
82
+ # Nonce MUST be included if present, regardless of body or additional_signature_components
83
+ if "Nonce" in headers:
84
+ body_components.append("nonce")
85
+
86
+ # Determine covered components
87
+ from ..signing.signature_base import _determine_covered_components
88
+ covered_components = _determine_covered_components(
89
+ query_string,
90
+ body,
91
+ additional_components=body_components
92
+ )
93
+
94
+ # Build signature params using new function
95
+ created = int(time.time())
96
+ signature_params = build_signature_params(
97
+ covered_components=covered_components,
98
+ created=created
99
+ )
100
+
101
+ # Build Signature-Input header (needed for @signature-params in signature base)
102
+ signature_input_header = f"sig1={signature_params}"
103
+
104
+ # Build signature base (now includes @signature-params per RFC 9421)
105
+ signature_base = build_signature_base(
106
+ method=method,
107
+ authority=authority,
108
+ path=path,
109
+ query=query_string,
110
+ headers=headers,
111
+ body=body,
112
+ signature_key_header=signature_key_header,
113
+ covered_components=covered_components,
114
+ signature_params=signature_params
115
+ )
116
+
117
+ import logging
118
+ logger = logging.getLogger("aauth.signing")
119
+ logger.debug(f"🔐 AAUTH LIBRARY SIGNATURE BASE:")
120
+ logger.debug(f"🔐 Signature base length: {len(signature_base)} bytes")
121
+ logger.debug(f"🔐 Signature base hex (first 200): {signature_base.encode('utf-8').hex()[:200]}...")
122
+ for i, line in enumerate(signature_base.split('\n')):
123
+ logger.debug(f"🔐 Line {i}: {repr(line)}")
124
+
125
+ # Sign the signature base
126
+ signature_bytes = private_key.sign(signature_base.encode('utf-8'))
127
+
128
+ # Build Signature header
129
+ signature_header = build_signature_header(signature_bytes, label="sig1")
130
+
131
+ return {
132
+ "Signature-Input": signature_input_header,
133
+ "Signature": signature_header,
134
+ "Signature-Key": signature_key_header
135
+ }
136
+ except Exception as e:
137
+ raise SignatureError(f"Failed to sign request: {e}", details={"scheme": sig_scheme}) from e
138
+
@@ -0,0 +1,323 @@
1
+ """HTTP signature verification for AAuth."""
2
+
3
+ import re
4
+ import time
5
+ from typing import Dict, Any, Optional, Callable
6
+ from urllib.parse import urlparse
7
+ import jwt
8
+ from ..headers.signature_key import parse_signature_key
9
+ from ..headers.signature_input import parse_signature_input
10
+ from ..headers.signature import parse_signature
11
+ from ..signing.signature_base import build_signature_base
12
+ from ..keys.jwk import jwk_to_public_key
13
+ from ..tokens.agent_token import verify_agent_token
14
+ from ..errors import SignatureError
15
+
16
+
17
+ def verify_signature(
18
+ method: str,
19
+ target_uri: str,
20
+ headers: Dict[str, str],
21
+ body: Optional[bytes],
22
+ signature_input_header: str,
23
+ signature_header: str,
24
+ signature_key_header: str,
25
+ public_key=None,
26
+ jwks_fetcher: Optional[Callable] = None
27
+ ) -> bool:
28
+ """Verify HTTP signature using HTTP Message Signatures (RFC 9421).
29
+
30
+ Args:
31
+ method: HTTP method
32
+ target_uri: Target URI
33
+ headers: Request headers
34
+ body: Request body bytes (None if no body)
35
+ signature_input_header: Signature-Input header value
36
+ signature_header: Signature header value
37
+ signature_key_header: Signature-Key header value
38
+ public_key: Optional public key (for hwk scheme)
39
+ jwks_fetcher: Optional JWKS fetcher function (for jwks/jwt schemes)
40
+
41
+ Returns:
42
+ True if signature is valid, False otherwise
43
+
44
+ Raises:
45
+ SignatureError: If verification fails due to invalid format
46
+ """
47
+ import logging
48
+ logger = logging.getLogger("aauth.signing")
49
+
50
+ logger.debug(f"🔐 VERIFIER: verify_signature() called")
51
+ logger.debug(f"🔐 VERIFIER: method={method}, target_uri={target_uri}")
52
+ logger.debug(f"🔐 VERIFIER: signature_input_header={signature_input_header}")
53
+
54
+ try:
55
+ # Parse Signature-Input
56
+ components, sig_params = parse_signature_input(signature_input_header)
57
+
58
+ # Verify created timestamp (per spec Section 10.4)
59
+ if "created" in sig_params:
60
+ created = int(sig_params["created"])
61
+ now = int(time.time())
62
+ if abs(now - created) > 60:
63
+ return False
64
+
65
+ # Parse Signature-Key
66
+ parsed_key = parse_signature_key(signature_key_header)
67
+ scheme = parsed_key["scheme"]
68
+ params = parsed_key["params"]
69
+ label = parsed_key["label"]
70
+
71
+ # Verify label consistency (per spec Section 10.1.1)
72
+ label_match = re.match(r'(\w+)=', signature_input_header)
73
+ sig_label_match = re.match(r'(\w+)=', signature_header)
74
+
75
+ if not (label_match and sig_label_match):
76
+ return False
77
+
78
+ if not (label_match.group(1) == sig_label_match.group(1) == label):
79
+ return False
80
+
81
+ # Extract public key based on scheme
82
+ if scheme == "hwk":
83
+ if not public_key:
84
+ jwk = {
85
+ "kty": params.get("kty"),
86
+ "crv": params.get("crv"),
87
+ "x": params.get("x")
88
+ }
89
+ public_key = jwk_to_public_key(jwk)
90
+
91
+ elif scheme == "jwks":
92
+ if not jwks_fetcher:
93
+ raise SignatureError("sig=jwks requires jwks_fetcher")
94
+
95
+ agent_id = params.get("id")
96
+ kid = params.get("kid")
97
+ well_known = params.get("well-known")
98
+ jwks_param = params.get("jwks")
99
+
100
+ # Per spec Section 10.7 Mode 2: jwks parameter MUST NOT be present
101
+ if jwks_param:
102
+ return False
103
+
104
+ # Fetch JWKS
105
+ if callable(jwks_fetcher):
106
+ # Try both calling patterns
107
+ try:
108
+ jwks = jwks_fetcher(agent_id, kid) if kid else jwks_fetcher(agent_id)
109
+ except:
110
+ jwks = jwks_fetcher(agent_id)
111
+ else:
112
+ jwks = jwks_fetcher
113
+
114
+ if not jwks:
115
+ return False
116
+
117
+ # Find key by kid
118
+ keys = jwks.get("keys", [])
119
+ signing_key = None
120
+ for key in keys:
121
+ if key.get("kid") == kid:
122
+ signing_key = key
123
+ break
124
+
125
+ if not signing_key:
126
+ return False
127
+
128
+ public_key = jwk_to_public_key(signing_key)
129
+
130
+ elif scheme == "jwt":
131
+ if not jwks_fetcher:
132
+ raise SignatureError("sig=jwt requires jwks_fetcher")
133
+
134
+ jwt_token = params.get("jwt")
135
+ if not jwt_token:
136
+ return False
137
+
138
+ # Parse JWT to determine type
139
+ try:
140
+ header = jwt.get_unverified_header(jwt_token)
141
+ payload = jwt.decode(jwt_token, options={"verify_signature": False})
142
+ except Exception:
143
+ return False
144
+
145
+ typ = header.get("typ")
146
+ if typ not in ("agent+jwt", "auth+jwt"):
147
+ return False
148
+
149
+ # Validate JWT and extract cnf.jwk
150
+ if typ == "agent+jwt":
151
+ # Verify agent token
152
+ try:
153
+ agent_claims = verify_agent_token(
154
+ token=jwt_token,
155
+ jwks_fetcher=jwks_fetcher,
156
+ expected_aud=None
157
+ )
158
+ cnf = agent_claims.get("cnf")
159
+ cnf_jwk = cnf.get("jwk") if cnf else None
160
+ except Exception:
161
+ return False
162
+
163
+ elif typ == "auth+jwt":
164
+ # Extract cnf.jwk from payload
165
+ cnf = payload.get("cnf")
166
+ if not cnf:
167
+ return False
168
+
169
+ cnf_jwk = cnf.get("jwk")
170
+ if not cnf_jwk:
171
+ return False
172
+
173
+ # Verify JWT signature using auth server's JWKS
174
+ iss = payload.get("iss")
175
+ kid_header = header.get("kid")
176
+ if not iss or not kid_header:
177
+ return False
178
+
179
+ # Fetch auth server JWKS
180
+ try:
181
+ if callable(jwks_fetcher):
182
+ auth_jwks = jwks_fetcher(iss)
183
+ else:
184
+ auth_jwks = jwks_fetcher
185
+
186
+ if not auth_jwks:
187
+ return False
188
+
189
+ # Find signing key
190
+ keys = auth_jwks.get("keys", [])
191
+ signing_key = None
192
+ for key in keys:
193
+ if key.get("kid") == kid_header:
194
+ signing_key = key
195
+ break
196
+
197
+ if not signing_key:
198
+ logger.debug(f"🔐 VERIFIER: Signing key not found in JWKS (kid={kid_header})")
199
+ return False
200
+
201
+ logger.debug(f"🔐 VERIFIER: Found signing key in JWKS: {signing_key}")
202
+
203
+ # Get algorithm from JWT header
204
+ alg = header.get("alg")
205
+ if not alg:
206
+ logger.debug(f"🔐 VERIFIER: JWT header missing 'alg' field")
207
+ return False
208
+
209
+ # Map algorithm to PyJWT algorithm names
210
+ # RS256 -> RS256, EdDSA -> EdDSA, etc.
211
+ algorithms = [alg]
212
+
213
+ logger.debug(f"🔐 VERIFIER: Verifying JWT with algorithm: {alg}")
214
+ logger.debug(f"🔐 VERIFIER: JWT token (first 100 chars): {jwt_token[:100]}...")
215
+
216
+ # Handle different key types
217
+ # For RSA keys (RS256, etc.), convert JWK to public key using PyJWT's RSA algorithm
218
+ # For Ed25519 keys, we need to convert using jwk_to_public_key
219
+ key_type = signing_key.get("kty")
220
+ if key_type == "RSA":
221
+ # Convert RSA JWK to public key object using PyJWT's RSA algorithm
222
+ from jwt.algorithms import RSAAlgorithm
223
+ auth_public_key = RSAAlgorithm.from_jwk(signing_key)
224
+ logger.debug(f"🔐 VERIFIER: Converted RSA JWK to public key using RSAAlgorithm.from_jwk()")
225
+ elif key_type == "OKP" and signing_key.get("crv") == "Ed25519":
226
+ # Convert Ed25519 JWK to public key
227
+ auth_public_key = jwk_to_public_key(signing_key)
228
+ logger.debug(f"🔐 VERIFIER: Converted Ed25519 JWK to public key")
229
+ else:
230
+ logger.debug(f"🔐 VERIFIER: Unsupported key type: {key_type}")
231
+ return False
232
+
233
+ logger.debug(f"🔐 VERIFIER: Auth public key type: {type(auth_public_key)}")
234
+
235
+ # Verify JWT signature
236
+ try:
237
+ jwt.decode(
238
+ jwt_token,
239
+ auth_public_key,
240
+ algorithms=algorithms,
241
+ options={"verify_signature": True, "verify_exp": False, "verify_aud": False}
242
+ )
243
+ logger.debug(f"🔐 VERIFIER: JWT signature verification PASSED")
244
+ except Exception as jwt_error:
245
+ logger.debug(f"🔐 VERIFIER: JWT decode failed: {jwt_error}")
246
+ import traceback
247
+ logger.debug(f"🔐 VERIFIER: JWT decode traceback: {traceback.format_exc()}")
248
+ raise
249
+
250
+ # Check expiration
251
+ exp = payload.get("exp")
252
+ if exp and int(time.time()) >= exp:
253
+ logger.debug(f"🔐 VERIFIER: JWT expired (exp={exp}, now={int(time.time())})")
254
+ return False
255
+ except Exception as e:
256
+ logger.debug(f"🔐 VERIFIER: JWT verification failed with exception: {e}")
257
+ import traceback
258
+ logger.debug(f"🔐 VERIFIER: Exception traceback: {traceback.format_exc()}")
259
+ return False
260
+
261
+ # Convert cnf.jwk to public key for HTTPSig verification
262
+ public_key = jwk_to_public_key(cnf_jwk)
263
+
264
+ else:
265
+ raise SignatureError(f"Unknown signature scheme: {scheme}")
266
+
267
+ # Reconstruct signature base
268
+ parsed_uri = urlparse(target_uri)
269
+ authority = parsed_uri.netloc
270
+ path = parsed_uri.path or "/"
271
+ query_string = parsed_uri.query if parsed_uri.query else None
272
+
273
+ # Extract signature params (the part after "sig1=") for @signature-params line
274
+ # Signature-Input format: sig1=("@method" "@authority" ...);created=...
275
+ # RFC 9421 Section 2.5: @signature-params is required
276
+ signature_params = signature_input_header[5:] if signature_input_header.startswith("sig1=") else signature_input_header
277
+ if not signature_params:
278
+ return False # Invalid - signature_params required
279
+
280
+ logger.debug(f"🔐 VERIFIER: Building signature base")
281
+ logger.debug(f"🔐 VERIFIER: method={method}, authority={authority}, path={path}")
282
+ logger.debug(f"🔐 VERIFIER: covered_components={components}")
283
+ logger.debug(f"🔐 VERIFIER: signature_params={signature_params}")
284
+ logger.debug(f"🔐 VERIFIER: signature_key_header={signature_key_header[:100]}...")
285
+ logger.debug(f"🔐 VERIFIER: body is None: {body is None}")
286
+
287
+ signature_base = build_signature_base(
288
+ method=method,
289
+ authority=authority,
290
+ path=path,
291
+ query=query_string,
292
+ headers=headers,
293
+ body=body,
294
+ signature_key_header=signature_key_header,
295
+ covered_components=components,
296
+ signature_params=signature_params
297
+ )
298
+
299
+ logger.debug(f"🔐 VERIFIER SIGNATURE BASE:")
300
+ logger.debug(f"🔐 Signature base length: {len(signature_base)} bytes")
301
+ logger.debug(f"🔐 Signature base hex (first 200): {signature_base.encode('utf-8').hex()[:200]}...")
302
+ for i, line in enumerate(signature_base.split('\n')):
303
+ logger.debug(f"🔐 Line {i}: {repr(line)}")
304
+
305
+ # Parse signature
306
+ signature_bytes = parse_signature(signature_header, label=label)
307
+
308
+ # Verify signature
309
+ try:
310
+ public_key.verify(signature_bytes, signature_base.encode('utf-8'))
311
+ logger.debug(f"🔐 VERIFIER: ✅ Signature verification PASSED")
312
+ return True
313
+ except Exception as e:
314
+ logger.debug(f"🔐 VERIFIER: ❌ Signature verification FAILED: {e}")
315
+ import traceback
316
+ logger.debug(f"🔐 VERIFIER: Exception traceback: {traceback.format_exc()}")
317
+ return False
318
+
319
+ except SignatureError:
320
+ raise
321
+ except Exception as e:
322
+ raise SignatureError(f"Signature verification failed: {e}") from e
323
+
@@ -0,0 +1,2 @@
1
+ """JWT token handling for AAuth."""
2
+