Utilities

Signature Validation

Complete guide to validating Super Qi webhook and API response signatures for secure payment integration

Overview

Super Qi uses RSA256 digital signatures to sign all webhook notifications and API responses. Validating these signatures is critical to ensure that:

  • The request actually came from Super Qi (authentication)
  • The data has not been tampered with (integrity)
  • Your application processes only legitimate payment notifications
Always validate signatures before processing webhook data. Skipping signature validation exposes your application to security risks including fraudulent payment notifications and data manipulation.

When to Validate Signatures

Webhooks (Incoming from Super Qi)

Always validate signatures for webhook notifications including:

  • Payment notifications
  • Refund notifications
  • Order status updates
  • Any asynchronous callbacks from Super Qi

Webhook Source IP Addresses

If your infrastructure requires IP whitelisting, webhook calls from Super Qi will originate from the following IP addresses:

  • 8.209.108.80
  • 47.254.141.238
  • 47.254.132.7
Ensure these IPs are whitelisted in your firewall or security group settings to receive webhook notifications.

Signature Format

Super Qi sends signatures in the Signature HTTP header with this format:

Signature: algorithm=RSA256, keyVersion=2, signature=<base64_encoded_signature>

Components:

  • algorithm - Always RSA256 (SHA256withRSA)
  • keyVersion - Version number of the public key to use
  • signature - Base64-encoded RSA signature
The keyVersion indicates which Super Qi public key to use for verification. Make sure you're using the correct public key version that matches the signatures you receive.

Validation Process

Step 1: Extract Required Headers

Extract these headers from the webhook request:

clientID := request.Header.Get("Client-Id")
requestTime := request.Header.Get("Request-Time")
signatureHeader := request.Header.Get("Signature")

Step 2: Read the Request Body

bodyBytes, err := io.ReadAll(request.Body)
if err != nil {
    return err
}
bodyString := string(bodyBytes)
Important: Read the raw body bytes exactly as received. Do not parse, format, or modify the JSON before validation.

Step 3: Construct Content to be Validated

Build the content string that was signed by Super Qi:

Format:

<HTTP_METHOD> <HTTP_URI>
<Client-Id>.<Request-Time>.<HTTP_BODY>

Example:

httpMethod := "POST"
path := "/api/payment/webhook"

signContent := fmt.Sprintf("%s %s\n%s.%s.%s",
    httpMethod,
    path,
    clientID,
    requestTime,
    bodyString,
)

Result:

POST /api/payment/webhook
<Client-Id>.<Request-Time>.<JSON_BODY>

Step 4: Parse the Signature Header

Extract the base64-encoded signature from the header:

func parseSignature(signatureHeader string) (string, error) {
    parts := strings.Split(signatureHeader, ",")
    for _, part := range parts {
        part = strings.TrimSpace(part)
        if strings.HasPrefix(part, "signature=") {
            return strings.TrimPrefix(part, "signature="), nil
        }
    }
    return "", errors.New("signature not found in header")
}

signatureBase64, err := parseSignature(signatureHeader)
if err != nil {
    return err
}

Step 5: Decode the Signature

signature, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
    return fmt.Errorf("failed to decode signature: %v", err)
}

Step 6: Verify the Signature

Use the Super Qi public key to verify:

import (
    "crypto"
    "crypto/rsa"
    "crypto/sha256"
)

// Hash the sign content
hash := sha256.Sum256([]byte(signContent))

// Verify using Super Qi's public key
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hash[:], signature)
if err != nil {
    return fmt.Errorf("signature verification failed: %v", err)
}

// Signature is valid!

Complete Example

Here's a complete Go implementation:

package webhook

import (
    "crypto"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
)

type SignatureValidator struct {
    publicKey *rsa.PublicKey
}

func NewSignatureValidator(publicKeyPath string) (*SignatureValidator, error) {
    publicKey, err := loadPublicKey(publicKeyPath)
    if err != nil {
        return nil, err
    }
    return &SignatureValidator{publicKey: publicKey}, nil
}

func loadPublicKey(path string) (*rsa.PublicKey, error) {
    pemData, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(pemData)
    if block == nil {
        return nil, errors.New("failed to parse PEM block")
    }

    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    rsaPub, ok := pub.(*rsa.PublicKey)
    if !ok {
        return nil, errors.New("not an RSA public key")
    }

    return rsaPub, nil
}

func (v *SignatureValidator) ValidateWebhook(
    r *http.Request,
    path string,
) error {
    // Extract headers
    clientID := r.Header.Get("Client-Id")
    requestTime := r.Header.Get("Request-Time")
    signatureHeader := r.Header.Get("Signature")

    if clientID == "" || requestTime == "" || signatureHeader == "" {
        return errors.New("missing required headers")
    }

    // Read body
    bodyBytes, err := io.ReadAll(r.Body)
    if err != nil {
        return err
    }

    // Construct sign content
    signContent := fmt.Sprintf("POST %s\n%s.%s.%s",
        path,
        clientID,
        requestTime,
        string(bodyBytes),
    )

    // Parse signature
    signatureBase64, err := parseSignatureHeader(signatureHeader)
    if err != nil {
        return err
    }

    // Decode signature
    signature, err := base64.StdEncoding.DecodeString(signatureBase64)
    if err != nil {
        return fmt.Errorf("failed to decode signature: %v", err)
    }

    // Hash sign content
    hash := sha256.Sum256([]byte(signContent))

    // Verify signature
    err = rsa.VerifyPKCS1v15(v.publicKey, crypto.SHA256, hash[:], signature)
    if err != nil {
        return fmt.Errorf("signature verification failed: %v", err)
    }

    return nil
}

