# WordPress: Verify Laravel Signed Requests

This document explains how your WordPress plugin endpoint can verify requests signed by Laravel using per-user public keys stored in the `plugin_key` field.

Summary
- Laravel signs: `message = timestamp + '.' + raw_body` using the user's private key (RSA SHA-256).
- WordPress verifies: reads `X-Timestamp` and `X-Signature` headers, reconstructs message, verifies with the PEM public key stored in `plugin_key`.
- Use a timestamp window (e.g. 300s) to protect against replay.

Headers
- `X-Timestamp`: integer seconds since epoch (string).
- `X-Signature`: base64 of the RSA signature.
- Optional: `X-Key-Id` or `X-Key-Version` for rotation.

Composer dependency (fallback)
- Native OpenSSL is preferred (`openssl_verify`). If the host lacks the OpenSSL PHP extension, include a pure-PHP fallback using phpseclib:

```bash
composer require phpseclib/phpseclib
```

Minimal endpoint snippet

```php
<?php
// Example callback for a WP REST endpoint or plugin hook.

function verify_laravel_signed_request_for_user($user_public_pem) {
    $timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
    $signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
    if (empty($timestamp) || empty($signature)) {
        return new WP_Error('missing_headers', 'Missing signature headers', ['status' => 400]);
    }

    $rawBody = file_get_contents('php://input');
    $message = $timestamp . '.' . $rawBody;

    // Try native OpenSSL first
    $verified = false;
    if (extension_loaded('openssl') && function_exists('openssl_verify')) {
        $pub = openssl_pkey_get_public($user_public_pem);
        if ($pub) {
            $ok = openssl_verify($message, base64_decode($signature), $pub, OPENSSL_ALGO_SHA256);
            $verified = ($ok === 1);
            openssl_free_key($pub);
        }
    }

    // phpseclib fallback
    if (! $verified && class_exists('\phpseclib3\Crypt\PublicKeyLoader')) {
        try {
            $pub = \phpseclib3\Crypt\PublicKeyLoader::load($user_public_pem);
            $verified = $pub->withHash('sha256')->verify($message, base64_decode($signature));
        } catch (\Throwable $e) {
            $verified = false;
        }
    }

    if (! $verified) {
        return new WP_Error('invalid_signature', 'Signature invalid', ['status' => 403]);
    }

    // Replay protection: accept requests within +/- 300s by default
    if (abs(time() - (int)$timestamp) > 300) {
        return new WP_Error('expired', 'Request expired', ['status' => 403]);
    }

    // Verified and fresh
    return true;
}

// Example usage in a REST callback
add_action('rest_api_init', function() {
    register_rest_route('myplugin/v1', '/laravel-hook', [
        'methods' => 'POST',
        'callback' => function(WP_REST_Request $request) {
            // determine which WP user this is for (e.g. from body or a site option)
            $body = $request->get_body();
            $data = json_decode($body, true);
            $plugin_key_pem = ''; // load from DB: user meta or options

            $res = verify_laravel_signed_request_for_user($plugin_key_pem);
            if (is_wp_error($res)) {
                return new WP_REST_Response($res->get_error_message(), $res->get_error_data()['status'] ?? 403);
            }

            // proceed: $data is trusted (signature verified)
            return new WP_REST_Response(['ok' => true], 200);
        }
    ]);
});

```

Operational notes
- Store the public PEM exactly as generated (including `-----BEGIN PUBLIC KEY-----` / footer) in the `plugin_key` field.
- Consider storing `sha256` of the PEM in a separate indexed column for fast lookups.
- If you store private keys in Laravel's DB (for per-user keypairs), encrypt them server-side with Laravel's `Crypt::encryptString()` or use a KMS.
- Log failed verification attempts and rate-limit requests to the endpoint.
- Enforce HTTPS on all endpoints.

That's it — drop this file into your repo and copy the snippet into your plugin endpoint. If you want, I can create a composer-ready plugin skeleton with this code wired in.
