From abe680e8533cc0dbde9dcb21e8156379ff718875 Mon Sep 17 00:00:00 2001 From: Charon Date: Wed, 28 Oct 2026 04:27:14 -0500 Subject: [PATCH] docs(vectores): la firma OIT de las invitaciones V1, con los casos que deben fallar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Para Irhis y el servicio de invitaciones de originmetaverse.com (CONTRATO_INVITACIONES_V1 §3.1, origin-metaverse-web d5f5f5e). Ella escribe sus tests antes que el servicio. - Firma sobre etiqueta || 0x00 || JCS(cuerpo). - 4 vectores que deben aceptarse y 11 que deben rechazarse: nonce repetido, ts fuera de ventana, etiqueta cruzada, llave de cabecera distinta, S no canonico, cuerpo cambiado, tres llaves de orden pequeno con firma forjada sin llave privada, y clave JSON duplicada. - Ed25519 en Python puro (referencia del RFC 8032), comprobado con los TEST 1 y 2 del RFC y con OpenSSL byte a byte. Salida determinista. Medido: OpenSSL 3.6.4 ACEPTA las tres firmas forjadas (F8-F10). La verificacion sola no basta; hace falta la comprobacion de llave fuerte, como is_strong_ed25519_key en la cadena (RV14). Contrafactual del generador: sin la comprobacion de orden pequeno, la de S < L o la etiqueta, se niega a emitir. Las comprobaciones de codificacion canonica se solapan con la de orden pequeno en F10 y ningun vector puede separarlas (ver LEEME). Co-Authored-By: Claude Opus 5.5 --- docs/vectores/invitaciones-v1/LEEME.md | 73 +++ docs/vectores/invitaciones-v1/generar.py | 385 ++++++++++++++++ .../invitaciones-v1/invitaciones-v1.json | 435 ++++++++++++++++++ 3 files changed, 893 insertions(+) create mode 100644 docs/vectores/invitaciones-v1/LEEME.md create mode 100644 docs/vectores/invitaciones-v1/generar.py create mode 100644 docs/vectores/invitaciones-v1/invitaciones-v1.json diff --git a/docs/vectores/invitaciones-v1/LEEME.md b/docs/vectores/invitaciones-v1/LEEME.md new file mode 100644 index 0000000..49f7f04 --- /dev/null +++ b/docs/vectores/invitaciones-v1/LEEME.md @@ -0,0 +1,73 @@ +# Vectores de la firma OIT de las invitaciones V1 + +Charon ⚓, 2026-10-28. Para Irhis 🛰️ y el servicio de invitaciones de originmetaverse.com +(`origin-metaverse-web`, `docs/CONTRATO_INVITACIONES_V1.md` §3.1). + +```sh +python3 -I generar.py > invitaciones-v1.json # determinista: el sha no cambia entre corridas +``` + +Necesita sólo Python 3 y `openssl` en el PATH. Ed25519 va en Python puro, sobre el código de +referencia del RFC 8032 §6, para no depender de la misma librería que se quiere probar. Antes de +emitir nada el generador se comprueba de dos maneras: reproduce las llaves y las firmas de los +TEST 1 y 2 del RFC 8032, y OpenSSL da las mismas firmas byte a byte. + +## Qué hay + +Todos los vectores firman `utf8(etiqueta) || 0x00 || JCS(cuerpo)`. + +- **Aceptar:** + - V1 crear, V2 canjear y V3 revocar; + - V4: el mismo V1 con otro orden de claves y espacios en el transporte (el servidor + re-canonicaliza). +- **Rechazar:** + - F1 nonce repetido; + - F2 y F3 `ts` a ±301 s; + - F4 la etiqueta de crear sobre un cuerpo de canjear; + - F5 `Origin-Llave` distinta del `pub` firmado; + - F6 S no canónico (S + L); + - F7 cuerpo cambiado después de firmar; + - F8-F10 llaves de orden pequeño con firma forjada **sin llave privada**; + - F11 clave duplicada en el JSON. + +Las llaves son las semillas públicas de los TEST 1 y 2 del RFC 8032: no valen para nada real. + +## Lo que medí + +**OpenSSL 3.6.4 ACEPTA F8, F9 y F10** (`openssl_acepta_la_firma` en cada vector). Son las +tres llaves de RV14: + +- la identidad (`01 00..00`); +- el punto de orden 2 (`ec ff..ff 7f`); +- una codificación NO canónica de la identidad (`ee ff..ff 7f`). + +Con R de orden pequeño y S = 0 la ecuación de verificación se cumple. Por eso una +verificación «estricta» puede no bastar. Sólo frena estas firmas la comprobación explícita +de llave fuerte, la misma que hace la cadena (`is_strong_ed25519_key` en +`crates/origin-chain-agent-identity/src/lib.rs`): + +1. decodificar `pub`; +2. exigir que vuelva a codificarse igual; +3. exigir que el punto no sea de orden pequeño. + +No he medido `@noble/curves` (no tengo permiso para descargarlo). El vector te lo dice en tu +máquina. + +Contrafactual del propio generador. Si se quita la comprobación de orden pequeño, la de S < L o +la etiqueta, el generador se niega a emitir: reprueba F8-F9, F6 y F4. + +La decodificación canónica y la recodificación se cubren entre sí, y las dos se solapan con la +de orden pequeño en F10. Ningún vector puede separarlas. Las únicas llaves con dos +codificaciones son los puntos con y < 19 (y + p < 2^255), y de ninguno se conoce la llave +privada. Quitar una sola de esas tres no tumba nada; quitar las tres, sí. + +## Supuestos por confirmar (Irhis) + +1. `expiraEn` es un instante absoluto en segundos. Si es una duración, se regeneran. +2. **Canjear** firma `{aud, nonce, pub, token, ts}`: + - `pub` es la OIT de quien canjea; + - el token (la preimagen) va dentro de lo firmado. + + El §3 del contrato manda `{token, canjeadorOit, firma}` y el §3.1 habla de «la huella del + token». Con la huella sola el servidor no puede comprobar la preimagen. +3. **Revocar** firma `{aud, huella, nonce, pub, ts}`, con `pub` igual a `invitadorOit`. diff --git a/docs/vectores/invitaciones-v1/generar.py b/docs/vectores/invitaciones-v1/generar.py new file mode 100644 index 0000000..8229a8e --- /dev/null +++ b/docs/vectores/invitaciones-v1/generar.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +"""Vectores de prueba de la firma OIT de las invitaciones V1 (CONTRATO_INVITACIONES_V1 §3.1). + + python3 generar.py > invitaciones-v1.json + +Bytes firmados: etiqueta || 0x00 || JCS(cuerpo). Ed25519 en Python puro, sobre el código de +referencia del RFC 8032 §6, para no depender de la misma librería que se va a probar. Antes de +emitir nada se comprueba contra los vectores del RFC 8032 (TEST 1 y 2) y contra OpenSSL. + +JCS (RFC 8785) se implementa sólo para el dominio de estos cuerpos: claves y cadenas ASCII y +enteros < 2^53. Para ese dominio JCS es exactamente claves ordenadas, sin espacios y los escapes +mínimos de JSON; fuera de él, este generador se niega. + +Las llaves son las semillas PÚBLICAS de los TEST 1 y 2 del RFC 8032: nunca valen para nada real. +""" +import base64, calendar, hashlib, json, os, subprocess, sys, tempfile + +# ── Ed25519 (RFC 8032 §6) ──────────────────────────────────────────────────── +p = 2**255 - 19 +L = 2**252 + 27742317777372353535851937790883648493 +d = -121665 * pow(121666, p - 2, p) % p +SQRT_M1 = pow(2, (p - 1) // 4, p) +IDENTIDAD = (0, 1, 1, 0) + + +def sha512_modl(b): + return int.from_bytes(hashlib.sha512(b).digest(), "little") % L + + +def sumar(P, Q): + A = (P[1] - P[0]) * (Q[1] - Q[0]) % p + B = (P[1] + P[0]) * (Q[1] + Q[0]) % p + C = 2 * P[3] * Q[3] * d % p + D = 2 * P[2] * Q[2] % p + E, F, G, H = B - A, D - C, D + C, B + A + return (E * F % p, G * H % p, F * G % p, E * H % p) + + +def multiplicar(s, P): + Q = IDENTIDAD + while s > 0: + if s & 1: + Q = sumar(Q, P) + P = sumar(P, P) + s >>= 1 + return Q + + +def iguales(P, Q): + return (P[0] * Q[2] - Q[0] * P[2]) % p == 0 and (P[1] * Q[2] - Q[1] * P[2]) % p == 0 + + +def recuperar_x(y, signo): + if y >= p: + return None + x2 = (y * y - 1) * pow(d * y * y + 1, p - 2, p) + if x2 == 0: + return None if signo else 0 + x = pow(x2, (p + 3) // 8, p) + if (x * x - x2) % p != 0: + x = x * SQRT_M1 % p + if (x * x - x2) % p != 0: + return None + if (x & 1) != signo: + x = p - x + return x + + +gy = 4 * pow(5, p - 2, p) % p +gx = recuperar_x(gy, 0) +BASE = (gx, gy, 1, gx * gy % p) + + +def comprimir(P): + zi = pow(P[2], p - 2, p) + x, y = P[0] * zi % p, P[1] * zi % p + return int.to_bytes(y | ((x & 1) << 255), 32, "little") + + +def descomprimir(s, laxo=False): + """Estricto: rechaza y >= p y el «cero negativo», como el RFC 8032. Laxo: reduce y mod p, + como hacen los verificadores permisivos (ZIP-215 y otros).""" + y = int.from_bytes(s, "little") + signo = y >> 255 + y &= (1 << 255) - 1 + if laxo: + y %= p + x = recuperar_x(y, signo) + if x is None and laxo and signo: + x = recuperar_x(y, 0) + if x is None: + return None + return (x, y, 1, x * y % p) + + +def expandir(semilla): + h = hashlib.sha512(semilla).digest() + a = int.from_bytes(h[:32], "little") + a &= (1 << 254) - 8 + a |= 1 << 254 + return a, h[32:] + + +def publica(semilla): + return comprimir(multiplicar(expandir(semilla)[0], BASE)) + + +def firmar(semilla, msg): + a, prefijo = expandir(semilla) + A = comprimir(multiplicar(a, BASE)) + r = sha512_modl(prefijo + msg) + R = comprimir(multiplicar(r, BASE)) + h = sha512_modl(R + A + msg) + return R + int.to_bytes((r + h * a) % L, 32, "little") + + +def orden_pequeno(P): + return iguales(multiplicar(8, P), IDENTIDAD) + + +def verificar(pub, msg, firma, modo): + """modo «estricto»: lo que §3.1 exige (codificación canónica de A y R, S < L, A de orden no + pequeño). modo «laxo»: sólo la ecuación, con A y S tal como lleguen. Sirve para enseñar qué + vectores pasan en un verificador permisivo y por qué hace falta cada comprobación.""" + if len(pub) != 32 or len(firma) != 64: + return False + estricto = modo == "estricto" + A = descomprimir(pub, laxo=not estricto) + R = descomprimir(firma[:32], laxo=not estricto) + if A is None or R is None: + return False + S = int.from_bytes(firma[32:], "little") + if estricto and (S >= L or comprimir(A) != pub or orden_pequeno(A)): + return False + h = sha512_modl(firma[:32] + pub + msg) + return iguales(multiplicar(S % L if not estricto else S, BASE), sumar(R, multiplicar(h, A))) + + +# ── comprobaciones del propio generador ───────────────────────────────────── +RFC = [ # RFC 8032 §7.1, TEST 1 y TEST 2 + ("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "", + "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" + "5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"), + ("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72", + "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da" + "085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"), +] +for sk, pk, m, sig in RFC: + sk, m = bytes.fromhex(sk), bytes.fromhex(m) + assert publica(sk).hex() == pk, "la llave pública no coincide con el RFC 8032" + assert firmar(sk, m).hex() == sig, "la firma no coincide con el RFC 8032" + assert verificar(bytes.fromhex(pk), m, bytes.fromhex(sig), "estricto") + assert not verificar(bytes.fromhex(pk), m + b"x", bytes.fromhex(sig), "estricto") + + +def openssl_firma(semilla, msg): + """Segunda implementación independiente: OpenSSL, con la misma semilla (Ed25519 es + determinista, así que la firma tiene que salir idéntica byte a byte).""" + der = bytes.fromhex("302e020100300506032b657004220420") + semilla + with tempfile.TemporaryDirectory() as t: + k, mm = os.path.join(t, "k.der"), os.path.join(t, "m") + open(k, "wb").write(der) + open(mm, "wb").write(msg) + return subprocess.run(["openssl", "pkeyutl", "-sign", "-inkey", k, "-keyform", "DER", + "-rawin", "-in", mm], capture_output=True, check=True).stdout + + +def openssl_verifica(pub, msg, firma): + """Lo que dice un verificador de terceros muy usado. Medido el 2026-10-28 con OpenSSL 3.6.4: + ACEPTA F8, F9 y F10, las firmas forjadas sin llave privada.""" + with tempfile.TemporaryDirectory() as t: + k, mm, ss = (os.path.join(t, n) for n in ("k.der", "m", "s")) + open(k, "wb").write(bytes.fromhex("302a300506032b6570032100") + pub) + open(mm, "wb").write(msg) + open(ss, "wb").write(firma) + return subprocess.run(["openssl", "pkeyutl", "-verify", "-pubin", "-inkey", k, "-keyform", "DER", + "-rawin", "-in", mm, "-sigfile", ss], capture_output=True).returncode == 0 + + +# ── JCS restringido ───────────────────────────────────────────────────────── +def jcs(obj): + for k, v in obj.items(): + assert isinstance(k, str) and k.isascii(), k + if isinstance(v, str): + assert v.isascii() and all(0x20 <= ord(c) < 0x7f and c not in '"\\' for c in v), v + else: + assert isinstance(v, int) and not isinstance(v, bool) and 0 <= v < 2**53, v + # con claves ASCII, el orden por unidades UTF-16 de RFC 8785 es el orden de bytes + return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode() + + +def b64u(b): + return base64.urlsafe_b64encode(b).rstrip(b"=").decode() + + +def det(etiqueta, n): + """Bytes de prueba deterministas. En producción, nonce y token son ALEATORIOS.""" + return hashlib.sha256(b"origin-invitaciones/vectores-de-prueba/" + etiqueta.encode()).digest()[:n] + + +ETQ = {op: f"origin-invitaciones/{op}/v1" for op in ("crear", "canjear", "revocar")} +AUD = "originmetaverse.com" +TS = calendar.timegm((2026, 10, 28, 12, 0, 0)) +LLAVE_A = bytes.fromhex(RFC[0][0]) # quien invita +LLAVE_B = bytes.fromhex(RFC[1][0]) # quien canjea +PUB_A, PUB_B = publica(LLAVE_A), publica(LLAVE_B) +TOKEN = b64u(det("token", 32)) +HUELLA = hashlib.sha256(TOKEN.encode()).hexdigest() +assert len(TOKEN) == 43 + + +def mensaje(etiqueta, cuerpo): + return etiqueta.encode() + b"\x00" + jcs(cuerpo) + + +def cuerpo(pub, nonce, **extra): + return {"aud": AUD, "ts": TS, "nonce": b64u(det(nonce, 16)), "pub": b64u(pub), **extra} + + +C_CREAR = cuerpo(PUB_A, "n1", destino="mundo-de-prueba.v1", expiraEn=TS + 7 * 86400) +C_CANJEAR = cuerpo(PUB_B, "n2", token=TOKEN) +C_REVOCAR = cuerpo(PUB_A, "n3", huella=HUELLA) + +vectores = [] + + +def caso(id, op, c, firma, llave_cabecera, espera, motivo, reloj=TS, nota=None, transporte=None, + firmado_con=None): + m = mensaje(ETQ[op], c) + v = { + "id": id, "operacion": op, "etiqueta": ETQ[op], + "cuerpo": c, "jcs": jcs(c).decode(), + "transporte_json": transporte if transporte is not None else jcs(c).decode(), + "mensaje_hex": m.hex(), + "cabeceras": {"Origin-Llave": b64u(llave_cabecera), "Origin-Firma": b64u(firma)}, + "reloj_del_servidor": reloj, + "espera": espera, "motivo": motivo, + "verificador_estricto_acepta_la_firma": verificar(llave_cabecera, m, firma, "estricto"), + "verificador_laxo_acepta_la_firma": verificar(llave_cabecera, m, firma, "laxo"), + "openssl_acepta_la_firma": openssl_verifica(llave_cabecera, m, firma), + } + if firmado_con: + v["firmado_con"] = firmado_con + if nota: + v["nota"] = nota + vectores.append(v) + return v + + +# Válidos +for id, op, c, sk, pub in (("V1", "crear", C_CREAR, LLAVE_A, PUB_A), + ("V2", "canjear", C_CANJEAR, LLAVE_B, PUB_B), + ("V3", "revocar", C_REVOCAR, LLAVE_A, PUB_A)): + f = firmar(sk, mensaje(ETQ[op], c)) + assert f == openssl_firma(sk, mensaje(ETQ[op], c)), "OpenSSL no da la misma firma" + caso(id, op, c, f, pub, "acepta", "petición válida") +F_V1 = firmar(LLAVE_A, mensaje(ETQ["crear"], C_CREAR)) + +# El orden y los espacios del transporte no importan: el servidor re-canonicaliza. +caso("V4", "crear", C_CREAR, F_V1, PUB_A, "acepta", + "mismo V1 con otro orden de claves y espacios en el transporte: el servidor firma sobre JCS, " + "no sobre los bytes recibidos", + transporte='{ "pub": "%s", "ts": %d, "nonce": "%s", "aud": "%s", "expiraEn": %d, "destino": "%s" }' + % (C_CREAR["pub"], TS, C_CREAR["nonce"], AUD, C_CREAR["expiraEn"], C_CREAR["destino"]), + nota="usar como petición nueva; si se manda DESPUÉS de V1, es el caso F2 (nonce repetido)") + +# Deben fallar +caso("F1", "crear", C_CREAR, F_V1, PUB_A, "rechaza", "nonce repetido", + nota="enviar V1 y luego exactamente la misma petición: la firma es buena, el par (pub, nonce) " + "ya se usó dentro de la ventana") +caso("F2", "crear", C_CREAR, F_V1, PUB_A, "rechaza", "ts fuera de la ventana de ±5 min", + reloj=TS + 301, nota="mismo V1, con el reloj del servidor 301 s por delante") +caso("F3", "crear", C_CREAR, F_V1, PUB_A, "rechaza", "ts fuera de la ventana de ±5 min", + reloj=TS - 301, nota="mismo V1, con el reloj del servidor 301 s por detrás (ts del futuro)") + +# F4: la firma de «crear» puesta sobre un cuerpo de «canjear» +f_cruzada = firmar(LLAVE_B, mensaje(ETQ["crear"], C_CANJEAR)) +caso("F4", "canjear", C_CANJEAR, f_cruzada, PUB_B, "rechaza", + "etiqueta cruzada: la firma se hizo con la etiqueta de crear sobre el cuerpo de canjear", + firmado_con=ETQ["crear"]) + +# F5: Origin-Llave distinta del pub firmado +caso("F5", "crear", C_CREAR, F_V1, PUB_B, "rechaza", + "Origin-Llave no es igual al pub del cuerpo (debe rechazarse ANTES de verificar)") + +# F6: S no canónico (S + L): misma firma, otros bytes +S = int.from_bytes(F_V1[32:], "little") +f_mal = F_V1[:32] + int.to_bytes(S + L, 32, "little") +caso("F6", "crear", C_CREAR, f_mal, PUB_A, "rechaza", "S no canónico (S >= L): firma maleable") + +# F7: firma de otro cuerpo (cambió destino después de firmar) +otro = dict(C_CREAR, destino="otro-mundo") +caso("F7", "crear", otro, F_V1, PUB_A, "rechaza", "el cuerpo no es el firmado (destino cambiado)") + + +# F8-F10: llaves de orden pequeño con la firma trivial, sin llave privada +def forjar(pub_bytes, etiqueta_nonce): + A = descomprimir(pub_bytes, laxo=True) + sub = [IDENTIDAD] + while True: + nxt = sumar(sub[-1], A) + if iguales(nxt, IDENTIDAD): + break + sub.append(nxt) + for intento in range(64): + c = cuerpo(pub_bytes, f"{etiqueta_nonce}-{intento}", + destino="mundo-de-prueba.v1", expiraEn=TS + 7 * 86400) + m = mensaje(ETQ["crear"], c) + for Rp in sub: + Rb = comprimir(Rp) + h = sha512_modl(Rb + pub_bytes + m) + if iguales(sumar(Rp, multiplicar(h, A)), IDENTIDAD): + return c, Rb + bytes(32) + raise SystemExit("no se pudo forjar") + + +for id, hexpub, nombre in ( + ("F8", "01" + "00" * 31, "la identidad (orden 1)"), + ("F9", "ec" + "ff" * 30 + "7f", "el punto de orden 2"), + ("F10", "ee" + "ff" * 30 + "7f", "una codificación NO canónica de la identidad (y = p + 1)")): + pb = bytes.fromhex(hexpub) + c, f = forjar(pb, id) + v = caso(id, "crear", c, f, pb, "rechaza", + f"llave de orden pequeño: {nombre}. Firma trivial R de orden pequeño y S = 0, hecha SIN " + "llave privada", + nota="la ecuación de verificación SE CUMPLE: sólo la rechaza la comprobación explícita " + "de llave fuerte (en la cadena, is_strong_ed25519_key; RV14)") + assert v["verificador_laxo_acepta_la_firma"], f"{id}: la forja debería pasar la ecuación" + if not v["openssl_acepta_la_firma"]: + print(f"aviso: este OpenSSL ya rechaza {id}", file=sys.stderr) + assert not v["verificador_estricto_acepta_la_firma"] + +# F11: clave duplicada en el transporte +caso("F11", "crear", C_CREAR, F_V1, PUB_A, "rechaza", + "clave duplicada en el JSON recibido. JCS (RFC 8785 §3.1, I-JSON) no la admite, y JSON.parse " + "se queda en silencio con la última", + transporte=jcs(C_CREAR).decode()[:-1] + ',"destino":"otro-mundo"}') + +# coherencia final +for v in vectores: + if v["espera"] == "acepta": + assert v["verificador_estricto_acepta_la_firma"], v["id"] +for id in ("F4", "F6", "F7"): + v = next(x for x in vectores if x["id"] == id) + assert not v["verificador_estricto_acepta_la_firma"], id + +salida = { + "version": "origin-invitaciones/vectores/v1", + "contrato": "origin-metaverse-web docs/CONTRATO_INVITACIONES_V1.md §3.1 (d5f5f5e)", + "autora": "Charon, 2026-10-28", + "mensaje_firmado": "utf8(etiqueta) || 0x00 || JCS(cuerpo)", + "codificacion": "pub, firma, nonce y token en base64url sin relleno; huella = sha256(token) en hex", + "llaves_de_prueba": { + "A_quien_invita": {"semilla_hex": LLAVE_A.hex(), "pub_hex": PUB_A.hex(), "pub_b64url": b64u(PUB_A), + "origen": "RFC 8032 §7.1 TEST 1 (pública: nunca para nada real)"}, + "B_quien_canjea": {"semilla_hex": LLAVE_B.hex(), "pub_hex": PUB_B.hex(), "pub_b64url": b64u(PUB_B), + "origen": "RFC 8032 §7.1 TEST 2"}, + }, + "token_de_prueba": TOKEN, "huella_de_prueba": HUELLA, + "comprobado_con": ["RFC 8032 TEST 1 y 2 (llave y firma)", "OpenSSL " + subprocess.run( + ["openssl", "version"], capture_output=True, text=True).stdout.split()[1] + " (V1-V3 byte a byte)"], + "como_usarlos": [ + "Cada vector es una petición: transporte_json es el cuerpo HTTP y cabeceras sus cabeceras.", + "Fija el reloj del servidor en reloj_del_servidor y empieza cada vector con el almacén de " + "nonces vacío, salvo F1, que va justo después de V1.", + "espera = acepta | rechaza. Un test que no sepa reprobar no vale: quita cada comprobación " + "y mira que su vector pase a aceptarse.", + "verificador_laxo_acepta_la_firma dice qué pasaría con un verificador permisivo: F6 y F8-F10 " + "pasan ahí. F8-F10 cumplen la ecuación: OpenSSL 3.6.4 los ACEPTA (openssl_acepta_la_firma). " + "Si tu verify con zip215:false tampoco los rechaza, falta la comprobación de llave fuerte: " + "decodificar pub, exigir que vuelva a codificarse igual y que no sea de orden pequeño.", + ], + "supuestos_por_confirmar": [ + "expiraEn es un instante absoluto en segundos (ts + 7 días aquí). Si es una duración, se regeneran.", + "canjear firma {aud, nonce, pub, token, ts}: pub es la OIT de quien canjea y el token (la " + "preimagen) va DENTRO de lo firmado. El §3 punto 3 dice {token, canjeadorOit, firma} y el " + "§3.1 dice «la huella del token»: con la huella sola el servidor no puede comprobar la preimagen.", + "revocar firma {aud, huella, nonce, pub, ts}, con pub igual a invitadorOit.", + ], + "vectores": vectores, +} +json.dump(salida, sys.stdout, ensure_ascii=False, indent=2) +print() diff --git a/docs/vectores/invitaciones-v1/invitaciones-v1.json b/docs/vectores/invitaciones-v1/invitaciones-v1.json new file mode 100644 index 0000000..7cb7a78 --- /dev/null +++ b/docs/vectores/invitaciones-v1/invitaciones-v1.json @@ -0,0 +1,435 @@ +{ + "version": "origin-invitaciones/vectores/v1", + "contrato": "origin-metaverse-web docs/CONTRATO_INVITACIONES_V1.md §3.1 (d5f5f5e)", + "autora": "Charon, 2026-10-28", + "mensaje_firmado": "utf8(etiqueta) || 0x00 || JCS(cuerpo)", + "codificacion": "pub, firma, nonce y token en base64url sin relleno; huella = sha256(token) en hex", + "llaves_de_prueba": { + "A_quien_invita": { + "semilla_hex": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "pub_hex": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "pub_b64url": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "origen": "RFC 8032 §7.1 TEST 1 (pública: nunca para nada real)" + }, + "B_quien_canjea": { + "semilla_hex": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "pub_hex": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", + "pub_b64url": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw", + "origen": "RFC 8032 §7.1 TEST 2" + } + }, + "token_de_prueba": "HVC4TTwAc5X9awxdLTV8X9kDOcw_g3yUJSOadF_UW64", + "huella_de_prueba": "d24ddd65bd757dc69b7a630d8410556221a433c4329531d22f66d23d4348b76c", + "comprobado_con": [ + "RFC 8032 TEST 1 y 2 (llave y firma)", + "OpenSSL 3.6.4 (V1-V3 byte a byte)" + ], + "como_usarlos": [ + "Cada vector es una petición: transporte_json es el cuerpo HTTP y cabeceras sus cabeceras.", + "Fija el reloj del servidor en reloj_del_servidor y empieza cada vector con el almacén de nonces vacío, salvo F1, que va justo después de V1.", + "espera = acepta | rechaza. Un test que no sepa reprobar no vale: quita cada comprobación y mira que su vector pase a aceptarse.", + "verificador_laxo_acepta_la_firma dice qué pasaría con un verificador permisivo: F6 y F8-F10 pasan ahí. F8-F10 cumplen la ecuación: OpenSSL 3.6.4 los ACEPTA (openssl_acepta_la_firma). Si tu verify con zip215:false tampoco los rechaza, falta la comprobación de llave fuerte: decodificar pub, exigir que vuelva a codificarse igual y que no sea de orden pequeño." + ], + "supuestos_por_confirmar": [ + "expiraEn es un instante absoluto en segundos (ts + 7 días aquí). Si es una duración, se regeneran.", + "canjear firma {aud, nonce, pub, token, ts}: pub es la OIT de quien canjea y el token (la preimagen) va DENTRO de lo firmado. El §3 punto 3 dice {token, canjeadorOit, firma} y el §3.1 dice «la huella del token»: con la huella sola el servidor no puede comprobar la preimagen.", + "revocar firma {aud, huella, nonce, pub, ts}, con pub igual a invitadorOit." + ], + "vectores": [ + { + "id": "V1", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793188800, + "espera": "acepta", + "motivo": "petición válida", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true + }, + { + "id": "V2", + "operacion": "canjear", + "etiqueta": "origin-invitaciones/canjear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "ioBVewvUqbJNvOUwlE380w", + "pub": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw", + "token": "HVC4TTwAc5X9awxdLTV8X9kDOcw_g3yUJSOadF_UW64" + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"nonce\":\"ioBVewvUqbJNvOUwlE380w\",\"pub\":\"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw\",\"token\":\"HVC4TTwAc5X9awxdLTV8X9kDOcw_g3yUJSOadF_UW64\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"nonce\":\"ioBVewvUqbJNvOUwlE380w\",\"pub\":\"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw\",\"token\":\"HVC4TTwAc5X9awxdLTV8X9kDOcw_g3yUJSOadF_UW64\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63616e6a6561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c226e6f6e6365223a22696f42566577765571624a4e764f55776c4533383077222c22707562223a2250554158772d6844695671537477716e5452742d764a79594c4d3875784a614d774d3156385372305a6777222c22746f6b656e223a22485643345454774163355839617778644c54563858396b444f63775f673379554a534f6164465f55573634222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw", + "Origin-Firma": "3Z79SlRC62mrNAzMLP6o1CNbIdj2uIWlc2sWzQvQDhZP5m9dHCKPjrI3SqM33eZQRnYx7b4ouq2ARR2Yi-ujBA" + }, + "reloj_del_servidor": 1793188800, + "espera": "acepta", + "motivo": "petición válida", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true + }, + { + "id": "V3", + "operacion": "revocar", + "etiqueta": "origin-invitaciones/revocar/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "CbdsIxFrDPvBSN_2qYeVcw", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "huella": "d24ddd65bd757dc69b7a630d8410556221a433c4329531d22f66d23d4348b76c" + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"huella\":\"d24ddd65bd757dc69b7a630d8410556221a433c4329531d22f66d23d4348b76c\",\"nonce\":\"CbdsIxFrDPvBSN_2qYeVcw\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"huella\":\"d24ddd65bd757dc69b7a630d8410556221a433c4329531d22f66d23d4348b76c\",\"nonce\":\"CbdsIxFrDPvBSN_2qYeVcw\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f7265766f6361722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c226875656c6c61223a2264323464646436356264373537646336396237613633306438343130353536323231613433336334333239353331643232663636643233643433343862373663222c226e6f6e6365223a22436264734978467244507642534e5f32715965566377222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "FR69zi1RDZYrREfVHhZkoO190h27Mrw74_NLPIxze79_QZMRXL8f700OPAzOu00CQLaDIuel4VD30mvqtRcHBQ" + }, + "reloj_del_servidor": 1793188800, + "espera": "acepta", + "motivo": "petición válida", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true + }, + { + "id": "V4", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{ \"pub\": \"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\", \"ts\": 1793188800, \"nonce\": \"l-SsDclPrSgeO_MdNULifQ\", \"aud\": \"originmetaverse.com\", \"expiraEn\": 1793793600, \"destino\": \"mundo-de-prueba.v1\" }", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793188800, + "espera": "acepta", + "motivo": "mismo V1 con otro orden de claves y espacios en el transporte: el servidor firma sobre JCS, no sobre los bytes recibidos", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true, + "nota": "usar como petición nueva; si se manda DESPUÉS de V1, es el caso F2 (nonce repetido)" + }, + { + "id": "F1", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "nonce repetido", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true, + "nota": "enviar V1 y luego exactamente la misma petición: la firma es buena, el par (pub, nonce) ya se usó dentro de la ventana" + }, + { + "id": "F2", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793189101, + "espera": "rechaza", + "motivo": "ts fuera de la ventana de ±5 min", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true, + "nota": "mismo V1, con el reloj del servidor 301 s por delante" + }, + { + "id": "F3", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793188499, + "espera": "rechaza", + "motivo": "ts fuera de la ventana de ±5 min", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true, + "nota": "mismo V1, con el reloj del servidor 301 s por detrás (ts del futuro)" + }, + { + "id": "F4", + "operacion": "canjear", + "etiqueta": "origin-invitaciones/canjear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "ioBVewvUqbJNvOUwlE380w", + "pub": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw", + "token": "HVC4TTwAc5X9awxdLTV8X9kDOcw_g3yUJSOadF_UW64" + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"nonce\":\"ioBVewvUqbJNvOUwlE380w\",\"pub\":\"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw\",\"token\":\"HVC4TTwAc5X9awxdLTV8X9kDOcw_g3yUJSOadF_UW64\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"nonce\":\"ioBVewvUqbJNvOUwlE380w\",\"pub\":\"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw\",\"token\":\"HVC4TTwAc5X9awxdLTV8X9kDOcw_g3yUJSOadF_UW64\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63616e6a6561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c226e6f6e6365223a22696f42566577765571624a4e764f55776c4533383077222c22707562223a2250554158772d6844695671537477716e5452742d764a79594c4d3875784a614d774d3156385372305a6777222c22746f6b656e223a22485643345454774163355839617778644c54563858396b444f63775f673379554a534f6164465f55573634222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw", + "Origin-Firma": "8tzYumpNlvl9a42l970jmXJB5PFYUq1pK-RJagxXDjq74a_hkeOVtDqGrOK0eopWrfNs9kWwE423IP99fsKDDg" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "etiqueta cruzada: la firma se hizo con la etiqueta de crear sobre el cuerpo de canjear", + "verificador_estricto_acepta_la_firma": false, + "verificador_laxo_acepta_la_firma": false, + "openssl_acepta_la_firma": false, + "firmado_con": "origin-invitaciones/crear/v1" + }, + { + "id": "F5", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "Origin-Llave no es igual al pub del cuerpo (debe rechazarse ANTES de verificar)", + "verificador_estricto_acepta_la_firma": false, + "verificador_laxo_acepta_la_firma": false, + "openssl_acepta_la_firma": false + }, + { + "id": "F6", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZInqzhKG1Umj33GNd1bhQnwZ3ead6U9JeqJUdgY0Oc8HEg" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "S no canónico (S >= L): firma maleable", + "verificador_estricto_acepta_la_firma": false, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": false + }, + { + "id": "F7", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "otro-mundo", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"otro-mundo\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"otro-mundo\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226f74726f2d6d756e646f222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "el cuerpo no es el firmado (destino cambiado)", + "verificador_estricto_acepta_la_firma": false, + "verificador_laxo_acepta_la_firma": false, + "openssl_acepta_la_firma": false + }, + { + "id": "F8", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "XJeasE1GFNTkdfN8ER9KCg", + "pub": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"XJeasE1GFNTkdfN8ER9KCg\",\"pub\":\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"XJeasE1GFNTkdfN8ER9KCg\",\"pub\":\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a22584a656173453147464e546b64664e384552394b4367222c22707562223a2241514141414141414141414141414141414141414141414141414141414141414141414141414141414141222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "Origin-Firma": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "llave de orden pequeño: la identidad (orden 1). Firma trivial R de orden pequeño y S = 0, hecha SIN llave privada", + "verificador_estricto_acepta_la_firma": false, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true, + "nota": "la ecuación de verificación SE CUMPLE: sólo la rechaza la comprobación explícita de llave fuerte (en la cadena, is_strong_ed25519_key; RV14)" + }, + { + "id": "F9", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "REfXPa4tGjc5slEI5bpktg", + "pub": "7P_______________________________________38", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"REfXPa4tGjc5slEI5bpktg\",\"pub\":\"7P_______________________________________38\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"REfXPa4tGjc5slEI5bpktg\",\"pub\":\"7P_______________________________________38\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a225245665850613474476a6335736c45493562706b7467222c22707562223a2237505f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f3338222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "7P_______________________________________38", + "Origin-Firma": "7P_______________________________________38AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "llave de orden pequeño: el punto de orden 2. Firma trivial R de orden pequeño y S = 0, hecha SIN llave privada", + "verificador_estricto_acepta_la_firma": false, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true, + "nota": "la ecuación de verificación SE CUMPLE: sólo la rechaza la comprobación explícita de llave fuerte (en la cadena, is_strong_ed25519_key; RV14)" + }, + { + "id": "F10", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "PBi0xI2621syp0145Qoi5Q", + "pub": "7v_______________________________________38", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"PBi0xI2621syp0145Qoi5Q\",\"pub\":\"7v_______________________________________38\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"PBi0xI2621syp0145Qoi5Q\",\"pub\":\"7v_______________________________________38\",\"ts\":1793188800}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a225042693078493236323173797030313435516f693551222c22707562223a2237765f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f3338222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "7v_______________________________________38", + "Origin-Firma": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "llave de orden pequeño: una codificación NO canónica de la identidad (y = p + 1). Firma trivial R de orden pequeño y S = 0, hecha SIN llave privada", + "verificador_estricto_acepta_la_firma": false, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true, + "nota": "la ecuación de verificación SE CUMPLE: sólo la rechaza la comprobación explícita de llave fuerte (en la cadena, is_strong_ed25519_key; RV14)" + }, + { + "id": "F11", + "operacion": "crear", + "etiqueta": "origin-invitaciones/crear/v1", + "cuerpo": { + "aud": "originmetaverse.com", + "ts": 1793188800, + "nonce": "l-SsDclPrSgeO_MdNULifQ", + "pub": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "destino": "mundo-de-prueba.v1", + "expiraEn": 1793793600 + }, + "jcs": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800}", + "transporte_json": "{\"aud\":\"originmetaverse.com\",\"destino\":\"mundo-de-prueba.v1\",\"expiraEn\":1793793600,\"nonce\":\"l-SsDclPrSgeO_MdNULifQ\",\"pub\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\",\"ts\":1793188800,\"destino\":\"otro-mundo\"}", + "mensaje_hex": "6f726967696e2d696e7669746163696f6e65732f63726561722f7631007b22617564223a226f726967696e6d65746176657273652e636f6d222c2264657374696e6f223a226d756e646f2d64652d7072756562612e7631222c22657870697261456e223a313739333739333630302c226e6f6e6365223a226c2d537344636c50725367654f5f4d644e554c696651222c22707562223a223131715941594b7843726656535f3754795751484f6737686376506170694d6c727749616150634855526f222c227473223a313739333138383830307d", + "cabeceras": { + "Origin-Llave": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "Origin-Firma": "94ykfqmBeDJEvKgBdGBGmdUZFQPprWW2F79-heC7ZIn9-hwpu-aQh5vwf7MCSZ0E3ead6U9JeqJUdgY0Oc8HAg" + }, + "reloj_del_servidor": 1793188800, + "espera": "rechaza", + "motivo": "clave duplicada en el JSON recibido. JCS (RFC 8785 §3.1, I-JSON) no la admite, y JSON.parse se queda en silencio con la última", + "verificador_estricto_acepta_la_firma": true, + "verificador_laxo_acepta_la_firma": true, + "openssl_acepta_la_firma": true + } + ] +}