🔐 Autenticación

¿Cómo autenticarte en la API de Skipo?

1) API Key

Una API Key es requerida para todas las requests a la API de Skipo con el propósito de identificar al usuario que realiza la petición. Esta debe especificarse en un header HTTP llamado X-API-KEY.

Para crear la API Key debes iniciar sesión en nuestra aplicación web 👉 app.skipo.com

Una vez dentro debes ir a Configuración → Desarrollador → Generar API key. Lo que despliegará la siguiente ventana:

Modal creación API Key

Modal creación API Key

Acá deberás seleccionar un nombre, fecha de expiración y permisos para la API Key. En caso de que elijas sólo el permiso de Ver se te pedirá el código de doble factor (2FA) y obtendrás tu API Key.

Por otro lado, si decides asignar otros permisos, entonces se te pedirá un paso previo para la generación y entrega de un PUBLIC-CERTIFICATE (ver punto 2).

Modal para entrega del PUBLIC-CERTIFICATE

Modal para entrega del PUBLIC-CERTIFICATE


2) Generación de llave privada y certificado público

2.1) Llave privada

Primero debemos generar una llave privada con el algoritmo RSA, para lo cual basta con ejecutar el siguiente comando en la terminal:

openssl genrsa -out skipo_private.key 2048

Esto genera un archivo skipo_private.key el cual contiene la llave privada generada. Posteriormente deberás utilizarla para firmar las request que realizarás a la API (más detalles en el punto 3).

⚠️

Seguridad

Te pedimos que tanto la API Key como la llave privada (sobre todo está última) las resguardes en un lugar seguro y no las compartas con nadie, ni siquiera con nosotros. Asimismo, en caso de pérdida de la llave privada el equipo de Skipo no podrá recuperarla y se deberá repetir el proceso de generación de llave.

2.2) Certificado público

Luego, debemos generar un PUBLIC-CERTIFICATE(certificado público) a partir de la llave privada creada anteriormente. Para esto se debe ejecutar el siguiente comando en la terminal:

openssl req -x509 -new -key skipo_private.key -out skipo_public.crt -sha256

ℹ️

Sobre certificados RSA

Al correr el comando la terminal te pedirá ciertos datos como el código del país, el nombre de la organización, entre otros. Esto es propio de los certificados RSA y es utilizado para identificar el origen del mismo. Simplemente completa con la información requerida para la creación del certificado.

Esta instrucción genera un archivo skipo_public.crt el cual contiene el PUBLIC-CERTIFICATE.

2.3) Envío de certificado y entrega del API Key

Una vez creado el archivo skipo_public.crt te pedimos que nos lo proporciones para hacerte entrega de la API Key. Descuida, este certificado es público por lo que no es información sensible a diferencia de la llave privada skipo_private.key la cual debes resguardar y bajo ninguna circunstancia compartir.


3) Firma de mensajes

La firma de mensajes se realiza combinando el algoritmo de hashing SHA256 con la encriptación RSA utilizando la llave privada (SHA256withRSA). Para esto, los lenguajes de programación facilitan bastante la implementación al proveer librerías para obtener la firma deseada.

⚙️

Librerías criptográficas

Algunos ejemplos de librerías criptográficas son las siguientes: crypto (NodeJS) - cryptography o rsa (Python) - Crypto++ (C++) - openssl (Ruby)

Preparación de string a firmar:

El contenido del string a firmar dependerá del endpoint que se invoque y se define de la siguiente manera:

{GET|POST|PUT|DELETE} {path} {sorted_alphabetically_body}

Donde:

  • {GET|POST|PUT|DELETE}: Corresponde el método HTTP a ejecutar.

  • path: Corresponde a la ruta de la request incluyendo los query params pero excluyendo el host.

       Ej: `/v1/ledger_movements/BTC?startDate=2024-01-01&endDate=2024-04-01`
    
  • sorted_alphabetically_body: Corresponde al body de la request con sus atributos ordenados alfabéticamente. En el caso de los métodos GET este valor es {}.

Headers

Así como la API Key se incluye en el header X-API-KEY, la firma generada con SHA256withRSA sobre el string especificado arriba se incluye en un header llamado X-SIGNATURE. La firma en este caso debe ir codificada en Base64.

Recuerda que para métodos GET (sólo lectura) no es necesario especificar la firma del header X-SIGNATURE, por lo que sólo bastara con el envío del header X-API-KEY para la ejecución del método.

Código

A continuación se muestra un ejemplo completo de la obtención del string para el mensaje, la firma con la llave privada y la request a la API.

import axios from "axios";
import { sign } from "crypto";
import { readFileSync } from "fs";
import { resolve } from "path";

const PATH_TO_PRIVATE_KEY = "PATH_TO_PRIVATE_KEY";
const YOUR_API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.skipo.com";

// Interface for request body
interface RequestBody {
  [key: string]: any;
}

class SkipoApiClient {
  private readonly baseUrl: string;
  private readonly secret: string;

  constructor(private readonly apiKey: string) {
    this.baseUrl = BASE_URL;
    // Replace "PATH_TO_PRIVATE_KEY" with the path to your private key file
    // or you can store the private key in an environment variable
    this.secret = readFileSync(resolve(PATH_TO_PRIVATE_KEY), "utf8");
  }

