DEV Community

Cover image for Signing & Verifying OnRequest PubNub Functions
PubNub Developer Relations for PubNub

Posted on

Signing & Verifying OnRequest PubNub Functions

PubNub OnRequest Functions allow developers to create hosted REST API endpoints that process incoming HTTP requests. This feature is incredibly useful for real-time communication services where scale and reliability are needed quickly. However, it may be necessary to sign these requests to ensure the integrity and authenticity of the data being sent. In this blog post, we'll cover how to sign requests and verify them in PubNub OnRequest Functions and why this process is essential for secure communication.

How Does Signing Work?

Signing a request involves generating a digital signature uniquely representing the data within the request payload and request headers using a private key. When the server receives the request, it can be verified by comparing the provided signature with one generated using the corresponding public key. This process ensures that the request has not been tampered with during transmission. 

In essence, signing a request helps authenticate both the sender and the integrity of the data. If the request is altered at any point during transit, the request signature will not match the expected value, and the request can be rejected.

Why Sign Requests? The Advantages

  1. Data Integrity: Signing ensures that the payload has not been altered during transmission. If any modification is made, the signature will no longer match, flagging the request as compromised.

  2. Authenticity: Using a public/private key pair, you can verify that the request came from a trusted source with the correct permissions. This eliminates the risk of spoofed requests.

  3. Security: Encrypted signatures prevent unauthorized access or manipulation of your data, especially when sensitive information is transmitted.

  4. Replay Attack Prevention: Using unique signing for each request helps guard against replay attacks, where malicious actors attempt to resend previously intercepted valid requests.

What Are PubNub Functions and Why Use Them?

PubNub Functions are serverless environments that allow developers to run custom code directly within PubNub’s platform in response to messages, events, or, in this case, HTTP requests. They offer several key benefits:

  1. Global Deployment: Cloud functions are deployed globally within seconds, ensuring low-latency execution near users, no matter their location.
  2. Auto-Scaling: PubNub handles automatic scaling, ensuring functions can handle any load—from small to large traffic—without manual intervention.
  3. No Server Management: With PubNub Functions, developers focus on code while PubNub manages infrastructure, eliminating the need for server provisioning or maintenance.

PubNub Functions are delivered as a Node.js environment, which provides access to several helper functions to save you development effort, including cryptography and the PubNub SDK. For more information on PubNub functions, including dependencies, please see our Functions help documentation.

Verifying Signatures in PubNub Functions

PubNub OnRequest Functions are designed to process incoming HTTP requests, and by using the crypto module, it's possible to verify signatures using public keys. In this blog, we’ll show you how to:

  • Generate key pairs
  • Sign payloads
  • Create an OnRequest Function that verifies signed requests using ECDSA (Elliptic Curve Digital Signature Algorithm).

Step 1: Generate a PEM Key Pair

You need a public/private key pair to sign and verify requests. There are multiple ways to generate these keys; one common method is using OpenSSL, and since we are using ECDSA, the ec param is used. If you have Git Bash installed, you can use the following command to generate a private key in PEM format:

openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
Enter fullscreen mode Exit fullscreen mode

Step 2: Convert PEM to JWK Format

Next, we must parse and transform the private key from PEM format into JWK (JSON Web Key) format. Specifically, we want to extract and initialize the following fields:

  • privateKey.d: The private key material.
  • publicKey.x: The X-coordinate of the public key.
  • publicKey.y: The Y-coordinate of the public key.

You can use a Python script to handle this conversion and ensure the fields are properly initialized. Below is an example script to generate the JWKs:

import json
import base64
from ecdsa import SigningKey, VerifyingKey, NIST256p

# Path to the private key PEM file
PRIVATE_KEY_PEM = "private-key.pem"

# Load the private key from the PEM file
def load_private_key_from_pem(pem_file):
    with open(pem_file, 'rb') as f:
        private_key_data = f.read()
        private_key = SigningKey.from_pem(private_key_data)
        return private_key

