Implement webhook signature checks

Created on 30 November 2024, 2 months ago

Problem/Motivation

According to the Webhook payload signing docs they...

... strongly recommend that you start integrating the new signing mechanism into your webhooks as soon as possible. This will ensure a smooth transition when the feature becomes mandatory. After the transition period, the previous webhook handling process will be gradually phased out.

Steps to reproduce

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

📌 Task
Status

Active

Version

1.0

Component

Code

Created by

🇨🇭Switzerland znerol

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @znerol
  • 🇨🇭Switzerland znerol

    The Webhook Encryption Service > read endpoint is a bit different than other resources on this API:

    1. It does not require authentication.
    2. It does not take any spaceId parameter.

    It follows that the keyId field in the x-signature request header in the webook call is neither bound to an account nor to a space. At the moment, webhook calls from all of my test accounts are signed with the same keyID=15a411ae-5d1b-465e-aaa0-65347e4c9f0e.

    The public key can be retrieved using WebhookEncryptionService::read($id) - or any other HTTP client. Calling the endpoint with curl reveals that there are proper caching headers (cache-control: public, max-age=31536000) on the response.

    % curl -ifs 'https://checkout.postfinance.ch/api/webhook-encryption/read?id=15a411ae-5d1b-465e-aaa0-65347e4c9f0e'
    HTTP/2 200
    date: Fri, 06 Dec 2024 13:16:33 GMT
    content-type: application/json;charset=utf-8
    vary: Accept-Encoding
    x-svid: 0e518d183291a1bcc
    set-cookie: _csrf_token_443=79hlv52gv6h8dnu7f4gq373r4s; Path=/; Expires=Sat, 7 Dec 2024 13:16:33 GMT; Max-Age=86400; Secure; HttpOnly
    set-cookie: language=en-US; Path=/; Secure; HttpOnly
    set-cookie: language=en-US; Path=/; Secure; HttpOnly
    expires: Sat, 06 Dec 2025 13:16:33 GMT
    reporting-endpoints: csp-endpoint="/csp-reports"
    report-to: {   "group": "csp-endpoint",   "max_age": 10886400,   "endpoints": [     { "url": "/csp-reports" }   ] }
    content-security-policy: default-src 'self'; child-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self' 'nonce-W/InUZhxHt2YcndKLu8Utw=='; style-src 'self' 'nonce-W/InUZhxHt2YcndKLu8Utw=='; worker-src 'self'; report-to csp-endpoint; report-uri /csp-reports;
    x-xss-protection: 1
    cache-control: public, max-age=31536000
    cf-cache-status: DYNAMIC
    strict-transport-security: max-age=0; includeSubDomains; preload
    x-content-type-options: nosniff
    server: cloudflare
    cf-ray: 8edc8d165e9627bd-LYS
    
    {"id":"15a411ae-5d1b-465e-aaa0-65347e4c9f0e","publicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcP72Buvf1myOG9qqpcx4wkdKXdYC+FTCqstYH+NzX0o4vg6X0PY80nXmcbHlmu7TBMEc55MCv4z/n4Xy4HqC3A=="}
  • 🇨🇭Switzerland znerol

    Regrettably the ApiClient constructor requires an user id and a secret.

    Hence, we basically have three options to retrieve the public key when a webhook signature needs checking:

    1. Stick with ApiClient and correct credentials. In order to do that we'd need a way to configure some sort of global or default credentials which are not bound to a payment method.
    2. Stick with ApiClient and supply fake credentials. Easy to implement, but doesn't feel like a proper solution.
    3. Use guzzle without credentials. This additionally opens up the option to cache the public key and avoid repeated lookups.
  • 🇨🇭Switzerland znerol

    There WebhookEncryptionService::isContentValid which does all the parsing, but also the read() call. Since I'm not keen to reimplement that, option 3 is out.

    In order to get things rolling here, I'll implement for option 2 for the start.

  • 🇨🇭Switzerland znerol

    Implement this as an access check. This needs tests and the check needs to be configurable in some way in order to provide a smooth transition.

  • 🇨🇭Switzerland znerol

    With the test coverage I'm pretty confident that everything is working as expected now. Need to think about the docs, it is somewhat important that people first enable payload signing at the PSP side and only afterwards the webhook access check.

Production build 0.71.5 2024