Firmar Mensajes

Cómo firmar mensajes shinkansen con JWS

Firmar Mensajes

La mayoría de los mensajes que son enviados por un Participante hacia Shinkansen deben ser firmados usando JWS (JSON Web Signature). Para que el API sea legible, Shinkansen usa detached signatures, lo que significa que el contenido a firmar es el cuerpo HTTP de los mensajes que se envían en Shinkansen 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 require 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 según lo descrito en el modelo de seguridad.

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 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í:

-----BEGIN CERTIFICATE-----
MIIGAjCCBOqgAwIBAgIIREUj+lcjuVcwDQYJKoZIhvcNAQELBQAwgbcxHjAcBgkq
hkiG9w0BCQEWD3NvcG9ydGVAaWRvay5jbDEfMB0GA1UEAwwWSURPSyBGSVJNQSBF
TEVDVFJPTklDQTEXMBUGA1UECwwOUlVULTc2NjEwNzE4LTQxIDAeBgNVBAsMF0F1
dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMRkwFwYDVQQKDBBCUE8gQWR2aXNvcnMgU3BB
MREwDwYDVQQHDAhTYW50aWFnbzELMAkGA1UEBhMCQ0wwHhcNMjAwODEyMjIwNjIx
WhcNMjMwNTI4MTMwNzMwWjB6MSYwJAYDVQQDDB1MRU9OQVJETyBIVU1CRVJUTyBT
T1RPIE1Vw5FPWjEhMB8GCSqGSIb3DQEJARYSbGVvLnNvdG9AZ21haWwuY29tMRMw
EQYDVQQFEwoxNTYyMDMxOC0xMQswCQYDVQQGEwJDTDELMAkGA1UEBwwCUk0wggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDoRiVZ2Ya1JpcaIDJi3xLH8g0v
MF6XzvQONdTYZRnPXui3fk3+LM2YZ9w9xdck38H0i9Qp/aRAKvxmgPIGQuAWZLJy
9HujCCKH1EY8HWEcWCHPu2zy4puJV3jBB/mhkHvKbiBsriCVOFvHCQlcJOytQyOb
AtGbl2dMNzb2w5cavRnPkNaWQGo3BLY1gsoXTsKBGF2rDPmOPipEGcz9QHntz8qP
JLrYD2GMXZIwtjGzHP1+K2eP7NHyoTTApMOaDBkqfRmyXJ84goc6jyHCSuLyQJsl
2+A6nxctyENA9Hh4EAKLUU1E81rz0ovqrAxdYAi8Gg8MnF9cwI8MaLlEG1C7AgMB
AAGjggJMMIICSDAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFPBsM7+sl5NYeqHgzp7s
6N77ZT76MIGYBgNVHSAEgZAwgY0wgYoGCisGAQQBg4weAQQwfDAsBggrBgEFBQcC
ARYgaHR0cHM6Ly9wc2MuaWRvay5jbC9vcGVuL2Nwcy5wZGYwTAYIKwYBBQUHAgIw
QB4+AEMAZQByAHQAaQBmAGkAYwBhAGQAbwAgAHAAYQByAGEAIAB1AHMAbwAgAFQA
cgBpAGIAdQB0AGEAcgBpAG8wggEHBgNVHR8Egf8wgfwwgfmgN6A1hjNodHRwczov
L3BzYy5pZG9rLmNsL29wZW4vSURPSyBGSVJNQSBFTEVDVFJPTklDQS5jcmyigb2k
gbowgbcxHjAcBgkqhkiG9w0BCQEWD3NvcG9ydGVAaWRvay5jbDEfMB0GA1UEAwwW
SURPSyBGSVJNQSBFTEVDVFJPTklDQTEXMBUGA1UECwwOUlVULTc2NjEwNzE4LTQx
IDAeBgNVBAsMF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMRkwFwYDVQQKDBBCUE8g
QWR2aXNvcnMgU3BBMREwDwYDVQQHDAhTYW50aWFnbzELMAkGA1UEBhMCQ0wwHQYD
VR0OBBYEFFh0yhCCXYJ7OUn2dAPaZUOjfT0iMAsGA1UdDwQEAwIEkDAjBgNVHRIE
HDAaoBgGCCsGAQQBwQECoAwWCjc2NjEwNzE4LTQwIwYDVR0RBBwwGqAYBggrBgEE
AcEBAaAMFgoxNTYyMDMxOC0xMA0GCSqGSIb3DQEBCwUAA4IBAQBxPD7ETW2rK/zH
WEzMM0HKirpqU4Hf8GIErjHHHXukeUjPidNjnWq7aGGAW5sLyYPDhuFSEF/YBSau
9m6fuerwisFFd9n1hjjAQPmKb0MltajPXH9vdIXk4+A7g1W5Stbq1Ezt8E32+Zv9
17l36d33P8wwjkkPkmW14LSm9GwLvwULNkZrocgb4o8oIIzhqQSs/f+qo9rezs6S
WMmbbubDDdOm+HRBao2yjbKilFcMiIA35AJPVUZdSUc/6MzyoA0n0KePXnFQ93HR
S1BRayUnRU+8sNvKVrI7gaSVckYP1xO+747mDzyAYHYAAkE9LI1uA3VwRj5lWuEq
i4eytLag
-----END CERTIFICATE-----

