API Signature

MetaKeep APIs support API signatures to ensure end-to-end security and data integrity.

API signatures are a security mechanism to prevent unauthorized access and ensure the integrity of data transmitted through an API. They protect against various security threats, including replay attacks, man-in-the-middle attacks, and data tampering.

This document explains how API signatures work and how to implement them in code.

Why Use API Signatures?

  • Prevent replay attacks: By including a timestamp in the signature that expires after a short period of time (e.g. 1 minute), requests cannot be reused and must be fresh. This mitigates replay attacks where recorded API requests could be reused later.
  • Validate requests: The signature can be used to verify that requests are from authorized sources and have not been tampered with in transit.

How Do API Signatures Work?

API signatures are generated using the API or Account secret(which you can get from the MetaKeep developer console) and by signing key information in the request:

  • Hostname: The hostname of the API server api.metakeep.xyz.
  • HTTP method: The HTTP API method being called e.g. POST, GET, PUT, DELETE etc.
  • API path: The API path e.g. /v2/app/sign/message.
  • Idempotency Key: Idempotency Key if the API supports it.
  • Timestamp: Timestamp when the signature was generated. Note that the timestamp must be within 1 minute of the server time. The request will be rejected if the timestamp is too old or new.
  • Request data: Request body data if there is any, else empty string.

Then on the server side, the signature is verified to ensure the request is valid and authorized.

šŸ“˜

Note that MetaKeep never stores your API or Account secret. It is only used to generate the signature and is never sent to the server.

Including the Signature in API Requests

To include the signature in your API requests, you will need to add 2 headers to your request:

  • X-Api-Signature: The signature generated using the API or Account secret.
  • X-Timestamp: The timestamp in milliseconds when the signature was generated.

Note that account keys use X-Account-Key header instead of X-API-Key header for sending the key.

Code Samples

ā—ļø

Security Advisory

We strongly recommend that you use the code samples provided below to generate API signatures. MetaKeep has audited and vetted the crypto libraries used in the code samples. Let us know if you need a code sample in a language not listed below :)

Here are some sample code snippets for generating API signatures:

Node JS

Here's a reference implementation of this code: https://replit.com/@tallmint/MetaKeepApiSignatureNodeJs#index.js

// run `node index.js` to run the script
// The script uses `crypto` and `elliptic` libraries to sign request and `axios`
// to make API calls. You can use any other library like `fetch`
// to make API calls.
const crypto = require("crypto");
const ec = require("elliptic").ec;
const axios = require("axios");

// Replace with your API or ACCOUNT key and secret
// Note that KEY and SECRET should never be stored in the code.
// They should be stored in a secret store or kms and loaded at runtime.
const KEY = "<Replace with your API or ACCOUNT key>";
const SECRET = "<Replace with your API or ACCOUNT secret>";

class MetaKeepAPI {
  // Original key and secret
  key = "";
  secret = "";

  // Parsed key and secret
  parsedKey = "";
  parsedSecret = "";

  // Signing key for generating API signature
  signingKey = null;

  isAccountKey = false;

  constructor(key, secret) {
    this.key = key;
    this.parsedKey = key;

    this.secret = secret;
    this.parsedSecret = secret;

    if (!this.key || !this.secret) {
      throw new Error("Key and secret are required.");
    }

    // Handle account keys
    if (this.key.startsWith("account_key_")) {
      if (!this.secret.startsWith("account_secret_")) {
        throw new Error(
          "Secret should start with account_secret_ for account keys."
        );
      }

      this.isAccountKey = true;
      this.parsedKey = this.key.substring(12); // Remove account_key_ prefix
      this.parsedSecret = this.secret.substring(15); // Remove account_secret_ prefix
    }

    this.signingKey = this.getSigningKey();
  }

  // Generate signing key for the API request.
  getSigningKey() {
    const pubKey = new ec("p256").keyFromPublic(
      Buffer.from(this.parsedKey, "base64")
    );

    // Remove padding from x and y
    const x = Buffer.from(pubKey.getPublic().getX().toBuffer()).toString(
      "base64"
    );
    const y = Buffer.from(pubKey.getPublic().getY().toBuffer()).toString(
      "base64"
    );

    return {
      key: crypto.createPrivateKey({
        key: {
          kty: "EC",
          // P-256 curve
          crv: "P-256",
          x: x,
          y: y,
          d: this.parsedSecret,
        },
        format: "jwk",
      }),
      dsaEncoding: "ieee-p1363",
    };
  }

  // Generate API signature for request.
  generateApiSignature(
    httpMethod,
    apiPath,
    idempotencyKey,
    timestampMillis,
    requestDataString
  ) {
    // Each element is separated by a newline character

    const hostElement = "api.metakeep.xyz\n";

    const methodElement = `${httpMethod}\n`;

    const pathElement = `${apiPath}\n`;

    // Idempotency key is optional
    const idempotencyElement = idempotencyKey
      ? `Idempotency-Key:${idempotencyKey}\n`
      : "";

    const timestampElement = `X-Timestamp:${timestampMillis}\n`;

    // If there is no request data, use an empty string
    const dataElement = requestDataString || "";

    return crypto
      .sign(
        "SHA256",
        crypto
          .createHash("SHA256")
          .update(
            hostElement +
              methodElement +
              pathElement +
              idempotencyElement +
              timestampElement +
              dataElement,
            "utf8"
          )
          .digest(),
        this.signingKey
      )
      .toString("base64");
  }

