Tu voz, tu voto

La infraestructura de voto para elecciones estatales, comunidades, clubes y empresas.

KVoice es un sistema de voto verificable: identidad, censo, credencial ciega, urna cifrada, anti-doble-voto, árbol Merkle y notario en cadena. El mismo motor sirve a una comunidad de vecinos y a un proceso de escala estatal.

KVoice — infraestructura de voto verificable

Qué hay debajo: tres verdades, no una

La arquitectura canónica de KVoice separa roles que otras plataformas mezclan. Eso no es estética: es la condición para que el voto sea secreto y el recuento sea comprobable.

Postgres 16 + RLS La verdad operativa: censo, votaciones, commitments, logs append-only
Telos El notario: solo hashes, raíces Merkle y firmas de resultado
Qdrant + AVA Memoria semántica: reglamentos y resúmenes. Nunca el voto individual
App votante Angular 22 · Ionic 9 · Capacitor 7 API KVoice FastAPI · 2 réplicas k3s BCN Firebase Auth JWT · Google / email / passkey Redis Rate limit · colas Qdrant Memoria AVA Postgres 16 · Row Level Security tenant_id obligatorio · kvoice_app NOBYPASSRLS Scheduler Merkle 60 s SHA-256 · domain separation 0x00 / 0x01 Anclaje Telos Solo raíz + voting_id + lote · sin PII El votante no tiene cuenta Telos. En cadena no aparece quién votó ni qué votó.

Capas reales del producto: app, API en Barcelona, Postgres con RLS, lotes Merkle y notario Telos.

CapaQué construimos
ClienteApp Angular 22 + Ionic 9 + Capacitor 7. El voto se cifra en el dispositivo. Cliente Chaum en TypeScript.
APIFastAPI en k3s Barcelona, 2 réplicas, filesystem de solo lectura. Multi-tenant desde el primer request.
IdentidadFirebase Auth (email, Google, passkey) para comunidades y empresas. Adaptadores Cl@ve, DNIe, FNMT y eIDAS Wallet para sector público. SSO SAML/OIDC (Google Workspace, Microsoft Entra, Okta) para empresas.
DatosPostgres 16 con Row Level Security. Roles kvoice_owner / kvoice_app. Logs append-only. Censo con snapshot al abrir.
Motor de privacidadInterfaz PrivacyEngine intercambiable: CommitmentEngine (HMAC + nullifier + commitment), firma ciega Chaum RSA-FDH 2048, y SemaphoreEngine (pruebas ZK) para anonimato fuerte.
Urna secretaSealedBox NaCl (X25519 + XSalsa20-Poly1305). Shamir GF(2⁸). Umbral de custodios 2/3, 3/5, 4/7 o 5/9. La urna no ve el user_id.
IntegridadÁrbol Merkle SHA-256 con domain separation, prueba de inclusión, anclaje de la raíz y resultado firmado.
Notario TelosCuentas kvoiceanchor y kvoicetally: solo hashes, raíces Merkle y firmas. El votante no tiene wallet. Multisig de operadores.
IAAVA resume reglamentos y comentarios agregados. No toca el voto individual, no recomienda y no lee el censo.

Flujo de punta a punta

Doce procesos. Cada uno tiene un responsable, un input y un output. Si uno falla de forma silenciosa, el recuento no es auditable.

1. Tenant organización 2. Censo eligible_voters 3. Mesa clave + Shamir 4. Publicar invitaciones 5. Identidad JWT Firebase 6. Chaum token ciego 7. Cifrar SealedBox cliente 8. Urna sin JWT · nullifier 9. Recibo acta firmada 10. Merkle lote cada 60 s 11. Unseal umbral custodios 12. Verificar cualquiera Separación crítica (pasos 6–8) El emisor (JWT) firma la credencial ciega y no ve la papeleta. La urna verifica la firma Chaum y no ve el user_id. El cliente descega s = s′ · r⁻¹ mod n. Nullifier v2 = H(credential ∥ voting_id). Un nullifier = un voto contado. En cadena solo viaja la raíz Merkle del lote. Manipular un voto cambia la raíz. El recibo permite recalcular el camino. Esquema chaum-rsa-fdh-v1 · RSA-2048 · e=65537 · FDH SHA-256 con dominio versionado.

