"""Server-side signing examples for Merchant Payin v2, using built-in modules. The issued secret is used literally; never base64-decode it or put it in browser code. Request targets and body bytes must be the ones actually sent over HTTP. """ import hashlib import hmac import re def _bytes(value): if isinstance(value, str): return value.encode("utf-8") if isinstance(value, (bytes, bytearray)): return bytes(value) raise TypeError("The raw body must be a UTF-8 string or bytes.") def _secret_bytes(secret): if not isinstance(secret, str) or not secret: raise TypeError("A literal Shop secret is required.") return secret.encode("utf-8") def sign_request(timestamp, nonce, method, target, raw_body, secret): """Return lowercase-hex HMAC-SHA512 of the exact five-component request.""" if not isinstance(timestamp, str) or not re.fullmatch(r"(0|[1-9][0-9]*)", timestamp): raise TypeError("Timestamp must be canonical Unix seconds as a string.") if not isinstance(nonce, str) or not re.fullmatch( r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", nonce, re.I ): raise TypeError("Nonce must be UUID v4.") if not isinstance(method, str) or not re.fullmatch(r"[A-Za-z]+", method): raise TypeError("Invalid HTTP method.") if not isinstance(target, str) or not re.fullmatch(r"/(?!/)[^\r\n#]*", target): raise TypeError("Use the exact path and encoded query.") prefix = "\n".join((timestamp, nonce, method.upper(), target, "")).encode("utf-8") return hmac.new(_secret_bytes(secret), prefix + _bytes(raw_body), hashlib.sha512).hexdigest() def verify_webhook(raw_body, timestamp, signature, secret): """Verify timestamp + '.' + raw body with a constant-time comparison. Also check freshness in the HTTP receiver (recommended ±300 seconds) and commit id/status_id with the business effect atomically. A delayed overdue notification must not downgrade a previously accepted success. """ if not isinstance(timestamp, str) or not re.fullmatch(r"(0|[1-9][0-9]*)", timestamp): return False if not isinstance(signature, str) or not re.fullmatch(r"[0-9a-f]{128}", signature): return False canonical = timestamp.encode("utf-8") + b"." + _bytes(raw_body) expected = hmac.new(_secret_bytes(secret), canonical, hashlib.sha512).hexdigest() return hmac.compare_digest(expected, signature)