> ## Documentation Index
> Fetch the complete documentation index at: https://developers.partoo.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Secure your webhook integrations with signature verification and shared secrets.

Security is critical when dealing with automated event notifications.

## Digital Signature Verification

All webhook requests include a digital signature using the ED25519 algorithm. The signature is provided in the following HTTP header:

```http theme={null}
X-Partoo-Signature-v1: BASE64_SIGNATURE
```

To verify:

1. Base64 decode the signature
2. Recalculate the hash of the payload
3. Use our public key to validate the signature

<Tip>
  Use separate keys for production and sandbox environments.
</Tip>

Use different public keys depending on the environment:

<AccordionGroup>
  <Accordion title="Production Public Key">
    ```plaintext theme={null}
    -----BEGIN PUBLIC KEY-----
    MCowBQYDK2VwAyEA0G9ciHL6XZQXuWq6W4dFLvwNEPWgcdtQgEVlBIwZWBQ=
    -----END PUBLIC KEY-----
    ```
  </Accordion>

  <Accordion title="Sandbox Public Key">
    ```plaintext theme={null}
    -----BEGIN PUBLIC KEY-----
    MCowBQYDK2VwAyEALsyvX2yVnG3ZKRIFfEvYk2nkzanoNgAqBSqdeNub4sM=
    -----END PUBLIC KEY-----
    ```
  </Accordion>
</AccordionGroup>

## Code Examples

Here are sample snippets to help you implement signature verification:

<CodeGroup>
  ```python Python theme={null}
  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
  ```

  ```javascript JavaScript theme={null}
  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');
    }
  }
  ```

  ```go Go theme={null}
  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
  }
  ```

  ```java Java theme={null}
  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 PHP theme={null}
  <?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');
      }
  }
  ?>
  ```
</CodeGroup>

## Shared Secret in URL

You may include a shared secret in your webhook URL:

Examples:

```bash theme={null}
https://my.integration.io/webhooks/partoo/9fa91de19/business_update
https://my.integration.io/webhooks/partoo/business_update?key=9fa91de19
```
