hooks.yaml
· 911 B · YAML
Surowy
- id: debug
execute-command: /usr/local/bin/log-hook.sh
include-command-output-in-response: true
pass-arguments-to-command:
- source: entire-headers
- source: raw-request-body
- id: liberaforms
execute-command: /usr/local/bin/verify-hook.py
include-command-output-in-response: true
pass-arguments-to-command:
- source: header
name: X-Timestamp
- source: header
name: X-Signature-256
- source: raw-request-body
pass-environment-to-command:
- source: string
name: foobar
envname: WEBHOOK_SECRET
trigger-rule:
and:
- match:
type: regex
regex: '^[0-9a-f]{64}$'
parameter:
source: header
name: X-Signature-256
- match:
type: regex
regex: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?(Z|[+-]\d{2}:\d{2})$'
parameter:
source: header
name: X-Timestamp
| 1 | - id: debug |
| 2 | execute-command: /usr/local/bin/log-hook.sh |
| 3 | include-command-output-in-response: true |
| 4 | pass-arguments-to-command: |
| 5 | - source: entire-headers |
| 6 | - source: raw-request-body |
| 7 | - id: liberaforms |
| 8 | execute-command: /usr/local/bin/verify-hook.py |
| 9 | include-command-output-in-response: true |
| 10 | pass-arguments-to-command: |
| 11 | - source: header |
| 12 | name: X-Timestamp |
| 13 | - source: header |
| 14 | name: X-Signature-256 |
| 15 | - source: raw-request-body |
| 16 | pass-environment-to-command: |
| 17 | - source: string |
| 18 | name: foobar |
| 19 | envname: WEBHOOK_SECRET |
| 20 | trigger-rule: |
| 21 | and: |
| 22 | - match: |
| 23 | type: regex |
| 24 | regex: '^[0-9a-f]{64}$' |
| 25 | parameter: |
| 26 | source: header |
| 27 | name: X-Signature-256 |
| 28 | - match: |
| 29 | type: regex |
| 30 | regex: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?(Z|[+-]\d{2}:\d{2})$' |
| 31 | parameter: |
| 32 | source: header |
| 33 | name: X-Timestamp |
verify-hook.py
· 1.4 KiB · Python
Surowy
#!/usr/bin/env python3
import os
import sys
import hmac
import hashlib
from datetime import datetime, timezone
MAX_SKEW = 300 # secondes
def main():
if len(sys.argv) != 4:
print("INVALIDE: arguments manquants (attendu: timestamp signature raw_body)")
return 1
timestamp, signature, body_arg = sys.argv[1:4]
signature = signature.removeprefix("sha256=")
secret = os.environ.get("WEBHOOK_SECRET", "")
if not secret:
print("INVALIDE: WEBHOOK_SECRET non défini")
return 1
# 1. Fraîcheur du timestamp
try:
ts = datetime.fromisoformat(timestamp)
if ts.tzinfo is None:
ts = ts.replace(tzinfo=timezone.utc)
except ValueError:
print(f"INVALIDE: timestamp illisible ({timestamp})")
return 1
skew = abs((datetime.now(timezone.utc) - ts).total_seconds())
if skew > MAX_SKEW:
print(f"INVALIDE: timestamp trop ancien/futur ({skew:.0f}s d'écart)")
return 1
# 2. Signature sur les octets bruts + timestamp
# os.fsencode restitue les octets exacts reçus dans argv
raw_body = os.fsencode(body_arg)
expected = hmac.new(
secret.encode("utf-8"),
raw_body + timestamp.encode("utf-8"),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
print("INVALIDE: signature incorrecte")
return 1
print("VALIDE")
return 0
if __name__ == "__main__":
sys.exit(main())
| 1 | #!/usr/bin/env python3 |
| 2 | import os |
| 3 | import sys |
| 4 | import hmac |
| 5 | import hashlib |
| 6 | from datetime import datetime, timezone |
| 7 | |
| 8 | MAX_SKEW = 300 # secondes |
| 9 | |
| 10 | def main(): |
| 11 | if len(sys.argv) != 4: |
| 12 | print("INVALIDE: arguments manquants (attendu: timestamp signature raw_body)") |
| 13 | return 1 |
| 14 | |
| 15 | timestamp, signature, body_arg = sys.argv[1:4] |
| 16 | signature = signature.removeprefix("sha256=") |
| 17 | |
| 18 | secret = os.environ.get("WEBHOOK_SECRET", "") |
| 19 | if not secret: |
| 20 | print("INVALIDE: WEBHOOK_SECRET non défini") |
| 21 | return 1 |
| 22 | |
| 23 | # 1. Fraîcheur du timestamp |
| 24 | try: |
| 25 | ts = datetime.fromisoformat(timestamp) |
| 26 | if ts.tzinfo is None: |
| 27 | ts = ts.replace(tzinfo=timezone.utc) |
| 28 | except ValueError: |
| 29 | print(f"INVALIDE: timestamp illisible ({timestamp})") |
| 30 | return 1 |
| 31 | |
| 32 | skew = abs((datetime.now(timezone.utc) - ts).total_seconds()) |
| 33 | if skew > MAX_SKEW: |
| 34 | print(f"INVALIDE: timestamp trop ancien/futur ({skew:.0f}s d'écart)") |
| 35 | return 1 |
| 36 | |
| 37 | # 2. Signature sur les octets bruts + timestamp |
| 38 | # os.fsencode restitue les octets exacts reçus dans argv |
| 39 | raw_body = os.fsencode(body_arg) |
| 40 | |
| 41 | expected = hmac.new( |
| 42 | secret.encode("utf-8"), |
| 43 | raw_body + timestamp.encode("utf-8"), |
| 44 | hashlib.sha256, |
| 45 | ).hexdigest() |
| 46 | |
| 47 | if not hmac.compare_digest(expected, signature): |
| 48 | print("INVALIDE: signature incorrecte") |
| 49 | return 1 |
| 50 | |
| 51 | print("VALIDE") |
| 52 | return 0 |
| 53 | |
| 54 | if __name__ == "__main__": |
| 55 | sys.exit(main()) |
| 56 |