In this example, our client is going to use OpenSSL to sign the data before sending it to the server. When the server receives the data it will use OpenSSL to check if it was signed with the given signature or not. If the verification was successful then the client is considered as trustworthy. In this process client uses his "private key" and the server uses client's "public key" for verification. See more info for Signing HTTP Messages, openssl_sign and openssl_verify.


Flow



Classes


SignatureController


I am being very lazy here and doing too much in controller but it is purely because I wanted to keep the post as short as possible. You can move the logic to a service class - just an example.


namespace App\Controller;

use App\Util\SignatureUtil;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
* @Route("/signatures")
*/
class SignatureController
{
private $signatureUtil;
private $publicKey;
private $privateKey;

public function __construct(
SignatureUtil $signatureUtil,
string $publicKey,
string $privateKey
) {
$this->signatureUtil = $signatureUtil;
$this->publicKey = $publicKey;
$this->privateKey = $privateKey;
}

/**
* @Route("/client/create-keys", methods={"GET"})
*/
public function createKeys(): Response
{
$keys = $this->signatureUtil->createKeys();

file_put_contents($this->publicKey, $keys['public_key']);
file_put_contents($this->privateKey, $keys['private_key']);

return new JsonResponse('Done!');
}

/**
* @Route("/client/create-signature", methods={"POST"})
*/
public function createSignature(Request $request): Response
{
$privateKey = file_get_contents($this->privateKey);
$data = json_decode($request->getContent(), true)['data'];
$signature = $this->signatureUtil->createSignature($data, $privateKey);

return new JsonResponse($signature);
}

/**
* @Route("/server/verify-signature", methods={"POST"})
*/
public function verifySignature(Request $request): Response
{
$publicKey = file_get_contents($this->publicKey);
$data = json_decode($request->getContent(), true)['data'];
$signature = $request->headers->get('X-Signature');

$valid = $this->signatureUtil->verifySignature($data, $signature, $publicKey);

return new JsonResponse(['valid' => $valid]);
}
}

services:
...

App\Controller\SignatureController:
arguments:
$publicKey: '%kernel.project_dir%/config/ssl/public_key.pem'
$privateKey: '%kernel.project_dir%/config/ssl/private_key.pem'


SignatureUtil


Read the comments in the code please.


namespace App\Util;

use RuntimeException;

class SignatureUtil
{
/**
* This is only relevant to server API.
*
* The "public_key" is shared with server.
* The "private_key" is kept securely by the client only.
*/
public function createKeys(): array
{
$keys = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);

openssl_pkey_export($keys, $privateKey);

$keyDetails = openssl_pkey_get_details($keys);
$publicKey = $keyDetails['key'];

return [
'public_key' => $publicKey,
'private_key' => $privateKey,
];
}

/**
* This is only relevant to client API.
*
* The client calls this method to create a "signature" based on the "data".
* The client then sends the "signature" and the "data" to the server.
*/
public function createSignature(string $data, string $privateKey): string
{
openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);

return base64_encode($signature);
}

/**
* This is only relevant to server API.
*
* The server checks "data" against the "signature" to see if the client is genuine or not.
*/
public function verifySignature(string $data, string $signature, string $publicKey): bool
{
$verify = openssl_verify($data, base64_decode($signature), $publicKey, 'sha256WithRSAEncryption');

if (1 == $verify) {
return true;
}

if (0 === $verify) {
return false;
}

throw new RuntimeException('An error occurred while verifying the signature.');
}
}

Test


Create client keys


$ ls -l config/ssl/
total 0

$ curl -X GET \
> http://localhost/signatures/client/create-keys
"Done!"

$ ls -l config/ssl/
-rw-rw-rw- 1 501 dialout 1704 Jan 13 2019 private_key.pem
-rw-rw-rw- 1 501 dialout 451 Jan 13 2019 public_key.pem

Create signature


$ curl -X POST \
http://localhost/signatures/client/create-signature \
-H 'Content-Type: application/json' \
-d '{
"data": "Hello World!"
}'

"S3PRm0yePp3fIgEhtIYXq+oQrFkbkxSHiJHbyESu0pLRILg7N2AWrdsvkK6LzK9F2WB1DKXrAAXD3L+R0au2penzBCnX9ERtJXZq71h2qVZYbkrxbY4jQsYU/aUg3KxakVQxTi4gehj0zeGvTV6OxkjFbKq0/KLgWD9AjYZxeWnmFhh8wbbGtmFMZzBkGU7iXSC+/5kKguI28WWt1ALYHJJxSsJWmZnccnAELo6tVhqBKxavuB/E6TZTEiWGeTC0wAWcjuqfEIFoA5b1fx73L6vcCGqf17Ov42NIJL50BRlmRcHkmnNJXVUUbSLmI+Cu+uzwE3KN9z/pVv8dfLIFAA=="

Consume API


Use signature in X-Signature header.


$ curl -X POST \
> http://localhost/signatures/server/verify-signature \
> -H 'Content-Type: application/json' \
> -H 'X-Signature: S3PRm0yePp3fIgEhtIYXq+oQrFkbkxSHiJHbyESu0pLRILg7N2AWrdsvkK6LzK9F2WB1DKXrAAXD3L+R0au2penzBCnX9ERtJXZq71h2qVZYbkrxbY4jQsYU/aUg3KxakVQxTi4gehj0zeGvTV6OxkjFbKq0/KLgWD9AjYZxeWnmFhh8wbbGtmFMZzBkGU7iXSC+/5kKguI28WWt1ALYHJJxSsJWmZnccnAELo6tVhqBKxavuB/E6TZTEiWGeTC0wAWcjuqfEIFoA5b1fx73L6vcCGqf17Ov42NIJL50BRlmRcHkmnNJXVUUbSLmI+Cu+uzwE3KN9z/pVv8dfLIFAA==' \
> -d '{
> "data": "Hello World!"
> }'

{"valid":true}