# Convert public key and private key to JWK format
def convert_to_jwk(private_key):
    public_key = private_key.get_verifying_key()
    public_key_bytes = public_key.to_string()

    # Split public key into x and y coordinates
    x, y = public_key_bytes[:len(public_key_bytes) // 2], public_key_bytes[len(public_key_bytes) // 2:]

    # Convert to base64url encoding
    x_base64 = base64.urlsafe_b64encode(x).decode().rstrip("=")
    y_base64 = base64.urlsafe_b64encode(y).decode().rstrip("=")
    d_base64 = base64.urlsafe_b64encode(private_key.to_string()).decode().rstrip("=")

    # Create the public and private JWKs
    public_jwk = {
        "kty": "EC",
        "crv": "P-256",
        "x": x_base64,
        "y": y_base64,
        "use": "sig"
    }

    private_jwk = {
        "kty": "EC",
        "crv": "P-256",
        "d": d_base64,
        "use": "sig"
    }

    return public_jwk, private_jwk

# Simplified script execution
# Load private key from PEM
private_key = load_private_key_from_pem(PRIVATE_KEY_PEM)

# Convert to JWK format
public_jwk, private_jwk = convert_to_jwk(private_key)

# Output the JWKs to the console
print("Public JWK:\n", json.dumps(public_jwk, indent=4))
print("Private JWK:\n", json.dumps(private_jwk, indent=4))
Enter fullscreen mode Exit fullscreen mode

Step 3: Sign the Payload Using the Private Key

Once the JWKs have been generated, the next step is to sign the message payload using the private key. This will produce a unique signature that corresponds to the content of the payload. The private key ensures that the signature is cryptographically tied to the request data.

import base64
from ecdsa import SigningKey, NIST256p
import hashlib
import json

# Private JWK constant (manually copied from the output of the first script)
PRIVATE_JWK = {
    "kty": "EC",
    "crv": "P-256",
    "d": "PASTE_YOUR_D_VALUE_HERE",
    "use": "sig"
}

# Payload constant
PAYLOAD = "SOME_PAYLOAD"  # Replace with your actual payload

# Convert JWK to private key
def private_jwk_to_signing_key(jwk):
    d = base64.urlsafe_b64decode(jwk['d'] + '==')  # Add padding back for base64 decoding
    private_key = SigningKey.from_string(d, curve=NIST256p, hashfunc=hashlib.sha256)
    return private_key

# Sign the payload
def sign_payload(private_key, payload):
    signature = private_key.sign_deterministic(payload.encode(), hashfunc=hashlib.sha256)
    signature_base64 = base64.urlsafe_b64encode(signature).decode()
    return signature_base64

# Convert JWK to private key
private_key = private_jwk_to_signing_key(PRIVATE_JWK)

# Sign the payload
signature = sign_payload(private_key, PAYLOAD)

# Format the req payload
request_payload = {
    "payload": PAYLOAD,
    "signature": signature
}

# Output the formatted request payload as JSON
print(json.dumps(request_payload, indent=4))
Enter fullscreen mode Exit fullscreen mode

Step 4: Verify the Signature in PubNub Functions

Now that we have a signed payload, we can set up an OnRequest Function in PubNub to verify the request. Using the public JWK generated in step 2 and the PubNub crypto module, we can verify whether the signature matches and whether the request is valid.

Here’s an outline of how the OnRequest Function works:

  1. The payload and signature are sent as part of the HTTP request.
  2. The Function extracts these values and uses the public key (JWK format) to verify the integrity of the payload by checking the signature.
  3. If the signature is valid, the request is processed. If not, the request is rejected.
const crypto = require("crypto");
//  async method, callable only when invoked
export default (request, response) => {
  try {
    return request.json().then((requestBody) => {
      const payload = requestBody.payload;
      const signature = requestBody.signature;

      console.log('Payload: ' + payload);
      console.log('Signature: ' + signature);

      // Public key in JWK format
      const publicKey = {
        "kty": "EC",
        "crv": "P-256",
        "x": "<PUBLIC_KEY_X_VALUE>",
        "y": "<PUBLIC_KEY_Y_VALUE>",
        "use": "sig"
      };

      // Verify the signature using PubNub's crypto.verify with JWK directly
      return crypto.verify(signature, publicKey, payload, crypto.ALGORITHM.ECDSA_P256_SHA256).then((isSignatureValid) => {
        console.log("Signature verified:", isSignatureValid);
        if (isSignatureValid) {
          response.status = 200;
          return response.send("Ok");
        } else {
          response.status = 401;
          return response.send("Signature verification failed.");
        }
      }).catch((error) => {
        console.log("Unable to verify signature:", error);
        response.status = 400;
        return response.send("Unable to verify signature");
      });
    });
  } catch (error) {
    console.log("Unable to verify signature:", error);
    response.status = 400;
    return response.send("Unable to verify signature");
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 5: Testing the Function with Postman

Once the Function is set up, you can test it using tools like Postman. By sending POST requests with the valid payload and signature provided by the script in step 3, you can verify that the request is accepted and processed correctly. Conversely, altering the payload or sending an invalid signature will result in the request being rejected.

Successful Request

Below is an example screenshot of a successful request where signature validation has succeeded.

Signing & Verifying OnRequest PubNub Functions 2

Failed Request

Here is an example of a failed request due to a tampered payload. In this case the signature verification fails.

Signing & Verifying OnRequest PubNub Functions 1

Summary

Signing requests when using PubNub OnRequest Functions is a crucial step to ensure the security, authenticity, and integrity of your data. By leveraging the crypto module and using public/private key pairs, developers can safeguard their applications from malicious attacks or data tampering. Whether using RSA, DSA, or ECDSA, signing and verifying signatures should be a fundamental part of your application security strategy.

Once you have a working OnRequest Function that verifies signatures, your system is much more secure against unauthorized or compromised requests!

Get Started with PubNub by signing up for a free account right now.

How can PubNub help you?

This article was originally published on PubNub.com

Our platform helps developers build, deliver, and manage real-time interactivity for web apps, mobile apps, and IoT devices.

The foundation of our platform is the industry's largest and most scalable real-time edge messaging network. With over 15 points-of-presence worldwide supporting 800 million monthly active users, and 99.999% reliability, you'll never have to worry about outages, concurrency limits, or any latency issues caused by traffic spikes.

Experience PubNub

Check out Live Tour to understand the essential concepts behind every PubNub-powered app in less than 5 minutes

Get Setup

Sign up for a PubNub account for immediate access to PubNub keys for free

Get Started

The PubNub docs will get you up and running, regardless of your use case or SDK

Top comments (0)