Digital Signature Verification
All webhook requests include a digital signature using the ED25519 algorithm. The signature is provided in the following HTTP header:X-Partoo-Signature-v1: BASE64_SIGNATURE
- Base64 decode the signature
- Recalculate the hash of the payload
- Use our public key to validate the signature
Use separate keys for production and sandbox environments.
Production Public Key
Production Public Key
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA0G9ciHL6XZQXuWq6W4dFLvwNEPWgcdtQgEVlBIwZWBQ=
-----END PUBLIC KEY-----
Sandbox Public Key
Sandbox Public Key
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEALsyvX2yVnG3ZKRIFfEvYk2nkzanoNgAqBSqdeNub4sM=
-----END PUBLIC KEY-----
Code Examples
Here are sample snippets to help you implement signature verification:from base64 import b64decode
import binascii
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
# load public key from filesystem, you may adapt depending on your secret management framework
public_key = serialization.load_pem_public_key(open("/var/secrets/partoo.pub.pem"))
def validate_signature(request):
if (signature:=request.headers.get("X-Partoo-Signature-v1")) is None:
raise ValueError("Missing signature")
# don't trust your inputs
# will raise a subclass of ValueError if format is invalid
decoded_signature = b64decode(signature, validate=True)
# validate payload's signature
try:
public_key.verify(decoded_signature, request.body.encode())
except Exception as e:
raise ValueError("Invalid signature") from e
const fs = require('fs');
const crypto = require('crypto');
// Load public key from filesystem
const publicKey = fs.readFileSync('/var/secrets/partoo.pub.pem', 'utf8');
function validateSignature(request) {
const signature = request.headers['x-partoo-signature-v1'];
if (!signature) {
throw new Error('Missing signature');
}
// Decode the base64 signature
let decodedSignature;
try {
decodedSignature = Buffer.from(signature, 'base64');
} catch (err) {
throw new Error('Invalid signature format');
}
// Verify the payload's signature
const payload = request.body;
if (!crypto.verify(null, Buffer.from(payload), publicKey, decodedSignature)) {
throw new Error('Invalid signature');
}
}
package main
import (
"crypto/ed25519"
"encoding/base64"
"encoding/pem"
"errors"
"io/ioutil"
"net/http"
)
func loadPublicKey(path string) (ed25519.PublicKey, error) {
keyData, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(keyData)
if block == nil {
return nil, errors.New("failed to parse PEM block")
}
publicKey := ed25519.PublicKey(block.Bytes[12:]) // Skip ASN.1 header
return publicKey, nil
}
func validateSignature(r *http.Request, payload []byte) error {
signature := r.Header.Get("X-Partoo-Signature-v1")
if signature == "" {
return errors.New("missing signature")
}
// Decode base64 signature
decodedSignature, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return errors.New("invalid signature format")
}
// Load public key
publicKey, err := loadPublicKey("/var/secrets/partoo.pub.pem")
if err != nil {
return err
}
// Verify signature
if !ed25519.Verify(publicKey, payload, decodedSignature) {
return errors.New("invalid signature")
}
return nil
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class WebhookValidator {
private static PublicKey loadPublicKey(String path) throws Exception {
String keyPem = new String(Files.readAllBytes(Paths.get(path)));
keyPem = keyPem.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(keyPem);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
return keyFactory.generatePublic(spec);
}
public static void validateSignature(String signatureHeader, String payload) throws Exception {
if (signatureHeader == null || signatureHeader.isEmpty()) {
throw new IllegalArgumentException("Missing signature");
}
// Decode base64 signature
byte[] decodedSignature;
try {
decodedSignature = Base64.getDecoder().decode(signatureHeader);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid signature format", e);
}
// Load public key
PublicKey publicKey = loadPublicKey("/var/secrets/partoo.pub.pem");
// Verify signature
Signature sig = Signature.getInstance("Ed25519");
sig.initVerify(publicKey);
sig.update(payload.getBytes());
if (!sig.verify(decodedSignature)) {
throw new SecurityException("Invalid signature");
}
}
}
<?php
function validateSignature($request, $payload) {
$signature = $request->header('X-Partoo-Signature-v1');
if (!$signature) {
throw new Exception('Missing signature');
}
// Decode base64 signature
$decodedSignature = base64_decode($signature, true);
if ($decodedSignature === false) {
throw new Exception('Invalid signature format');
}
// Load public key from filesystem
$publicKeyPem = file_get_contents('/var/secrets/partoo.pub.pem');
$publicKey = openssl_pkey_get_public($publicKeyPem);
if (!$publicKey) {
throw new Exception('Failed to load public key');
}
// Verify signature using Ed25519
$keyDetails = openssl_pkey_get_details($publicKey);
$publicKeyRaw = $keyDetails['key'];
// For Ed25519 verification with PHP, you may need sodium extension
if (!function_exists('sodium_crypto_sign_verify_detached')) {
throw new Exception('Sodium extension required for Ed25519 verification');
}
// Extract raw public key (skip ASN.1 header)
$rawKey = substr(base64_decode(
str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n"], '', $publicKeyPem)
), 12);
if (!sodium_crypto_sign_verify_detached($decodedSignature, $payload, $rawKey)) {
throw new Exception('Invalid signature');
}
}
?>
Shared Secret in URL
You may include a shared secret in your webhook URL: Examples:https://my.integration.io/webhooks/partoo/9fa91de19/business_update
https://my.integration.io/webhooks/partoo/business_update?key=9fa91de19