# Signature Verification This document explains how to verify that API requests and webhooks from BRIJ are authentic and have not been tampered with. ## Overview All requests from BRIJ to your endpoints (API calls and webhooks) include a cryptographic signature in the `X-BRIJ-Signature` HTTP header. This signature is a JSON Web Token (JWT) signed with BRIJ's private key using the RS256 algorithm. By verifying this signature, you can ensure that: * The request originated from BRIJ * The request payload has not been modified in transit * The request is not a replay of a previous request ## Signature Format ### HTTP Header ``` X-BRIJ-Signature: eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0... ``` ### JWT Structure The signature is a standard JWT with three parts: header, payload, and signature. ```json { "alg": "RS256", // (1)! "typ": "JWT" // (2)! } ``` 1. Always `RS256` (RSA with SHA-256) 2. Token type, always `JWT` ```json { "iss": "brij.fi", // (1)! "aud": "your-partner-id", // (2)! "iat": 1700000000, // (3)! "exp": 1700000600, // (4)! "jti": "f47ac10b-58cc-4372-a567-0e02b2c3d479", // (5)! "payload_hash": "a1b2c3d4e5f6789..." // (6)! } ``` 1. Issuer - always `brij.fi` 2. Audience - your partner ID 3. Issued at - Unix timestamp (seconds) when the token was created 4. Expiration - Unix timestamp (seconds), 10 minutes after `iat` 5. JWT ID - unique UUID for this request (use for idempotency) 6. SHA-256 hash of the request body, hex-encoded ## Verification Steps Follow these steps to verify an incoming request: ### 1. Extract the JWT Get the `X-BRIJ-Signature` header from the incoming request. ### 2. Verify the JWT Signature Using BRIJ's public key (see below), verify the JWT signature. This confirms the token was signed by BRIJ. ### 3. Validate Claims After signature verification, validate the following claims: * **`iss`** must equal `brij.fi` * **`aud`** must equal your partner ID * **`exp`** must be greater than the current Unix timestamp (token not expired) ### 4. Verify Payload Integrity 1. Read the raw request body 2. Calculate the SHA-256 hash of the body 3. Hex-encode the hash 4. Compare with the `payload_hash` claim If they match, the payload has not been tampered with. ### 5. (Optional) Check for Replay Attacks Store the `jti` claim and reject any requests with a previously seen `jti`. This prevents replay attacks. ## Public Keys Use the appropriate public key based on your environment: #### Production ``` -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0JTgjx1M34EzEn8nZ+MH rjlE6wh0698MHeTsC9GUQX4SdhDLi02R+r6i7hknhfgCmTVDHyfvoCAp0Jm8G0nI OkgsA2VrBxLAZoCtQyRBz26+0nkQcWNtg4s4MtyQmCrrz/hrl6CU0IhiKKfSPf/i DeIXFJd7xyQQdHaBfl8ZQqdvZJeJJLlqYOHPn8vtq6ubpeFfA43jbeSyS+VzBfMM eAHu1HvG54oYJEYBNsHKe7ZMJvgX36I31aOFlMEuX4wjittBqdlGa4CvB7P2sc0V t2cp6QVbxjSAIA1bc6uM5dhCrC7fuZ4HNGqWWNK7yfqUsWrDsCRr3QjQiogbIQu4 oQIDAQAB -----END PUBLIC KEY----- ``` #### Demo ``` -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArPPIIDwHo5DEM7b+uor+ bYEhwjo1C3QGf7/QkQadsop1Qee7+bW3df2o4YANyWVZIj+0oWT8RsjQGmdNzvDp qQzRYYHy08ZmU3v/1pPXE4JpmMuXxKZ6tJDagn7poeaWpGdm3BKLyd8qNf4ZXs8q G3BoqBKjwaDpK2aYPAgRSoPT6iT7K2i7dfhJV5pjW4lUXqwGcVNBn7pxbkZGwj1T EHDIEUWgdq9L3Swo1hI2/fIa0KFa7wiKPWALyounRUGCEcOwpHG32k+L8odoAuLe NSjyNIDYXuNWzICuyGq/Glotf+FTrGlMT634v7nRC/qX5BGhZK3SA6KBz5O+hncH rwIDAQAB -----END PUBLIC KEY----- ``` ## Code Examples #### Node.js / TypeScript ```typescript import jwt from 'jsonwebtoken'; import crypto from 'crypto'; // BRIJ public key (use appropriate key for your environment) const BRIJ_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- [YOUR_ENVIRONMENT_PUBLIC_KEY] -----END PUBLIC KEY-----`; const YOUR_PARTNER_ID = 'your-partner-id'; interface BrijJwtPayload { iss: string; aud: string; iat: number; exp: number; jti: string; payload_hash: string; } function verifyBrijRequest( signatureHeader: string, requestBody: Buffer | string ): { valid: boolean; jti?: string; error?: string } { try { // 1. Verify JWT signature and decode const decoded = jwt.verify(signatureHeader, BRIJ_PUBLIC_KEY, { algorithms: ['RS256'], issuer: 'brij.fi', audience: YOUR_PARTNER_ID, }) as BrijJwtPayload; // 2. Verify payload hash const bodyBuffer = typeof requestBody === 'string' ? Buffer.from(requestBody) : requestBody; const calculatedHash = crypto .createHash('sha256') .update(bodyBuffer) .digest('hex'); if (calculatedHash !== decoded.payload_hash) { return {valid: false, error: 'Payload hash mismatch'}; } // 3. Return success with jti for idempotency tracking return {valid: true, jti: decoded.jti}; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } // Express.js middleware example import {Request, Response, NextFunction} from 'express'; function brijSignatureMiddleware( req: Request, res: Response, next: NextFunction ) { const signature = req.headers['x-brij-signature'] as string; if (!signature) { return res.status(401).json({error: 'Missing X-BRIJ-Signature header'}); } // Note: You need raw body access - configure express accordingly const result = verifyBrijRequest(signature, req.body); if (!result.valid) { return res.status(401).json({error: result.error}); } // Optionally check jti for replay protection // if (seenJtis.has(result.jti)) { // return res.status(409).json({ error: 'Duplicate request' }); // } // seenJtis.add(result.jti); next(); } ``` #### Python ```python import hashlib import jwt from typing import Tuple, Optional # BRIJ public key (use appropriate key for your environment) BRIJ_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- [YOUR_ENVIRONMENT_PUBLIC_KEY] -----END PUBLIC KEY-----""" YOUR_PARTNER_ID = "your-partner-id" def verify_brij_request( signature_header: str, request_body: bytes ) -> Tuple[bool, Optional[str], Optional[str]]: """ Verify a BRIJ request signature. Returns: Tuple of (is_valid, jti, error_message) """ try: # 1. Verify JWT signature and decode decoded = jwt.decode( signature_header, BRIJ_PUBLIC_KEY, algorithms=["RS256"], issuer="brij.fi", audience=YOUR_PARTNER_ID, ) # 2. Verify payload hash calculated_hash = hashlib.sha256(request_body).hexdigest() if calculated_hash != decoded["payload_hash"]: return False, None, "Payload hash mismatch" # 3. Return success with jti for idempotency tracking return True, decoded["jti"], None except jwt.ExpiredSignatureError: return False, None, "Token expired" except jwt.InvalidAudienceError: return False, None, "Invalid audience" except jwt.InvalidIssuerError: return False, None, "Invalid issuer" except jwt.InvalidSignatureError: return False, None, "Invalid signature" except Exception as e: return False, None, str(e) # ------------------------------ # Flask example # ------------------------------ from flask import Flask, request, jsonify flask_app = Flask(__name__) @flask_app.before_request def verify_brij_signature(): # Skip verification for non-BRIJ endpoints if not request.path.startswith("/webhook"): return signature = request.headers.get("X-BRIJ-Signature") if not signature: return jsonify({"error": "Missing X-BRIJ-Signature header"}), 401 is_valid, jti, error = verify_brij_request(signature, request.get_data()) if not is_valid: return jsonify({"error": error}), 401 # Optionally store jti in request context for replay protection request.brij_jti = jti # ------------------------------ # FastAPI example (alternative) # ------------------------------ from fastapi import FastAPI, Request, HTTPException, Depends fastapi_app = FastAPI() async def verify_brij_signature(request: Request) -> str: signature = request.headers.get("X-BRIJ-Signature") if not signature: raise HTTPException(status_code=401, detail="Missing X-BRIJ-Signature header") body = await request.body() is_valid, jti, error = verify_brij_request(signature, body) if not is_valid: raise HTTPException(status_code=401, detail=error) return jti @fastapi_app.post("/webhook") async def webhook(request: Request, jti: str = Depends(verify_brij_signature)): # Process webhook... return {"status": "ok", "request_id": jti} ``` #### Go ```go package main import ( "bytes" "context" "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" "github.com/golang-jwt/jwt/v5" ) // BRIJ public key (use appropriate key for your environment) var brijPublicKeyPEM = `-----BEGIN PUBLIC KEY----- [YOUR_ENVIRONMENT_PUBLIC_KEY] -----END PUBLIC KEY-----` const yourPartnerID = "your-partner-id" type BrijClaims struct { PayloadHash string `json:"payload_hash"` jwt.RegisteredClaims } func VerifyBrijRequest(signatureHeader string, requestBody []byte) (string, error) { // Parse the public key publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(brijPublicKeyPEM)) if err != nil { return "", fmt.Errorf("failed to parse public key: %w", err) } // Parse and verify the JWT token, err := jwt.ParseWithClaims( signatureHeader, &BrijClaims{}, func(token *jwt.Token) (interface{}, error) { // Verify the signing method if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return publicKey, nil }, ) if err != nil { return "", fmt.Errorf("failed to parse token: %w", err) } claims, ok := token.Claims.(*BrijClaims) if !ok || !token.Valid { return "", fmt.Errorf("invalid token") } // Verify issuer if claims.Issuer != "brij.fi" { return "", fmt.Errorf("invalid issuer: %s", claims.Issuer) } // Verify audience if !claims.VerifyAudience(yourPartnerID, true) { return "", fmt.Errorf("invalid audience") } // Verify payload hash hash := sha256.Sum256(requestBody) calculatedHash := hex.EncodeToString(hash[:]) if calculatedHash != claims.PayloadHash { return "", fmt.Errorf("payload hash mismatch") } return claims.ID, nil // Return jti for idempotency tracking } // HTTP middleware example func BrijSignatureMiddleware(next http.Handler) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("X-BRIJ-Signature") if signature == "" { http.Error(w, "Missing X-BRIJ-Signature header", http.StatusUnauthorized) return } // Read the body body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Failed to read body", http.StatusBadRequest) return } // Verify the signature jti, err := VerifyBrijRequest(signature, body) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } // Optionally check jti for replay protection // if seenJtis[jti] { // http.Error(w, "Duplicate request", http.StatusConflict) // return // } // seenJtis[jti] = true // Store body back for handler to read r.Body = io.NopCloser(bytes.NewBuffer(body)) // Add jti to context if needed ctx := context.WithValue(r.Context(), "brij_jti", jti) next.ServeHTTP(w, r.WithContext(ctx)) }, ) } ``` ## Security Best Practices ### Always Verify Signatures Never process a request from BRIJ without first verifying the signature. Unverified requests could be forged by malicious actors. ### Reject Expired Tokens The `exp` claim provides protection against replay attacks with old tokens. Always check that the token has not expired. ### Implement Idempotency Use the `jti` (JWT ID) claim to detect and reject duplicate requests: * Store seen `jti` values (e.g., in Redis with TTL matching token expiration) * Reject requests with previously seen `jti` values * This prevents replay attacks even with valid, non-expired tokens ### Use the Correct Public Key Make sure to use the appropriate public key for your environment: * Use the Demo public key for testing and development * Use the Production public key for live transactions ### Key Management * Perform signature verification server-side, never in client-side code * Subscribe to BRIJ notifications for key rotation announcements ## Troubleshooting ### "Invalid signature" Error * Ensure you're using the correct public key for the environment * Verify the JWT string is complete and not truncated ### "Token expired" Error * Check if your server's clock is synchronized (use NTP) * Tokens are valid for 10 minutes from issuance * If requests consistently expire, investigate network latency ### "Payload hash mismatch" Error * Ensure you're hashing the raw request body before any parsing * The hash must be calculated on the exact bytes received * Check for any middleware that modifies the body before verification ### "Invalid audience" Error * Verify your partner ID matches what BRIJ has configured * Partner IDs are case-sensitive ### Missing Header If you don't see the `X-BRIJ-Signature` header: * Check if your load balancer or proxy is stripping custom headers * Verify the header name is case-insensitive in your framework ## Support If you encounter issues with signature verification, contact BRIJ support with: * The full JWT token (from `X-BRIJ-Signature` header) * The raw request body * Your partner ID * The error message you're receiving