Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/shade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
HTTPError,
RateLimitError,
ShadeError,
SignatureVerificationError,
)

__version__ = "0.1.0"
Expand All @@ -33,6 +34,7 @@
"RateLimitError",
"ShadeClient",
"ShadeError",
"SignatureVerificationError",
"SyncHTTPClient",
"config",
"api_base",
Expand Down
34 changes: 34 additions & 0 deletions src/shade/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,40 @@ def from_response(
return cls(message, status_code=404, response_body=response_body)


class SignatureVerificationError(ShadeError):
"""
Raised by ``Webhook.construct_event()`` when the HMAC-SHA256 signature in
the ``Shade-Signature`` header does not match the signature computed for
the payload.

Attributes:
header: The raw ``Shade-Signature`` header value as received.
"""

def __init__(
self,
message: str,
header: Optional[str] = None,
) -> None:
super().__init__(message)
self.header = header

@classmethod
def from_mismatch(cls, header: Optional[str] = None) -> "SignatureVerificationError":
"""Construct the error for a computed/received signature mismatch.

The expected signature is deliberately not accepted as an argument
here, so it can never end up in the exception message.
"""
message = (
"Webhook signature verification failed: the received signature "
"does not match the signature computed for this payload. This "
"usually means the webhook secret is incorrect, or the payload "
"was modified in transit."
)
return cls(message, header=header)


class NetworkError(ShadeError):
"""Raised when the SDK cannot complete a network request."""

Expand Down
39 changes: 39 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
NotFoundError,
RateLimitError,
ShadeError,
SignatureVerificationError,
)
from shade.errors import raise_for_invalid_request

Expand Down Expand Up @@ -69,6 +70,7 @@ def test_package_root_exports_error_classes():
assert shade.NetworkError is NetworkError
assert shade.NotFoundError is NotFoundError
assert shade.RateLimitError is RateLimitError
assert shade.SignatureVerificationError is SignatureVerificationError


def test_invalid_request_error_parses_param_from_body():
Expand Down Expand Up @@ -211,3 +213,40 @@ def test_not_found_error_invalid_json_body():

assert error.resource_type is None
assert error.resource_id is None


def test_signature_verification_error_is_shade_error():
error = SignatureVerificationError("bad signature", header="t=1,v1=abc")

assert isinstance(error, ShadeError)
assert str(error) == "bad signature"
assert error.header == "t=1,v1=abc"


def test_signature_verification_error_header_optional():
error = SignatureVerificationError("bad signature")

assert error.header is None


def test_signature_verification_error_from_mismatch_stores_header():
error = SignatureVerificationError.from_mismatch(header="t=1,v1=deadbeef")

assert isinstance(error, ShadeError)
assert error.header == "t=1,v1=deadbeef"


def test_signature_verification_error_from_mismatch_explains_causes():
error = SignatureVerificationError.from_mismatch(header="t=1,v1=deadbeef")

message = str(error)
assert "secret" in message
assert "payload" in message


def test_signature_verification_error_never_includes_expected_signature():
expected_signature = "supersecrethmacvalue"
error = SignatureVerificationError.from_mismatch(header="t=1,v1=deadbeef")

assert expected_signature not in str(error)
assert not hasattr(error, "expected_signature")
Loading