Gateway - Procesamiento
QR Estático - Factura
17 min
mediante esta opción se puede generar de manera completamente offline un código qr estático que podrá ser impreso en una factura u enviado via email, whatsapp o el medio que fuese este qr se genera con un código de referencia que evita que el mismo pueda ser pagado múltiples veces información general paises 🇦🇷 argentina formato del qr emv qr merchant presented mode ( mpm ) librerias para generarlo php https //github com/arcticfalcon/emv qr cps c# https //github com/juanroman zz/emvqr net https //www nuget org/packages/emv qrcps/1 0 1 go https //github com/dongri/emv qrcode nodejs https //github com/steplix/emv qrcps java https //github com/mvallim/emv qrcode pasos 1\) obtener primero el punto de venta sobre el cual se generarán los qr esto puede hacerse desde la consola en "administrar pos" de aquí se debe obtener el "id" 2\) solicitar a nuestro equipo todos los datos indicados en la tabla 3\) utilizando alguna de las librerias generar el qr con la siguiente información nombre del campo campo sub campo tipo valor payload format indicator / indicador de formato 00 fijo 01 point of initiation method / metodo de iniciación 01 fijo 11 reversed domain / dominio reverso 41 sub campos 00 fijo io sugaway interoperable 01 fijo prisma 98 fijo 30598910045 99 fijo 01 merchant id / id del comercio 50 sub campos cuit 00 dinámico solicitar a soporte cuit formateado 01 dinámico campo anterior en formato xx xxxxxxxx x merchant account / cuenta del comercio 51 sub campos cvu 00 dinámico solicitar a soporte merchant category code / código de categoria del comercio 52 dinámico solicitar a soporte currency / moneda 53 fijo 032 total 54 dinámico total de la operación en formato nnnn dd donde n es la parte entera y d los decimales este total debe tener siempre 2 decimales country code / código de país 58 fijo ar merchant name / nombre del comercio 59 dinámico solicitar a soporte merchant city / ciudad del comercio 60 dinámico solicitar a soporte zip code / código postal 61 dinámico solicitar a soporte 80 01 dinámico d1=1775150280;t2=150 00;d2=1775409480; donde t2 es el total 2 d1 y d2 son los vencimientos como timestamp en milisegundos 81 01 dinámico firma del campo 80 additional data / datos adicionales 62 sub campos point of sale / punto de venta 01 dinámico dato obtenido en el punto 1 del tutorial este dato siempre va a ser el mismo en todos los qr solicitar a soporte reference / referencia 05 dinámico código único de operación, este campo permitirá que la factura o la operación no pueda ser abonada múltiples veces ejemplo código generado en el formato especificado 00020101021141490024io sugaway interoperable98113059891004599020150280009121231234011112 123123 4512600229999999999999999999999520473995913mi comercio 26004caba5802ar6108c1082abf5303032540410 5626201183t3wn9p9g22kt7uhda0536b3b3acb6 cb14 4a3f a05d 91e013d810d863041f39 generación de firma estructura el qr estático firmado utiliza los campos estándar emv qr merchant presented mode más tres campos adicionales para importes con vencimientos firmados campo nombre contenido 54 amount primer importe (t1) 80 unreserved template subcampo 01 payload firmado en base64url 81 unreserved template subcampo 01 firma ecdsa del payload en base64url formato del payload (campo 80 01) el contenido del subcampo 01 del campo 80 es un string codificado en base64url sin padding al decodificarlo se obtiene t1={monto};d1={timestamp};t2={monto};d2={timestamp}; donde t{n} es un importe con 2 decimales (ej 100 00) d{n} es el timestamp de vencimiento del importe t{n} en formato unix epoch en segundos n es un índice secuencial comenzando en 1 t1 se replica además en el campo 54 del qr (campo estándar emv amount) importante t1 se incluye tanto en el campo 54 como dentro del payload firmado esto garantiza que si alguien modifica el campo 54 del qr, la verificación de firma falla ejemplo decodificado t1=100 00;d1=1775150280;t2=150 00;d2=1775236680; esto define $100 00 válido hasta el timestamp 1775150280 $150 00 válido hasta el timestamp 1775236680 al momento del pago, el sistema selecciona el primer importe cuyo vencimiento aún no haya expirado si todos los importes están vencidos, se rechaza la operación actualmente se soportan hasta 2 importes con vencimientos codificación base64url sin padding se utiliza la variante url safe de base64 definida en rfc 4648 §5 codificar el string en base64 estándar reemplazar + por reemplazar / por eliminar los = del final (padding) para decodificar, realizar el proceso inverso reemplazar por +, por /, agregar = hasta que el largo sea múltiplo de 4, y decodificar base64 generación de la firma (campo 81 01) algoritmo ecdsa con sha 256 sobre curva p 256 (prime256v1) tamaño de la firma 64 bytes (86 caracteres en base64url) proceso construir el payload en texto plano t1={amount};d1={ts};t2={amount};d2={ts}; codificar en base64url sin padding → este valor va en el campo 80 01 firmar el valor codificado en base64url (el string del paso 2) con ecdsa sha256 usando la clave privada codificar la firma en base64url sin padding → este valor va en el campo 81 01 nota se firma el string base64url (paso 2), no el texto plano original esto es importante para que la verificación sea consistente ejemplos por lenguaje javascript (web crypto api) // 1 construir payload const payloadraw = "t1=100 00;d1=1775150280;t2=150 00;d2=1775236680;"; // 2 codificar en base64url const payloadb64 = btoa(payloadraw) replace(/\\+/g, ' ') replace(/\\//g, ' ') replace(/=+$/, ''); // 3 importar clave privada function pemtoarraybuffer(pem) { const b64 = pem replace(/ begin \[\w\s]+ /g, '') replace(/ end \[\w\s]+ /g, '') replace(/\s/g, ''); const binary = atob(b64); const bytes = new uint8array(binary length); for (let i = 0; i < binary length; i++) bytes\[i] = binary charcodeat(i); return bytes buffer; } const key = await crypto subtle importkey( 'pkcs8', pemtoarraybuffer(privatekeypem), { name 'ecdsa', namedcurve 'p 256' }, false, \['sign'] ); // 4 firmar const sig = await crypto subtle sign( { name 'ecdsa', hash 'sha 256' }, key, new textencoder() encode(payloadb64) ); // 5 codificar firma en base64url const signatureb64 = btoa(string fromcharcode( new uint8array(sig))) replace(/\\+/g, ' ') replace(/\\//g, ' ') replace(/=+$/, ''); python import base64 from cryptography hazmat primitives import hashes, serialization from cryptography hazmat primitives asymmetric import ec, utils payload raw = "t1=100 00;d1=1775150280;t2=150 00;d2=1775236680;" \# codificar en base64url payload b64 = base64 urlsafe b64encode(payload raw\ encode()) rstrip(b'=') decode() \# cargar clave privada private key = serialization load pem private key(pem bytes, password=none) \# firmar (der → raw r||s para compatibilidad con webcrypto) der sig = private key sign(payload b64 encode(), ec ecdsa(hashes sha256())) r, s = utils decode dss signature(der sig) raw sig = r to bytes(32, 'big') + s to bytes(32, 'big') \# codificar firma en base64url signature b64 = base64 urlsafe b64encode(raw sig) rstrip(b'=') decode() php $payloadraw = "t1=100 00;d1=1775150280;t2=150 00;d2=1775236680;"; // codificar en base64url $payloadb64 = rtrim(strtr(base64 encode($payloadraw), '+/', ' '), '='); // cargar clave privada $privatekey = openssl pkey get private($pemstring); // firmar openssl sign($payloadb64, $signature, $privatekey, openssl algo sha256); // codificar firma en base64url $signatureb64 = rtrim(strtr(base64 encode($signature), '+/', ' '), '='); go import ( "crypto/ecdsa" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "math/big" "strings" ) payloadraw = "t1=100 00;d1=1775150280;t2=150 00;d2=1775236680;" // codificar en base64url payloadb64 = strings trimright( base64 urlencoding encodetostring(\[]byte(payloadraw)), "=") // cargar clave privada block, = pem decode(\[]byte(pemstring)) key, = x509 parsepkcs8privatekey(block bytes) eckey = key ( ecdsa privatekey) // firmar hash = sha256 sum256(\[]byte(payloadb64)) r, s, = ecdsa sign(rand reader, eckey, hash\[ ]) // convertir a raw r||s (32 bytes cada uno para p 256) rbytes = r fillbytes(make(\[]byte, 32)) sbytes = s fillbytes(make(\[]byte, 32)) rawsig = append(rbytes, sbytes ) // codificar firma en base64url signatureb64 = strings trimright( base64 urlencoding encodetostring(rawsig), "=") c# ( net) using system security cryptography; using system text; var payloadraw = "t1=100 00;d1=1775150280;t2=150 00;d2=1775236680;"; // codificar en base64url var payloadb64 = convert tobase64string(encoding utf8 getbytes(payloadraw)) replace("+", " ") replace("/", " ") trimend('='); // cargar clave privada using var ecdsa = ecdsa create(); ecdsa importfrompem(pemstring); // firmar (ieee p1363 = raw r||s) var sig = ecdsa signdata( encoding utf8 getbytes(payloadb64), hashalgorithmname sha256, dsasignatureformat ieeep1363fixedfieldconcatenation); // codificar firma en base64url var signatureb64 = convert tobase64string(sig) replace("+", " ") replace("/", " ") trimend('='); java import java security ; import java security spec ; import java util base64; string payloadraw = "t1=100 00;d1=1775150280;t2=150 00;d2=1775236680;"; // codificar en base64url string payloadb64 = base64 geturlencoder() withoutpadding() encodetostring(payloadraw\ getbytes("utf 8")); // cargar clave privada pkcs8encodedkeyspec spec = new pkcs8encodedkeyspec( base64 getdecoder() decode(pembase64content)); keyfactory kf = keyfactory getinstance("ec"); privatekey privatekey = kf generateprivate(spec); // firmar signature signer = signature getinstance("sha256withecdsainp1363format"); signer initsign(privatekey); signer update(payloadb64 getbytes("utf 8")); byte\[] sig = signer sign(); // codificar firma en base64url string signatureb64 = base64 geturlencoder() withoutpadding() encodetostring(sig); verificación al recibir el qr, el sistema lee el campo 54 (primer importe), campo 80 01 (payload base64url) y campo 81 01 (firma base64url) obtiene la clave pública del certificado activo de la entidad para el message type qr si la entidad no tiene certificado propio, busca el del operador padre (payment facilitator) verifica la firma ecdsa sha256 del payload usando la clave pública decodifica el payload base64url para obtener los importes y vencimientos valida que t1 del payload coincida con el campo 54 del qr selecciona el monto correspondiente según el vencimiento vigente si todos los importes están vencidos, rechaza la operación ejemplo completo de qr campo 00 01 (payload format indicator) campo 01 11 (static qr) campo 41 \[anchor point con datos del administrador] campo 50 \[cuit del comercio] campo 51 \[cvu/cbu] campo 52 7399 (mcc) campo 53 032 (ars) campo 54 100 00 (primer importe) campo 58 ar campo 59 mi comercio campo 60 buenos aires campo 61 c1082 campo 62 \[pos id + referencia única] campo 80 01{len}{payload base64url} (signed payload) campo 81 01{len}{signature base64url} (signature ecdsa) campo 63 \[crc] certificados los certificados se gestionan desde la consola en la sección firmas para qr se utiliza el algoritmo ecdsa con curva p 256 (prime256v1) al crear un certificado se genera un par de claves (pública y privada) ecdsa p 256 la clave pública se almacena en el servidor para verificación la clave privada se entrega al usuario una única vez para que firme los qr desde su dispositivo la clave privada es un archivo pem que debe guardarse de forma segura para submerchants operados por un payment facilitator si el submerchant no tiene certificado propio, el sistema utiliza automáticamente el certificado del operador padre restricciones restricción valor algoritmo ecdsa sha 256 (único permitido para qr) curva p 256 / prime256v1 (única permitida para qr) codificación base64url sin padding (rfc 4648 §5) formato de firma ieee p1363 (raw r‖s, 64 bytes) largo campo 80 01 ≤ 95 caracteres (payload base64url) largo campo 81 01 86 caracteres (firma 64 bytes en base64url) largo firma 86 caracteres base64url (64 bytes raw) máximo importes 2 (t1/d1 + t2/d2)