13/01/2019 - PHP, SYMFONY
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.
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'
/client/create-keys
- Use this endpoint to programmatically create client's public and private keys. Then give private key to client and public key to server. Note: I added this endpoint just for demonstration purposes so you shouldn't do this in real world cases. Instead, create those keys in terminal./client/create-signature
- Use this endpoint to sign your data. The response of this endpoint will contain the signature which will be sent to the server along with the actual data. Note: Assume that this endpoint is in the client API./server/verify-signature
- When the client sends the request, it will land here where signature verification takes place. Note: Assume that this endpoint is in the server API.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.');
}
}
$ 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
$ 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=="
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}