La mayoría de los mensajes que son enviados deben ser firmados usando JWS (JSON Web Signature). Para que el API sea legible, usamos detached signatures, lo que significa que el contenido a firmar es el cuerpo HTTP de los mensajes que se envían mediante peticiones POST. La firma se envía en una cabecera HTTP Shinkansen-JWS-Signature.
Algoritmo
Para firmar mensajes, usamos el algoritmo PS256 de JWA, que usa RSA (ampliamente usado y con soporte en variedad de plataformas y arquitecturas) en una versión moderna y más segura que lo ofrecido por el algoritmo RS256 de JWA.
👍 En la práctica:
En la mayoría de las librerías JWS deberás pasar el valor "PS256" en el parámetro/cabecera "alg" a la hora de generar la firma.
Certificado(s)
El uso de RSA requiere que cada Participante tenga una llave privada para firmar los mensajes y sus contrapartes tengan las llaves públicas correspondientes. Para facilitar el manejo seguro de estas llaves públicas, las asociamos a un certificado obtenido en un PSC o CA.
Para firmar un mensaje deberás usar la llave privada y enviar el certificado con la llave púbica.
👍 En la práctica:
Deberás incluir el certificado DER encodeado en base64 en el parámetro/cabecera "x5c" de JWS a la hora de generar la firma.
Esto es muy cercano al encoding PEM de certificados que quizás has visto. Son mas o menos así:
En la mayoría de las librerías JWS deberás pasar el valor false en el parámetro "b64" a la hora de generar la firma. Es posible que también debas pasar explícitamente el parámetro "crit" con valor ["b64"].
Generar la firma
Si la librería que usas no tiene soporte nativo para generar detached signatures, deberás generar una representación JSON del JWS (con los campos protected, payload, y signature). Luego deberás concatenar el valor de protected con ".." y luego signature (el payload no se incluye).
Ejemplos
En la práctica esto es bastante mecánico usando librerías existentes. Puedes ver el ejemplo mas abajo en Python como referencia.
O aún mejor, puede resultar en muy pocas líneas usando librerías/wrappers que hemos creado, como el ejemplo Python que usa python-shinkansen más abajo.
{/* prettier-ignore-start */}
Pruebas
Para probar, te recomendamos usar nuestra librería de referencia en Python. Para eso te la puedes bajar via:
Las pruebas requieren que cuentes con un certificado y una llave privada. Si no tienes aún tu certificado real o de pruebas, puedes generar archivos para pruebas locales de esta forma (asumiendo que tienes openssl instalado):
Te pedirá una password para proteger el archivo de la llave privada. Aunque sea un certificado para jugar, te recomendamos protegerlo con una passephrase para nunca perder la costumbre de manejar estas cosas con mucha precaución.
También te pedirá indicar información para el certificado. No lo dejes en blanco, o fallará la creación del certificado.
Después de este proceso, tendrás dos archivos generados:
test-key.pem: Llave privada RSA de pruebas
test-cert.pem: Certificado auto-firmado para probar
Ahora podemos usar la librería de referencia (recuerda tenerla instalada via pip install python-shinkansen) para hacer algunas pruebas simples:
Eso va a generar un archivo jws.txt.
Ahora verifiquemos la firma:
Si no te aparece ningún error, entonces significa que la firma está correcta. Veamos que pasa si modificamos el contenido:
Debieras observar un error que termina con:
Si pruebas con otras permutaciones (ej: cambiando el certificado usado para generar el JWS y el usado para validarlo, o modificando jws.txt) también obtendrás errores.
📘 Las firmas con PS256 son probabilístcas
Eso significa que si vuelves a firmar el mismo payload.txt con la misma llave y certificado... ¡obtendrás una firma distinta! Por ende no puedes comparar dos firmas para ver si todo está ok. Lo que se debe hacer es verificar la firma.
Ahora que cuentas con estas herramientas puedes :
Generar tus propias firmas JWS y chequear si se verifican correctamente con python3 -m shinkansen.jws verify ...
Generar una firma con python3 -m shinkansen.jws sign ... y chequear si la puedes verificar correctamente con tu código.
👍 Todo esto sólo es necesario si debes o quieres escribir tu propio código
¿Quizás nuestras librerías no soportan las tecnologías que estás usando? Avísanos y trataremos de escribir una que te sirva.
from shinkansen import jws
from shinkansen.payouts import (
PayoutMessage,
PayoutMessageHeader,
PayoutTransaction,
SHINKANSEN,
CLP,
FinancialInstitution
)
from shinkansen.payouts import PayoutMessage
# Load RSA key and certificate from file system, password from env var.
private_key = jws.private_key_from_pem_file("/path/to/privatekey.pem",
password=os.getenv('PRIVATE_KEY_PASSWORD'))
public_cert = jws.certificate_from_pem_file("/path/to/certificate.pem")
# You can also use jws.private_key_from_pem_bytes() and
# jws.certificate_from_pem_bytes() if you prefer to load everything from env
# vars or somewhere else.
# Have an API Key:
api_key = os.getenv('SHINKANSEN_API_KEY')
# Build a message to sign...
shinkansen_message = PayoutMessage(
header=PayoutMessageHeader(
sender=FinancialInstitution("<MY-ID-AS-SENDER>")
receiver=SHINKANSEN
#...
),
transactions=[
PayoutTransaction(
currency=CLP,
amount="1000",
# ...
)
]
)
# Sign and send the message:
signature, response = shinkansen_message.sign_and_send(
private_key, public_cert, api_key,
# base_url="https://dev.shinkansen.finance/v1" # to use dev environment
)
# That's it. You can save the signature if you want. And read the
# response (instance of `PayoutHttpResponse`) containing `http_status_code`,
# `transaction_ids` (a dict mapping your ids to shinkansen ids) and `errors`
# (a list of `PayoutHttpResponseError` containing `error_code` and
# `error_message`)
import * as fs from 'fs';
import axios from 'axios';
import * as jose from 'jose'
import { X509Certificate } from 'crypto';
// The message to sign...
const shinkansen_message_body = JSON.parse("{"document":{ "header" : {" +
"# ... (the rest of the message body which needs to be signed)}}}")
// ...serialized as a JSON-encoded string. Since we are signing this string,
// you MUST send this exact string payload on the HTTP request
// (if you pass the original shinkansen_message_body dict it may be re-encoded
// on a slightly different way and that will make the signature invalid.
const payload = JSON.stringify(shinkansen_message_body)
// Load RSA key and certificate from file system,
const certificate = (new X509Certificate(fs.readFileSync('./cert.pem')))
const ALGORITHM = 'PS256'
const privateKey = await jose.importPKCS8(fs.readFileSync('./key.pem').toString(), ALGORITHM)
// x5c header, must go in base64 DER format:
const der_certificate = certificate.toString().replace(/[\r\n]/gm, '').slice(27, -25)
// Build the JWS with our payload using the header required by Shinkansen and PS256 algorithm
const jws = (await new jose.FlattenedSign(new Uint8Array(
Buffer.from(payload, 'utf-8')))).setProtectedHeader(
{
alg: ALGORITHM, // PS256 algorithm
b64: false, // The b64 = false header
crit: ['b64'], // Required for b64 header
x5c: [der_certificate] // The x509 header
}
)
// Sign with the private key
const signature = await jws.sign(privateKey)
// Extract only the protected header and signature, to build the compact
// detached representation
const jws_header = signature.protected + '..' + signature.signature
// Build the request and send it.
const shinkansen_api = axios.create(
{
baseURL: 'https://dev.shinkansen.finance/v1'
headers:{'shinkansen-api-key': 'apikey',
"content-type": "application/json"
}
}
)
try{
const response = await shinkansen_api.post(
'/messages/payouts',
payload,
{
headers:
{
'shinkansen-jws-signature': jws_header
}
}
)
// ...
}
catch(e){
// ...
}
using Jose;
using System.Linq;
using System.Text;
using System.Collections;
using Newtonsoft.Json.Linq;
using System.Net.Http.Headers;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography.X509Certificates;
// HttpClient is intended to be instantiated once per application, rather than per-use.
HttpClient client = new();
// The message to sign...You will probably put this on an Object/Class that is Serializable
// to Json or StringContent
string shinkansen_message_body = """
{"document": {"header" : {
# ... (the rest of the message body which needs to be signed)
}}}
""";
// In order to send the Json String, we ned a StringContent payload (JSON-encoded string)
// Since we are signing this string, you MUST send this exact payload
// on the HTTP request if you pass the original shinkansen_message_body
// it may be re-encoded on a slightly different way and that will make the signature invalid.
StringContent payload = new (shinkansen_message_body, Encoding.UTF8, "application/json");
// Load RSA key and certificate from file system, password from env var
// If your key is not password protected you need to use CreateFromPemFile() instead
var certificatePair = X509Certificate2.CreateFromEncryptedPemFile(
"cert.pem", Environment.GetEnvironmentVariable("PRIVATE_KEY_PASSWORD"), "key.pem");
// x5c header, must go in base64 DER format:
// GetRawCertData return byte representation of the cert (DER)
// Base64FormattingOptions.None return the base64 representation without space
var certificate_der_b64 = Convert.ToBase64String(
certificatePair.GetRawCertData(), Base64FormattingOptions.None);
// Build the JWS with our payload and sign with the private key using the header
// required by Shinkansen and PS256 algorithm
var jws_signature_header = JWT.Encode(
payload.ReadAsStringAsync().Result, certificatePair.GetRSAPrivateKey(),
JwsAlgorithm.PS256, options: new JwtOptions
{
DetachPayload = true, // As requested by Shinkansen
EncodePayload = false, // The b64 = false header
// "crit": ["b64"] is provided automatically by Jose.JWT
}, extraHeaders: new Dictionary<string, object>
{
{"x5c", new List<string>() { certificate_der_b64} } // The x509 header
});
// Build the request
client.BaseAddress = new Uri("https://dev.shinkansen.finance/v1"); // Base URL for DEV env
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json")); // Set Accept header
var request = new HttpRequestMessage
{
RequestUri = new("/messages/payouts", UriKind.Relative), // Payouts endpoint
Method = HttpMethod.Post,
Headers = {
{"Shinkansen-Api-Key", Environment.GetEnvironmentVariable("SHINKANSEN_API_KEY")},
{"Shinkansen-JWS-Signature" , jws_signature_header}, //The JWS Signature
},
Content = payload
};
// Call asynchronous network methods in a try/catch block to handle exceptions.
// And we are done!
try
{
HttpResponseMessage response = client.SendAsync(request).Result;
//...
}
catch {/*...*/}
from jwcrypto.jwk import JWK
from jwcrypto.jws import JWS
from base64 import b64encode, b64decode
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
import requests
import json
# A helper function
def file_content(path):
with open(path, "rb") as f:
return f.read()
# The message to sign...
shinkansen_message_body = {"document": {"header" : {
# ... (the rest of the message body which needs to be signed)
}}}
# ...serialized as a JSON-encoded string. Since we are signing this string, you
# MUST send this exact string payload on the HTTP request (if you pass the
# original shinkansen_message_body dict it may be re-encoded on a slightly
# different way and that will make the signature invalid.
payload = json.dumps(shinkansen_message_body)
# Load RSA key and certificate from file system, password from env var
key = serialization.load_pem_private_key(
file_content("/path/to/privatekey.pem"),
password=os.environ['PRIVATE_KEY_PASSWORD']
)
certificate = x509.load_pem_x509_certificate(
file_content("/path/to/certificate.pem"),
)
# Compute x5c header, must go in base64 DER format:
certificate_der_b64 = b64encode(
certificate.public_bytes(encoding=serialization.Encoding.DER)
).decode('ascii')
# Build the JWK with our key for signing
jwk = JWK()
jwk.import_from_pyca(key)
# Build the JWS
jws = JWS(payload) # with our payload
jws.add_signature( # and sign, using the header required by Shinkansen:
jwk,
protected={
"alg": "PS256", # PS256 algorithm
"b64": False, # The b64 = false header
"crit": ["b64"], # Required for b64 header
"x5c": [certificate_der_b64], # The x509 header
},
)
# Now we get the JWS in JSON format...
json_jws = jws.serialize()
# ...parse it
parsed_jws = json.loads(json_jws)
# ...and extract only the protected header and signature, to build the compact
# detached representation
jws_signature_header = f"{parsed_jws['protected']}..{parsed_jws['signature']}"
# And we are done!
requests.post(".../messages/move", data=payload,
headers={
'Content-Type': 'application/json',
'Shinkansen-JWS-Signature': jws_signature_header,
'Shinkansen-API-Key': os.environ['SHINKANSEN_API_KEY'])
}
)