El emisor no ve el voto. La urna no ve la identidad. Telos no ve ni uno ni otro.

Proceso 1–5 · Tenant, censo, mesa e identidad

1. Tenant y roles

Cada organización es un tenant. La sesión declara SET LOCAL app.current_tenant_id. RLS rechaza cualquier fila de otro tenant. Roles: super_admin, tenant_admin, creator, voter, auditor, observer.

Entra
JWT Firebase + X-Tenant-Id
Sale
Usuario provisionado y membresía

2. Censo (eligible_voters)

Solo quien está en el censo recibe credencial. El censo se fija al abrir (eligible_count_at_open). Objetivo de diseño: impedir altas, bajas o cambios a mitad de escrutinio — el ataque clásico de “añadir amigos a la lista”.

Entra
Lista de elegibles (email / id)
Sale
Snapshot inmutable al abrir

3. Mesa y custodios

En voto secreto se genera un par de claves de urna. La privada se parte con Shamir GF(2⁸). Umbrales configurables: 2/3, 3/5, 4/7, 5/9. Un solo operador no puede abrir la urna. El plaintext no se persiste; la clave se purga de RAM tras el unseal.

Entra
Umbral k de n y emails de custodios
Sale
election_pk + shares cifrados

4–5. Publicación e identidad

Al publicar se envían invitaciones con token de un solo uso (/votar/{token}). El votante se autentica: email, Google, passkey; en empresas SSO; en sector público Cl@ve, DNIe, FNMT o eIDAS. La identidad decide si puedes votar, no qué votas.

Entra
Login + pertenencia al censo
Sale
Sesión autorizada a pedir token

Quórum y mayoría son reglas de validez, no de recuento: none, min_votes o min_percent_eligible, más mayoría sobre preguntas single_choice formales. El recuento se publica siempre; la validez es un banner comprobable.

Proceso 6 · Firma ciega Chaum (el emisor no ve la papeleta)

Sin ceguera, el servidor que comprueba el censo puede correlacionar “a este usuario le di este token” con “este token votó B”. Eso rompe el secreto aunque el voto vaya cifrado. Por eso el cliente pide una firma ciega.

m′ = m · rᵉ mod n  el emisor firma m′ sin ver m  el cliente descega s = s′ · r⁻¹ mod n la urna comprueba sᵉ ≡ FDH(n, domain ∥ voting_id ∥ credential)
Emisor (con JWT) POST /v1/votings/{id}/tokens/blind-sign Ve: usuario + censo No ve: papeleta ni credential RSA-2048 · e = 65537 Cliente (móvil / web) Genera r, cega, descega, cifra Urna (sin JWT) POST /v1/votings/{id}/box/votes Ve: firma + ciphertext + nullifier No ve: user_id Rate limit por IP + Chaum

Dos puertas distintas. Correlacionarlas exigiría colusión emisor–urna más ruptura del cegado.

El nullifier de este modo no se deriva del user_id (eso reidentifica), sino de la credencial anónima:

nullifier_v2 = SHA256("kvoice-nullifier-v2" ∥ credential ∥ voting_id)

En el modo abierto (votaciones no secretas) el motor es CommitmentEngine:

token = b64u(payload) || "." || b64u(HMAC-SHA256(secret, payload))  TTL 30 min nullifier_v1 = SHA256("kvoice-nullifier-v1" ∥ user_id ∥ voting_id) commitment = SHA256("kvoice-commitment-v1" ∥ voting_id ∥ option_id ∥ nonce)

Procesos 7–8 · Cifrado en el cliente y depósito en urna

El voto secreto se cifra antes de salir del teléfono. El backend guarda ciphertext. La urna no necesita saber la opción para rechazar un segundo voto: le basta el nullifier único.