func parseSignatureHeader(header string) (string, error) {
    parts := strings.Split(header, ",")
    for _, part := range parts {
        part = strings.TrimSpace(part)
        if strings.HasPrefix(part, "signature=") {
            return strings.TrimPrefix(part, "signature="), nil
        }
    }
    return "", errors.New("signature not found in header")
}

Usage:

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    validator, err := NewSignatureValidator("path/to/superqi_public_key.pem")
    if err != nil {
        http.Error(w, "Server configuration error", 500)
        return
    }

    err = validator.ValidateWebhook(r, "/api/payment/webhook")
    if err != nil {
        log.Printf("Invalid signature: %v", err)
        http.Error(w, "Invalid signature", 401)
        return
    }

    // Signature is valid - safe to process webhook
    // ... process payment notification
}

Public Key Management

Obtaining the Public Key

Use the RSA public key that matches your environment for signature verification (PEM format). Make sure you use the UAT key when validating signatures from the UAT environment and the Production key when validating signatures from production.

UAT Public Key

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249W/txwtMNVMw2jv5uN
u06atBtlvOon+EYopcBV1WzSKXsllq8k+5GEKaRw2n2A2Ix7IalDLfxf5cbPLLnC
IdqyPEfeoQBN2xBL2m0cjFFoPL0JJIHHHvJs75MVR8bpUFuipGOxK+a4316mMlRv
eX4F+YmpCJFXoHgJzJrQlglRNO/HZYQVaP3JRZg4KNSg1o2esxgaCImhCt8owegw
ou9GgYlEYpQjlQYSAgGill36nIMEoUqUqVYUA0o4sOLYPb2hepUROED8Zuc0mW/q
oGVQBihuZInhM2UOiuuw/tP/cpmNbMR9SeCbEDUt9HHd60KpXciNim96tz1zIZQv
jQIDAQAB
-----END PUBLIC KEY-----

Production Public Key

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTCsasBhpdL0juG1mql4
8efGDM//c/GBcVs/weV0BQKQ6fdBYbgLZ0uR+OSsd4krEw3CltsQKUjE5B3VSjD1
xB2aYNe2uF7xIdqPlYlYGnXiQUTuXtOuNQMc9myFGZlhPPVJwotySOFRgjgv79ua
NLYaqlDYvUHyGaJACOfFAq9z2cn/KYysJPZQXikMToT2WaRzvmT1xjYDodd2jdq7
SDBNRZrp8loM25z4Q/MbcHxFYTssWU7i+DsIgUAsK7T832QXMLtYm45pa4UVPduP
RG02LDRQfzQkyXkrIe/DyjSwBL+AjFTgWiecrmknCdTSiMyUd1CYmYyRfEuKFv0e
CQIDAQAB
-----END PUBLIC KEY-----

If you encounter any issues with signature validation, please contact the Super Qi MiniApp team.

Common Issues & Troubleshooting

Issue: "signature verification failed: crypto/rsa: verification error"

Possible Causes:

  1. Wrong public key - Ensure you're using the correct key for the keyVersion
  2. Incorrect path - The HTTP path in sign content must match exactly (e.g., /api/payment/webhook)
  3. Modified body - Don't parse or reformat JSON before validation
  4. Wrong timestamp header - Use Request-Time for webhooks, not Response-Time

Debug Steps:

// Log the exact sign content
log.Printf("Sign Content:\n%s", signContent)

// Verify it matches this format:
// POST /api/payment/webhook
// <Client-Id>.<Request-Time>.<JSON_BODY>

Issue: "signature not found in header"

The signature header has spaces after commas. Make sure to trim spaces:

part = strings.TrimSpace(part)  // Important!
if strings.HasPrefix(part, "signature=") {
    return strings.TrimPrefix(part, "signature=")
}

Issue: "failed to parse PEM block"

Possible Causes:

  1. Public key file is corrupted or incomplete
  2. Wrong file format (not PEM)
  3. Extra spaces or characters in the key file

Verification:

# Check if it's a valid public key
openssl rsa -pubin -in superqi_public_key.pem -text -noout

Issue: Different signatures for same content

This is normal. RSA signatures include random padding (PKCS#1 v1.5), so the same content will produce different signatures each time. This doesn't affect verification.

Summary

Signature validation is essential for secure Super Qi integration:

  1. Extract Client-Id, Request-Time, Signature headers
  2. Construct sign content: POST <path>\n<Client-Id>.<Request-Time>.<Body>
  3. Parse the base64 signature from the Signature header
  4. Verify using RSA SHA256 with Super Qi's public key
  5. Process webhook only if signature is valid