Eso ya está base64-encodeado, pero con saltos de línea cada 64 caracteres y con delimitadores. Este mismo certificado en una cabecera JWS se ve así:

x5c: [
  "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",
];

Otros parámetros JWS

Para mayor eficiencia (y claridad), evitamos encodear en base64 el mensaje JSON a firmar. Esto implica usar la extensión b64 de JWS, que es parte del RFC 7797.

👍

En la práctica:

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.

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={
    'Shinkansen-JWS-Signature': jws_signature_header,
    'Shinkansen-API-Key': os.environ['SHINKANSEN_API_KEY'])
  }
)
from shinkansen import jws
import requests
import json

# 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 = jws.private_key_from_pem_file("/path/to/privatekey.pem",
  password=os.environ['PRIVATE_KEY_PASSWORD'])
cert = jws.certificate_from_pem_file("/path/to/certificate.pem")

# Get the JWS
jws_signature_header = jws.sign(payload, key, cert)

requests.post(".../messages/move", data=payload,
  headers={
    'Shinkansen-JWS-Signature': jws_signature_header,
    'Shinkansen-API-Key': '...'
  }
)

Pruebas

Para probar, te recomendamos usar nuestra librería de referencia en Python. Para eso te la puedes bajar via:

pip install python-shinkansen

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):

openssl req -x509 \
            -newkey rsa:2048 -keyout test-key.pem  \
            -out test-cert.pem -sha256 -days 3650

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:

echo '{"hola": "mundo"}' > payload.txt
python3 -m shinkansen.jws \
  sign test-cert.pem \
       --key test-key.pem \
       --payload payload.txt \
       --output jws.txt \
       --password TU-PASSWORD-ACA

Eso va a generar un archivo jws.txt.

Ahora verifiquemos la firma:

python3 -m shinkansen.jws \
  verify test-cert.pem \
         --jws jws.txt \
         --payload payload.txt

Si no te aparece ningún error, entonces significa que la firma está correcta. Veamos que pasa si modificamos el contenido:

echo '{"hello": "world"}' > otro-payload.txt
python3 -m shinkansen.jws \
  verify test-cert.pem \
         --jws jws.txt \
         --payload otro-payload.txt

Debieras observar un error que termina con:

jwcrypto.jws.InvalidJWSSignature: Verification failed for all signatures["Failed: [InvalidJWSSignature('Verification failed')]"]

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.

📘

Si tienes dudas

¡Pídenos ayuda acá o en el slack compartido (si ya eres cliente). Queremos que integrar Shinkansen sea algo fácil y probablemente lo más enredado sea esto de las firmas. Como es importante para mantener todo seguro no pudimos dejar todo sólo con API Keys. Es probable que vayamos generando SDKs en algunos lenguajes mas populares. Así que tu solicitud de ayuda funciona también como un voto para el SDK en tu lenguaje favorito.


Did this page help you?