Papeleta en el dispositivo

El cliente toma la election_pk anclada en el manifiesto, cifra con SealedBox y construye el commitment sobre el ciphertext (no sobre el texto en claro). El modelo de votación contempla single_choice, multi_choice, ranking y budget.

Anti-doble-voto

Índice único sobre nullifier activo. Si revote_enabled, las filas anteriores quedan superseded=true; solo cuenta una. El segundo depósito con el mismo nullifier se rechaza. Eso es “una persona, un voto” en el plano criptográfico, no un honor system.

Share 1
Share 2
Share 3
Share 4
Share 5

Shamir: con k partes se reconstruye; con k−1 la clave es información-teóricamente inútil. Nadie “abre un poco”.

Procesos 9–10 · Recibo, Merkle y notario Telos

Cada voto válido entra en un lote. El scheduler (cada 60 s) calcula la raíz y la ancla. Domain separation para no confundir hoja y nodo interno — el ataque clásico de segunda preimagen en Merkle-Damgård:

leaf = SHA256(0x00 ∥ commitment) nodo = SHA256(0x01 ∥ izquierda ∥ derecha) si el nivel es impar, se duplica la última hoja (convención tipo Bitcoin)
Raíz Merkle Hash AB Hash CD Voto A Voto B Voto C Voto D Anclaje en Telos

Cambiar un voto altera la raíz. La raíz anclada es la prueba pública de que el lote existió así.

En Telos no viaja el censo ni la opción. Viajan merkle_root, voting_id, batch_id, recuento de hojas. El votante no tiene wallet. Darle una cuenta en cadena a cada persona haría el voto seudónimo y correlacionable: exactamente lo que hay que evitar en unas elecciones.

result_hash = SHA256("kvoice-result-v1" ∥ voting_id ∥ json_canónico_ordenado)

Procesos 11–12 · Cierre, unseal, tally y verificación

Cierre y unseal

Al cerrar, los custodios aportan shares. Con el umbral se reconstruye la clave de mesa, se descifran los sealed boxes y se cuenta. La clave no se guarda. El log de auditoría es append-only: un trigger de Postgres impide UPDATE/DELETE.

Tally firmado

El recuento canónico se hashea y se firma. Cualquier observador puede bajar commitments, recomputar la raíz, compararla con el anclaje y contrastar el result_hash. Si alguien “ajusta” un 2% a última hora, la huella no cuadra.

Verificación independiente: no hace falta creernos

Las claves públicas de firma de actas están publicadas. Tres herramientas en este mismo sitio:

La página Trust Layer detalla las ocho capas de seguridad (identidad, elegibilidad, nullifier, privacidad, inmutabilidad, recuento, auditoría, ZK futuro).

Elecciones estatales: el mismo motor, a otra escala

Un proceso estatal no es “la misma app con más usuarios”. Es el mismo motor de urna con censo oficial, identidad reconocida por el Estado, custodios externos y recálculo público.

Censo oficial, no lista de emails

Millones de registros, corte de censo, mesas y colegios. El modelo eligible_voters + snapshot al abrir es el mismo patrón; el adaptador de entrada es el censo electoral, no un CSV de una comunidad de vecinos.

Identidad reconocida por el Estado

Firebase basta para una asociación. Un proceso estatal exige Cl@ve, DNIe, FNMT o la wallet eIDAS. La API ya aísla identidad detrás de AuthBackend: se cambia el adaptador, no el recuento.

Nadie (ni el operador) abre la urna solo

Shamir + custodios externos (administración, interventores, notaría, auditor). En un proceso oficial el umbral no puede vivir en un único servidor de una empresa.

Observadores y recálculo público

Rol observer / auditor, actas firmadas, raíces ancladas, verificadores en kvoice.org.es. Un interventor no necesita acceso al panel: necesita pruebas.

Cero datos personales en cadena

GDPR / LOPDGDD y derecho al olvido son incompatibles con “cada voto es una transacción con nombre”. Por eso Telos es notario de hashes, no padrón.

