Click the Castor logo or press Ctrl Alt T to change theme.
# Signed data A digital signature proves authenticity, not secrecy. Everyone may read the message, but only the holder of the signing key can produce a signature that the verification key accepts. This is useful when data travels through a place you do not fully trust: a browser URL, a webhook receiver, a message queue, an object store, or a customer-controlled system. ## Sign an invitation link Suppose you want invitation links that cannot be edited from one email address to another. Put the important fields in a canonical payload and sign those exact bytes. ```php <?php use Castor\Crypto\Base64; use Castor\Crypto\Secret; use Castor\Crypto\SecretText; use Castor\Crypto\Signing\SigningKeyPair; $pair = SigningKeyPair::generate(); $signingKey = $pair->getSigningKey(); $verificationKey = $pair->getVerificationKey(); $payload = json_encode([ 'invite_id' => 123, 'email' => 'ada@example.com', 'expires_at' => '2026-06-01T00:00:00Z', ], JSON_THROW_ON_ERROR); $signature = $signingKey->sign(Secret::raw($payload)); $url = 'https://example.com/accept?'.http_build_query([ 'payload' => base64_encode($payload), 'sig' => $signature->toBase64(Base64::UrlSafeNoPadding), ]); ``` When the link is used, verify before trusting any field in the payload: ```php $payload = base64_decode($_GET['payload'], strict: true); $signature = SecretText::fromBase64($_GET['sig'], Base64::UrlSafeNoPadding); if (!$verificationKey->verify(Secret::raw($payload), $signature)) { // Reject the link. } $claims = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); ``` The signature does not expire by itself. The `expires_at` field is protected from tampering, but your application must still check it. ## Canonical bytes are everything Signatures verify bytes, not meanings. These JSON values may mean the same thing to your application but produce different signatures if whitespace or key order changes: ```json {"id":123,"admin":false} ``` ```json { "admin": false, "id": 123 } ``` For tokens you create and verify yourself, sign the exact encoded payload you send. For interoperable protocols, define a canonical encoding and test it carefully. ## Signing webhooks If your app sends webhooks, sign the raw request body and include the signature in a header: ```php $body = json_encode($event, JSON_THROW_ON_ERROR); $signature = $signingKey->sign(Secret::raw($body)); $headers = [ 'X-App-Signature' => $signature->toBase64(Base64::UrlSafeNoPadding), ]; ``` The receiver verifies the raw body before parsing it. If you also include a timestamp in the body or headers, verify freshness to reduce replay attacks. ## Tradeoffs and attack vectors A signature does not hide data. If the payload contains private information, encrypt it instead or encrypt then sign according to your protocol needs. Protect the signing key like a production secret. If it leaks, attackers can mint valid payloads. Verification keys are public and can be distributed broadly. Signatures do not stop replay by default. Add a timestamp, nonce, event id, or one-time database record when the same valid message must not be accepted twice.
Castor ecosystem