  // Function to sort object keys alphabetically
  static sortObjectKeys(obj: RequestBody): RequestBody {
    return Object.keys(obj)
      .sort()
      .reduce((result: RequestBody, key: string) => {
        result[key] = obj[key];
        return result;
      }, {});
  }

  async request(
    method: "GET" | "POST" | "PUT" | "DELETE",
    path: string,
    body: RequestBody = {}
  ) {
    try {
      const url = `${this.baseUrl}${path}`;
      const headers: Record<string, string> = { "X-API-KEY": this.apiKey };
      if (method !== "GET") {
        const message = `${method} ${path} ${JSON.stringify(SkipoApiClient.sortObjectKeys(body))}`;
        const signature = sign("sha256", Buffer.from(message), this.secret).toString("base64");
        headers["X-SIGNATURE"] = signature;
      }
      const response = await axios.request({
        method,
        url,
        data: body,
        headers,
      });
      return response.data;
    } catch (error: any) {
      console.error("Error:", error.message);
      // Handle error with custom logic...
    }
  }
}

// Create an instance of the SkipoApiClient class and make a request
const skipoApiClient = new SkipoApiClient(YOUR_API_KEY);
skipoApiClient.request(
  "GET",
  "/v1/ledger_movements/CLP?startDate=2024-01-01&endDate=2024-04-01"
).then((response) => console.log("Response:", response));
const axios = require("axios");
const { sign } = require("crypto");
const { readFileSync } = require("fs");
const { resolve } = require("path");

const PATH_TO_PRIVATE_KEY = "PATH_TO_PRIVATE_KEY";
const YOUR_API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.skipo.com";

class SkipoApiClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = BASE_URL;
    this.secret = readFileSync(resolve(PATH_TO_PRIVATE_KEY), "utf8");
  }

  static sortObjectKeys(obj) {
    return Object.keys(obj)
      .sort()
      .reduce((result, key) => {
        result[key] = obj[key];
        return result;
      }, {});
  }

  async function request(method, path, body = {}) {
    try {
      const url = this.baseUrl + path;
      const headers = { "X-API-KEY": this.apiKey };

      if (method !== "GET") {
        const message = `${method} ${path} ${JSON.stringify(sortObjectKeys(body))}`;
        const signature = sign("sha256", Buffer.from(message), this.secret).toString("base64");
        headers["X-SIGNATURE"] = signature;
      }

      const response = await axios({
        method: method,
        url: url,
        data: body,
        headers: headers
      });

      return response.data;
    } catch (error) {
      console.error("Error:", error.message);
      // Handle error with custom logic...
    }
  }
}

const skipoApiClient = new SkipoApiClient(YOUR_API_KEY);
skipoApiClient.request(
  "GET",
  "/v1/ledger_movements/CLP?startDate=2024-01-01&endDate=2024-04-01"
).then((response) => console.log("Response:", response));

import json
from base64 import b64encode
from pathlib import Path

# This needs a requirements.txt for requests and cryptography libraries
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key

PRIVATEKEY_PATH = "PATH_TO_PRIVATE_KEY"
YOUR_API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.skipo.com"


class SkipoApiClient:
    def __init__(self, api_key):
        self.base_url = BASE_URL
        self.api_key = api_key
        self.secret = self.load_private_key(PRIVATEKEY_PATH)

    def load_private_key(self, path):
        with open(Path(path), 'rb') as key_file:
            private_key = load_pem_private_key(
                key_file.read(),
                password=None,
            )
        return private_key

    @staticmethod
    def sort_object_keys(obj):
        return {key: obj[key] for key in sorted(obj)}

    def sign_message(self, message):
        signature = self.secret.sign(
            message.encode(),
            padding.PKCS1v15(),  # Use PKCS#1 v1.5 padding for RSA
            hashes.SHA256()
        )
        # Base64 encode and convert to string
        return b64encode(signature).decode('utf-8')

    def request(self, method, path, body=None):
        if body is None:
            body = {}
        sorted_body = json.dumps(
            self.sort_object_keys(body), separators=(',', ':'))
        message = f"{method} {path} {sorted_body}"
        signature = self.sign_message(message)
        headers = {
            "X-API-KEY": self.api_key,
            "X-SIGNATURE": signature
        }
        response = requests.request(
            method=method,
            url=f"{self.base_url}{path}",
            headers=headers,
            json=body
        )
        try:
            response.raise_for_status()
            return response.json()
        except requests.RequestException as error:
            print(f"Error: {error}")
            return None


# Create an instance of the SkipoApiClient class and make a request
if __name__ == "__main__":
    skipo_api_client = SkipoApiClient(YOUR_API_KEY)
    response = skipo_api_client.request(
        "GET",
        "/v1/ledger_movements/CLP?startDate=2024-01-01&endDate=2024-04-01"
    )
    print(json.dumps(response, indent=2))

Recuerda que esto es sólo un ejemplo de implementación, siéntete libre de modificarlo de la forma que te parezca y se adecue más a tus necesidades.