-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathhmac_signature_verifier.py
More file actions
104 lines (91 loc) · 4.39 KB
/
Copy pathhmac_signature_verifier.py
File metadata and controls
104 lines (91 loc) · 4.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import hashlib
import hmac
from typing import Optional, List, Callable
from apimatic_core_interfaces.http.request import Request
from apimatic_core_interfaces.security.verification_result import VerificationResult
from apimatic_core.exceptions.signature_verification_error import SignatureVerificationError
from apimatic_core.security.encoders import DigestEncoder, HexEncoder
from apimatic_core.templating.template_engine import TemplateEngine
class HmacSignatureVerifier:
"""
Template-driven HMAC signature verifier.
- Builds the message to sign using a template with placeholders:
{raw_body}
{$method} | {$url} | {$request.path}
{$request.header.<HeaderName>}
{$request.query.<ParamName>}
{$request.body#/json/pointer}
- Computes HMAC(message, key, hash_alg) and encodes with the chosen encoder.
- Optionally wraps the encoded digest via `signature_value_template`, e.g., "sha256={digest}".
Parameters
----------
key : str
Shared secret (non-empty).
signature_header : str
Name of the header carrying the provided signature (case-insensitive lookup).
message_template : str
Template to construct the signed message (see placeholders above).
hash_alg : callable, optional
Hash function from hashlib (default: hashlib.sha256).
encoder : DigestEncoder, optional
Encoder for the raw HMAC bytes (default: HexEncoder()).
signature_value_template : str, optional
Optional template for the expected signature value, where `{digest}` is replaced
by the encoded digest. If omitted, the expected signature is the encoded digest itself.
"""
def __init__(
self,
*,
key: str,
signature_header: str,
message_template: str,
hash_alg=hashlib.sha256,
encoder: DigestEncoder = HexEncoder(),
signature_value_template: Optional[str] = None,
) -> None:
# Basic config validation
if not isinstance(key, str) or not key:
raise ValueError("key must be a non-empty string.")
if not isinstance(signature_header, str) or not signature_header.strip():
raise ValueError("signature_header must be a non-empty string.")
if not isinstance(message_template, str) or not message_template:
raise ValueError("message_template must be a non-empty string.")
self._key_bytes: bytes = key.encode("utf-8")
self._signature_header_lc = signature_header.lower().strip()
self._hash_alg = hash_alg
self._encoder = encoder
self._sig_value_template = signature_value_template
# Compile template once for performance
self._engine = TemplateEngine()
self._plan: List[Callable[[Request], bytes]] = self._engine.compile(message_template)
def verify(self, request: Request) -> VerificationResult:
"""Verify the signature in the request headers."""
try:
provided = self._read_signature_header(request)
if provided is None:
return VerificationResult.failed(
ValueError(f"Signature header '{self._signature_header_lc}' is missing.")
)
message = self._engine.render(self._plan, request)
digest = hmac.new(self._key_bytes, message, self._hash_alg).digest()
encoded = self._encoder.encode(digest)
expected = (
self._sig_value_template.replace("{digest}", encoded)
if self._sig_value_template else
encoded
)
ok = hmac.compare_digest(provided, expected)
return VerificationResult.passed() if ok else VerificationResult.failed(
SignatureVerificationError("Signature mismatch.")
)
except Exception as exc:
return VerificationResult.failed(
SignatureVerificationError(f"Signature Verification Failed: {exc}")
)
# ------------------------------------------------------------------
# Internals
# ------------------------------------------------------------------
def _read_signature_header(self, request: Request) -> Optional[str]:
headers = {str(k).lower(): str(v) for k, v in (getattr(request, "headers", {}) or {}).items()}
value = headers.get(self._signature_header_lc)
return None if value is None or str(value).strip() == "" else str(value)