  // Generate API signature and call the API.
  async callApi({
    httpMethod = "POST",
    apiPath,
    idempotencyKey = null,
    requestData,
  }) {
    const timestamp = Date.now().toString();

    return await axios({
      method: httpMethod,
      url: `https://api.metakeep.xyz${apiPath}`,
      headers: {
        "Content-Type": "application/json",
        // API and Account keys use different headers
        "X-API-Key": this.isAccountKey ? null : this.key,
        "X-Account-Key": this.isAccountKey ? this.key : null,
        "X-API-Signature": this.generateApiSignature(
          httpMethod,
          apiPath,
          idempotencyKey,
          timestamp,
          JSON.stringify(requestData)
        ),
        "X-Timestamp": timestamp,
      },
      data: JSON.stringify(requestData),
    });
  }
}

// Example sign message API call
async function callSignMessage(metakeepAPI) {
  const request = {
    message: "Hello World",
    reason: "API signature Testing",
  };

  try {
    console.log("Calling sign message");

    response = await metakeepAPI.callApi({
      apiPath: "/v2/app/sign/message",
      requestData: request,
    });

    console.log("Call succeeded with response", response.data);
  } catch (error) {
    console.error("Call failed with error", error.response?.data);
  }
}

async function apiSignatureTest() {
  const metakeepAPI = new MetaKeepAPI(KEY, SECRET);

  await callSignMessage(metakeepAPI);
}

apiSignatureTest();

Python

Here's a reference implementation of this code: https://replit.com/@tallmint/MetaKeepApiSignaturePython

import base64
import hashlib
import json
import logging
import time

import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature

# Replace with your API or ACCOUNT key and secret
# Note that KEY and SECRET should never be stored in the code.
# They should be stored in a secret store or kms and loaded at runtime.
KEY = "<Replace with your API or ACCOUNT key>"
SECRET = "<Replace with your API or ACCOUNT secret>"

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("MetaKeepApiSignature")


class MetaKeepAPI:
    def __init__(self, key: str, secret: str):
        # Original key
        self.key = key
        self.parsed_key = key

        # Original secret
        self.secret = secret
        self.parsed_secret = secret

        if not self.key or not self.secret:
            raise Exception("Key and secret are required")

        # Handle account keys
        if self.key.startswith("account_key_"):
            if not self.secret.startswith("account_secret_"):
                raise Exception(
                    "Secret should start with 'account_secret_' for account keys"
                )

            self.is_account_key = True
            self.parsed_key = self.key[12:]  # Remove 'account_key_' prefix
            self.parsed_secret = self.secret[15:]  # Remove 'account_secret_' prefix
        else:
            self.is_account_key = False

        self.signing_key = ec.derive_private_key(
            int.from_bytes(base64.urlsafe_b64decode(self.parsed_secret + "=="), "big"),
            ec.SECP256R1(),
        )

    def generate_api_signature(
        self,
        http_method: str,
        api_path: str,
        idempotency_key: str,
        timestamp_millis: int,
        request_data_string: str,
    ):
        # Each element is separated by a newline character
        host_element = "api.metakeep.xyz\n"
        method_element = f"{http_method}\n"
        path_element = f"{api_path}\n"

        # Idempotency key is optional
        idempotency_element = (
            f"Idempotency-Key:{idempotency_key}\n" if idempotency_key else ""
        )
        timestamp_element = f"X-Timestamp:{timestamp_millis}\n"

        # If there is no request data, use an empty string
        data_element = request_data_string or ""

        hashed_request = hashlib.sha256(
            (
                host_element
                + method_element
                + path_element
                + idempotency_element
                + timestamp_element
                + data_element
            ).encode("utf-8")
        ).digest()

        # Generate the signature
        signature = self.signing_key.sign(hashed_request, ec.ECDSA(hashes.SHA256()))

        (r, s) = decode_dss_signature(signature)

        return base64.b64encode(
            r.to_bytes(byteorder="big", length=32)
            + s.to_bytes(byteorder="big", length=32)
        ).decode("utf-8")

    def call_api(
        self, *, http_method="POST", api_path, idempotency_key=None, request_data: dict
    ):
        request_data_string = json.dumps(request_data)
        timestamp = int(time.time() * 1000)

        response = requests.post(
            f"https://api.metakeep.xyz{api_path}",
            data=request_data_string,
            headers={
                "Content-Type": "application/json",
                #  API and Account keys use different headers
                "X-API-Key": self.key if not self.is_account_key else None,
                "X-Account-Key": self.key if self.is_account_key else None,
                "X-API-Signature": self.generate_api_signature(
                    http_method,
                    api_path,
                    idempotency_key,
                    timestamp,
                    request_data_string=request_data_string,
                ),
                "X-Timestamp": str(timestamp),
            },
        )

        response.raise_for_status()

        return response


# Example sign message API call
def call_sign_message(metakeepAPI: MetaKeepAPI):
    request = {"message": "Hello World", "reason": "API signature Testing"}

    try:
        logger.info("Calling sign message")
        response = metakeepAPI.call_api(
            api_path="/v2/app/sign/message",
            request_data=request,
        )

        logger.info("Call succeeded with response: %s", response.text)
    except Exception as e:
        logger.exception("Call failed")
        logger.error(e.response.text)

if __name__ == "__main__":
    metakeepAPI = MetaKeepAPI(KEY, SECRET)

    call_sign_message(metakeepAPI)

Ā© Copyright 2024, Passbird Research Inc.