Throughput por lote, no por transacción

Un anclaje por minuto agrupa decenas de miles de votos. Postgres indexa (tenant_id, voting_id). La API es stateless con HPA. Así se escala a picos nacionales sin saturar una blockchain con un tx por votante.

ENS, DPIA, auditoría externa

El módulo de sector público incluye evaluación de impacto (DPIA), categorización ENS y criptografía revisable por terceros. Sin eso no hay urna de confianza para una administración.

Voto vinculante regulado

La urna, el censo, los interventores y las pruebas Merkle están pensados para un proceso vinculante. Las elecciones oficiales las convoca el Estado (LOREG / Junta Electoral); KVoice construye la capa técnica que ese proceso necesita.

Quién necesita hacer votar de verdad

El mismo motor. Distinto censo y distinta identidad. Si hoy decidís con un grupo de WhatsApp o un Excel, el recuento no resiste una impugnación.

Comunidades de vecinos

Juntas LPH, derramas, obras, cambio de estatutos, elección de presidente y administrador.

Asociaciones deportivas

Elección de junta, presupuestos, sedes, altas de secciones, sanciones internas.

Federaciones y ligas

Asambleas de clubes, reglamento, calendario, votos ponderados por estamento.

Empresas y grupos

Juntas de accionistas, comités, planes de igualdad, consultas de ERTE, anexiones.

Comités de empresa y sindicatos

Convenios, huelga, afiliación, congresos, delegación de voto.

Cooperativas y sociedades laborales

Asamblea, excedente, admisión de socios, consejo rector.

Colegios profesionales

Decano, presupuestos, códigos deontológicos (abogados, médicos, arquitectos, periodistas).

Universidades y AMPAs

Claustro, consejo de estudiantes, elección de delegados, actividades del centro.

ONGs y fundaciones

Patronato, asamblea de socios, líneas estratégicas, destinos de fondos.

Partidos y primarias internas

Candidaturas, listas, congresos. Identidad fuerte + urna ciega.

Ayuntamientos

Consultas vecinales, presupuestos participativos, consejos de barrio, elección de órganos locales.

Mancomunidades y diputaciones

Acuerdos entre municipios, planes comarcales, elección de órganos compartidos.

Comunidades autónomas y Estado

Consultas ciudadanas y procesos electorales de escala autonómica y estatal: mismo motor, censo e identidad públicos.

Comunidades de regantes

Turnos de agua, derramas de canal, junta de gobierno.

Juntas de compensación urbanística

Proyectos de reparcelación, derramas, representantes ante el ayuntamiento.

Comunidades de centros comerciales

Gastos comunes, horarios, obras, elección de gerente.

Mutualidades y colegios de huérfanos

Prestaciones, consejo, cambios estatutarios con censo cerrado.

Cámaras de comercio

Plenos, comités, elección de presidente y vocales.

Cofradías, hermandades, casals

Mayordomía, presupuestos de fiesta, admisión de hermanos.

Consejos de residentes / living

Residencias de mayores, colivings, campus: normas y presupuestos comunes.

Startups y consejos de administración

Pactos de socios, ampliación de capital, nombramiento de consejeros.

Comunidades de propietarios de naves

Polígonos industriales: vigilancia, accesos, derramas de urbanización.

Hospitales y comités de ética

Protocolos internos, elección de jefaturas, comisiones clínicas.

Medios y cooperativas de periodistas

Línea editorial, consejo, admisión de socios de la cooperativa.

Una votación, cuatro estados

Así queda escrito un proceso certificado: censo, urna, recibo y anclaje.

Ejemplo: ¿Aprobar el presupuesto comunitario 2026?

✓ eligible_count_at_open=28 · quorum min_percent_eligible=50 · custodios 3/5 · election_pk anclada · estado=draft→open

Si tu decisión tiene que aguantar una impugnación, no uses un chat.

Gratis para comunidades pequeñas. Business para SSO, API y procesos grandes. El Trust Layer es el mismo.