---
title: "Podpisové schémy — Webhooks for Laravel | Roundly"
description: "Predvolená schéma Standard Webhooks v1 bajt po bajte s testovacími vektormi, podpisy HTTP správ RFC 9421 a vlastné schémy v štýle GitHubu či Stripe."
url: https://roundly-consulting.com/sk/open-source/navody/webhooks-for-laravel/podpisove-schemy
language: sk
---

[Všetky balíky](https://roundly-consulting.com/sk/open-source.md)

[Webhooks for Laravel](https://roundly-consulting.com/sk/open-source/navody/webhooks-for-laravel.md)

# Podpisové schémy

Schémy sú ovládače registrované podľa názvu: default, rfc9421 a vaše vlastné (webhooks.schemes alebo Webhooks::schemes()->extend() v boot() service providera). Endpoint si ukladá názov schémy; neznámy názov odpovie 401 unknown\_scheme a webhooks:doctor ho nahlási. Každý HMAC — overenie aj podpis — beží cez Sentinel::macs() zo [Sentinelu](https://roundly-consulting.com/sk/open-source/navody/sentinel-for-laravel/mac-s-vlastnymi-tajomstvami.md): v konštantnom čase voči každému kandidátskemu kľúču, bez predčasného ukončenia. Webhooks nikdy sám nepočíta ani neporovnáva MAC a schéma nikdy neuvidí tajný kľúč.

## default — Standard Webhooks v1

Otvorený formát Standard Webhooks bajt po bajte. Tá istá schéma overuje prichádzajúce požiadavky aj podpisuje odchádzajúce doručenia, takže existujúce knižnice Standard Webhooks vaše doručenia overia bez úprav:

| Hlavička | Hodnota |
| --- | --- |
| `webhook-id` | Id doručenia: 1–255 bajtov viditeľného ASCII. Naše: msg\_<26-znakový ULID>, rovnaké pri každom pokuse aj opätovnom doručení. |
| `webhook-timestamp` | Celé Unix sekundy (UTC) tohto pokusu — len číslice. |
| `webhook-signature` | Jedna alebo viac položiek v1,<base64> oddelených jednou medzerou — štandardné base64 (s doplnením =) 32-bajtového HMAC-SHA256. Neznáme verzie sa ignorujú. |
| `content-type` | application/json (odchádzajúce). |

```
key            = base64_decode(secret without the "whsec_" prefix)
signed_content = webhook-id + "." + webhook-timestamp + "." + body     # body = the exact raw bytes
signature      = "v1," + base64(HMAC_SHA256(key, signed_content))
```

Žiadna kanonizácia, žiadny znova serializovaný JSON, žiadny pridaný nový riadok: overuje sa surové telo. Tajný kľúč bez prefixu whsec\_ (importovaný od odosielateľa, ktorý rozdáva obyčajné reťazce) sú jeho vlastné bajty. Na strane príjmu:

- Všetky tri hlavičky musia byť prítomné — inak missing\_signature, missing\_timestamp alebo missing\_id.
- webhook-timestamp zodpovedá ^\[0-9\]{1,12}$ (invalid\_timestamp) a je v rozsahu ± tolerancie od teraz — predvolene 300 s, do minulosti aj budúcnosti (stale\_timestamp).
- Najviac 8 položiek a 4 096 bajtov vo webhook-signature; prijme sa, ak ktorákoľvek položka v1 zodpovedá ktorémukoľvek platnému kľúču — aktuálnemu a počas prechodného obdobia aj predchádzajúcemu.
- webhook-id je kľúčom idempotencie.

## Testovacie vektory

| secret | webhook-id | webhook-timestamp | body | webhook-signature |
| --- | --- | --- | --- | --- |
| `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw` | `msg_p5jXN8AQM9LWM0D4loKWxJek` | `1614265330` | `{"test": 2432232314}` | `v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=` |
| `whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=` | `msg_01JCDEF0000000000000000000` | `1794139200` | `{"type":"invoice.paid","timestamp":"2026-11-08T12:00:00Z","data":{"invoice":42}}` | `v1,dBcYxACDimm+8xx9Ykx71LVUgk5H4STQXxAUHZ5xcNk=` |
| whsec\_ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8= (nový) alebo ten z riadku vyššie (starý) | ako vyššie | ako vyššie | ako vyššie | `v1,Nwl36Rvbn1xO29I4VEpYVwe7HJ3vZuLKGz6KoEVZDFI= v1,dBcYxACDimm+8xx9Ykx71LVUgk5H4STQXxAUHZ5xcNk=` |

Prvý je zverejnený vektor Standard Webhooks; tretí ukazuje rotáciu s podpisom novým aj starým kľúčom. Testy balíka každý z nich prepočítajú obyčajným hash\_hmac.

## rfc9421 — podpisy HTTP správ

Prichádzajúci endpoint overuje cez [HTTP podpisy Sentinelu](https://roundly-consulting.com/sk/open-source/navody/sentinel-for-laravel/podpisy-http.md) voči svojmu scheme\_options.profile (predvolene webhooks.rfc9421.profile). max\_age a clock\_skew zo Sentinelu nahrádzajú toleranciu, jeho úložisko nonce kontrolu opakovania a jeho content digest kontrolu tela. Podpisový kľúč musí byť importovaný do Sentinelu a naviazaný na tento endpoint — kľúč, ktorý vlastní niečo iné, odpovie 401 key\_not\_bound:

```php
$endpoint = Webhooks::inbound()->create(scheme: 'rfc9421', options: new InboundEndpointOptions(
    schemeOptions: ['profile' => 'partners', 'id_header' => 'webhook-id'],
))->endpoint;

Sentinel::keys()->ring('http')->import('acme-2026', Algorithm::Ed25519, $partnerPublicKeyPem, owner: $endpoint);
```

- Id doručenia sa berie zo scheme\_options.id\_header (predvolene webhook-id), inak z Idempotency-Key, inak z nonce podpisu.
- Za podpísané sa považuje len vtedy, ak je táto hlavička medzi komponentmi, ktoré podpis pokrýva. Predvolený profil Sentinelu pokrýva @method @authority @path (+ content-digest), preto hlavičku s id pridajte do komponentov profilu — aj odosielateľa; inak webhooks:doctor upozorní.
- Ochrana balíka pred opakovaním a väzba na telo sa spoliehajú na require\_nonce a require\_content\_digest; webhooks:doctor označí profil, ktorý niektorú z nich vypína.
- Odchádzajúce endpointy RFC 9421 podpisujú kľúčom Sentinelu, ktorým aplikácia vie podpisovať (scheme\_options.key\_id, inak webhooks.rfc9421.outbound\_key\_id), a pokrývajú @method @authority @path content-digest content-type webhook-id webhook-timestamp, pri URL s query aj @query. Bez id kľúča doručenie zlyhá so signing, bez požiadavky.

## Vlastná schéma

```php
interface SignatureScheme
{
    /** @throws WebhookSignatureException */
    public function verify(IncomingWebhook $request, EndpointSecrets $secrets): VerifiedWebhook;
}

interface SignsOutboundWebhooks   // optional: the scheme can sign outbound deliveries too
{
    public function sign(OutgoingWebhook $message, EndpointSecrets $secrets): SignatureHeaders;
}
```

- IncomingWebhook nesie headers (malými písmenami, prvá hodnota), body (surové bajty), receivedAt, endpointKey, toleranceSeconds, schemeOptions a request; header($name).
- EndpointSecrets nikdy neprezradí tajný kľúč: matches($message, $macBytes, SecretEncoding, Algorithm = HmacSha256): ?SecretSlot a sign($message, SecretEncoding, Algorithm): list<string> (surové MAC, aktuálny prvý) — oba cez Sentinel.
- SecretEncoding::StandardWebhooks číta bajty za whsec\_ (alebo bajty obyčajného kľúča); SecretEncoding::Raw použije vlastné bajty reťazca vrátane whsec\_. Vyberá ho schéma — podpisové kľúče Stripe začínajú na whsec\_, no používajú sa surové.
- Vráťte VerifiedWebhook(deliveryId, timestamp, eventType, idIsSigned, slot, replayProtected: false, commit: null). idIsSigned nastavte len vtedy, ak MAC pokrýva vrátené id; verdikt bez id sa nikdy nepovažuje za podpísaný. replayProtected nastavte len vtedy, ak schéma sama odmietne druhé použitie presne tej istej požiadavky; commit je closure, ktorá toto použitie zaznamená a beží v transakcii ukladajúcej doručenie.
- Vyhoďte WebhookSignatureException::because(RejectionReason::…). Ak implementujete aj SignsOutboundWebhooks, schéma podpisuje odchádzajúce doručenia — a SignedWebhookRequest ňou podpíše vaše testovacie požiadavky.

## Vygenerovanie receptu

Balík neobsahuje ovládače konkrétnych poskytovateľov. webhooks:make-scheme zapíše triedu schémy do app/Webhooks/Schemes z receptu — github, stripe alebo blank — a trieda je váš vlastný kód, ktorý si prispôsobíte:

```bash
php artisan webhooks:make-scheme github            # app/Webhooks/Schemes/GitHubScheme.php
php artisan webhooks:make-scheme stripe            # app/Webhooks/Schemes/StripeScheme.php
php artisan webhooks:make-scheme blank MySenderScheme
```

```php
// a service provider's boot()
Webhooks::schemes()->extend('github', GitHubScheme::class);

Webhooks::inbound()->create(scheme: 'github', secret: $secretYouEnteredInGitHub, handler: 'github');
```

V štýle GitHubu: X-Hub-Signature-256: sha256=<hex HMAC-SHA256(body)>, X-GitHub-Delivery a X-GitHub-Event; kľúčom sú vlastné bajty tajného kľúča. Časová pečiatka chýba, takže ochranu pred opakovaním tvorí len deduplikácia — pri takejto schéme je telo zhodné bajt po bajte duplicitou, kým existuje riadok prvého doručenia, a odosielateľ, ktorý oprávnene opakuje rovnaké telo, ho musí odlíšiť. Čo zapíše recept github:

```php
<?php

declare(strict_types=1);

namespace App\Webhooks\Schemes;

use RoundlyConsulting\Crypto\Codec\InvalidEncodingException;
use RoundlyConsulting\Crypto\Facades\Crypto;
use RoundlyConsulting\Webhooks\Contracts\SignatureScheme;
use RoundlyConsulting\Webhooks\Contracts\SignsOutboundWebhooks;
use RoundlyConsulting\Webhooks\DataTransferObjects\IncomingWebhook;
use RoundlyConsulting\Webhooks\DataTransferObjects\OutgoingWebhook;
use RoundlyConsulting\Webhooks\DataTransferObjects\SignatureHeaders;
use RoundlyConsulting\Webhooks\DataTransferObjects\VerifiedWebhook;
use RoundlyConsulting\Webhooks\Enums\RejectionReason;
use RoundlyConsulting\Webhooks\Enums\SecretEncoding;
use RoundlyConsulting\Webhooks\Exceptions\WebhookSignatureException;
use RoundlyConsulting\Webhooks\Schemes\EndpointSecrets;

final readonly class GitHubScheme implements SignatureScheme, SignsOutboundWebhooks
{
    public function verify(IncomingWebhook $request, EndpointSecrets $secrets): VerifiedWebhook
    {
        $header = $request->header('x-hub-signature-256') ?? throw WebhookSignatureException::because(RejectionReason::MissingSignature);

        try {
            $mac = str_starts_with($header, 'sha256=') ? Crypto::hexDecode(substr($header, 7)) : '';
        } catch (InvalidEncodingException) {
            $mac = '';
        }

        $slot = $secrets->matches($request->body, $mac, SecretEncoding::Raw) ?? throw WebhookSignatureException::because(RejectionReason::InvalidSignature);

        return new VerifiedWebhook($request->header('x-github-delivery'), null, $request->header('x-github-event'), false, $slot);
    }

    public function sign(OutgoingWebhook $message, EndpointSecrets $secrets): SignatureHeaders
    {
        return new SignatureHeaders([
            'X-Hub-Signature-256' => 'sha256='.Crypto::hexEncode($secrets->sign($message->body, SecretEncoding::Raw)[0]),
            'X-GitHub-Delivery' => $message->id,
            'X-GitHub-Event' => $message->eventType,
        ]);
    }
}
```

V štýle Stripe: Stripe-Signature: t=<ts>,v1=<hex HMAC-SHA256(„t.body“)>; kľúčom je celý reťazec whsec\_… ako surové bajty. Id udalosti je vnútri podpísaného tela, preto sa považuje za podpísané; telo bez reťazcového id si podpísané id nenárokuje a spracovanie použije nepodpísaný Idempotency-Key, prípadne odmietne s 400. Čo zapíše recept stripe:

```php
<?php

declare(strict_types=1);

namespace App\Webhooks\Schemes;

use Carbon\CarbonImmutable;
use RoundlyConsulting\Crypto\Codec\InvalidEncodingException;
use RoundlyConsulting\Crypto\Facades\Crypto;
use RoundlyConsulting\Webhooks\Contracts\SignatureScheme;
use RoundlyConsulting\Webhooks\Contracts\SignsOutboundWebhooks;
use RoundlyConsulting\Webhooks\DataTransferObjects\IncomingWebhook;
use RoundlyConsulting\Webhooks\DataTransferObjects\OutgoingWebhook;
use RoundlyConsulting\Webhooks\DataTransferObjects\SignatureHeaders;
use RoundlyConsulting\Webhooks\DataTransferObjects\VerifiedWebhook;
use RoundlyConsulting\Webhooks\Enums\RejectionReason;
use RoundlyConsulting\Webhooks\Enums\SecretEncoding;
use RoundlyConsulting\Webhooks\Exceptions\WebhookSignatureException;
use RoundlyConsulting\Webhooks\Schemes\EndpointSecrets;

final readonly class StripeScheme implements SignatureScheme, SignsOutboundWebhooks
{
    /** The most `v1=` entries checked, and the longest header — each entry costs a MAC per secret. */
    private const int MAX_ENTRIES = 8;

    private const int MAX_HEADER_BYTES = 4096;

    public function verify(IncomingWebhook $request, EndpointSecrets $secrets): VerifiedWebhook
    {
        $header = $request->header('stripe-signature') ?? throw WebhookSignatureException::because(RejectionReason::MissingSignature);
        $parts = [];

        if (strlen($header) > self::MAX_HEADER_BYTES) {
            throw WebhookSignatureException::because(RejectionReason::InvalidSignature);
        }

        foreach (explode(',', $header) as $part) {
            [$key, $value] = array_pad(explode('=', $part, 2), 2, '');
            $parts[$key][] = $value;
        }

        $timestamp = $parts['t'][0] ?? '';

        if (preg_match('/^\d{1,12}$/', $timestamp) !== 1) {
            throw WebhookSignatureException::because(RejectionReason::InvalidTimestamp);
        }

        if (abs($request->receivedAt->getTimestamp() - (int) $timestamp) > $request->toleranceSeconds) {
            throw WebhookSignatureException::because(RejectionReason::StaleTimestamp);
        }

        if (count($parts['v1'] ?? []) > self::MAX_ENTRIES) {
            throw WebhookSignatureException::because(RejectionReason::InvalidSignature);
        }

        foreach ($parts['v1'] ?? [] as $hex) {
            try {
                $slot = $secrets->matches($timestamp.'.'.$request->body, Crypto::hexDecode($hex), SecretEncoding::Raw);
            } catch (InvalidEncodingException) {
                continue;
            }

            if ($slot !== null) {
                $event = json_decode($request->body, true);
                $id = is_array($event) && is_string($event['id'] ?? null) ? $event['id'] : null;

                // Signed only when the body carries it; without one the id is an unsigned header.
                return new VerifiedWebhook($id, CarbonImmutable::createFromTimestampUTC((int) $timestamp), null, $id !== null, $slot);
            }
        }

        throw WebhookSignatureException::because(RejectionReason::InvalidSignature);
    }

    public function sign(OutgoingWebhook $message, EndpointSecrets $secrets): SignatureHeaders
    {
        $timestamp = (string) $message->timestamp->getTimestamp();
        $signatures = array_map(static fn (string $mac): string => 'v1='.Crypto::hexEncode($mac), $secrets->sign($timestamp.'.'.$message->body, SecretEncoding::Raw));

        return new SignatureHeaders(['Stripe-Signature' => 't='.$timestamp.','.implode(',', $signatures)]);
    }
}
```

## Prejavte lásku k open source

Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.

[Ďalšie spôsoby podpory vrátane kryptomien](https://roundly-consulting.com/sk/podporte-nas.md)

Odoslaním daru súhlasíte s našimi [podmienkami prijímania darov](https://roundly-consulting.com/sk/podmienky-darov.md).

[Podporte našu open-source prácu (otvorí sa v novej karte)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Pridajte sa k nám na Patreone (otvorí sa v novej karte)](https://www.patreon.com/cw/roundly)

## Chcete to zabudovať do svojho produktu?

Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.

[Ponuka do 48 hodín](https://roundly-consulting.com/sk/kontakt.md) [Prezrieť všetky balíky](https://roundly-consulting.com/sk/open-source/navody/webhooks-for-laravel.md)
