ADR-030: La constancia de «firma inobtenible» es prueba, no una anotación¶
Estado: Aceptado
Fecha: 2026-10-09
Autores: Giampiero (mantenedor principal); remediación de los dictámenes de archival-compliance-auditor y tenant-security-auditor
Relacionados: ADR-016 (firma electrónica), ADR-023 (WORM), ADR-010 (auditoría inmutable)
Contexto¶
El acto «firma inobtenible» (AREA-07-10, migración 059) permite declarar que una parte del acta
de entrega (elabora, entrega, recibe) no puede firmar. Su primera versión dejaba una fila
inmutable en la base y un asiento en audit_log. Los dos auditores la devolvieron con un hallazgo
alto y varios medios:
- La constancia no estaba firmada, no tenía Object-Lock y su soporte era un texto libre
(
referencia_documental): ante el AGN era una afirmación de la entidad sobre sí misma, no una prueba de por qué el acta carece de una firma. - Solo se impedía que declarara la parte afectada; quien envió, recibió o elaboró la transferencia podía declarar la excepción de otra parte.
- Dos partes con excepción y ninguna firma se reportaban
firmada_con_excepcion: un acta con cero firmas personales decía «firmada». - No había salida si la excepción se declaraba por error.
- El
fundamento(puede aludir a salud o fallecimiento) se devolvía completo a cualquier lector del acta. firma_personal_pendienteleía la política XAdES del entorno, no la de la entidad.
Decisión¶
- La constancia es un XML canónico (
urn:orpycamcp:acta-firma-excepcion:v1) que cubre la huella del acta congelada, la transferencia, el rol, la parte, el motivo, la huella del fundamento (nunca el texto: el artefacto se replica y se conserva para siempre), el radicado/anexo de soporte, el declarante y el instante. Se firma con el sello institucional XAdES (nivel T solo si la TSA está configurada y responde; el nivel efectivo queda enxades_levely la verificación lo advierte) por el mismo camino que el acta (objeto_tipo=acta_transferencia) y, además, con la firma personal XAdES del declarante cuando envíatotp_codey tiene credencial activa; es obligatoria si la política de la entidad (firma_personal_require_xades, parámetro de tenant) estodos, oclasificadoscon expediente de nivel >= 2. - Sin sello no hay constancia: la firma se obtiene antes de escribir y, si no hay sello, el acto responde 503 sin dejar nada. Una constancia «pendiente de firma» es justo lo que no sirve.
- WORM con la retención del acta: seis
tipoen storage (acta_firma_excepcion_<rol>yacta_firma_excepcion_personal_<rol>, instrumentos de control), bookkeeping en la misma transacción de la constancia, protección post-commit best-effort con reconciliación y renovación CT (mismo patrón que las firmas personales,057/060). - El soporte es un radicado (y opcionalmente un anexo) existente y visible para el declarante, resuelto por el servidor contra document-service con el token interno y el clearance del declarante (422 único para «no existe» y «no lo puede ver»; 503 si document-service no responde).
- Conflicto de interés: ni quien declara ni quien revoca puede ser
enviada_por,decidida_pornicreada_porde la transferencia (403declarante_es_parte_de_la_transferencia, con asiento de denegación). Lo repite un trigger en el motor. - Etiqueta honesta:
firmada_con_excepcionexige que al menos una de las dos partes haya firmado personalmente; si ninguna firmó essellada_con_excepcion. Se eligió esta etiqueta y no rechazar la segunda excepción porque, si las dos partes no pueden firmar, el acta no puede quedar sin salida. - Revocar es una fila nueva append-only (
..._excepcion_revocacion), con asiento atómico, nivel 3 y sin poder ser parte. El trigger de exclusión mutua firma<->excepción solo cuenta excepciones no revocadas: la parte puede volver a firmar. Una excepción revocada no se re-declara (la constancia y su revocación son un par de evidencia cerrado y el artefacto WORM es singleton por(expediente, tipo)). - El fundamento completo solo sale a
USUA_PERM_ADMINnivel >= 3; el resto ve motivo, referencia yfundamento_sha256. - La política XAdES es la de la entidad:
/acta/verificacionleefirma_personal_require_xadesdesgd_param_admin(el entorno solo respalda si la entidad no la definió; un valor corrupto =todos). - Las advertencias de
/verifyde signature (advertencias[]) se propagan a/indice/verifyy a/acta/verificacion.
Consecuencias¶
Positivas: la constancia es verificable por terceros (XAdES-T + huella del acta + Object-Lock; desde ADR-034 — antes de él, la firma XAdES no la verificaba un validador independiente) y no depende de que la base se conserve intacta; el texto sensible no vive en artefactos replicados.
Negativas / límites:
- Declarar exige que signature-service esté disponible y que el declarante tenga PERM_FIRMA >= 2
(el sello se estampa a su nombre).
- Las constancias anteriores a la migración 061 quedan sin firma (se declaran así: firmada=false,
constancias_sin_firma en la verificación, valida=false); no se firman retroactivamente.
- Una excepción declarada por error y revocada no admite una segunda declaración para ese rol.
Addendum FIRMA-2 (2026-10-09) — cadena de constancias, revocación como prueba¶
Sustituye el punto 7 en lo que dice «una excepción revocada no se re-declara».
- Re-declaración encadenada. Tras revocar se puede declarar de nuevo para el mismo rol: la nueva
constancia lleva
ordinal = n+1ysustituye_a= la revocada (UNIQUE (transferencia_id, rol, ordinal)yUNIQUE (sustituye_a): cadena lineal). Solo si la revocación anterior está sellada y todos sus artefactos protegidos WORM; si no, 409revocacion_sin_proteger. Si la parte firmó durante la ventana revocada, la cadena se cierra (409parte_ya_firmo): la firma prevalece. Lo repite el trigger. - La revocación es prueba: XML canónico (
urn:orpycamcp:acta-firma-excepcion-revocacion:v1) ligado a la huella de la constancia revocada, sello institucional y firma personal de quien revoca si tiene certificado activo; WORM con su propia reconciliación y renovación. Sin sello: 503 sin asiento. - Firma personal siempre que haya certificado activo (declarar y revocar), no solo por política.
- Constancia v2 autosuficiente (
urn:orpycamcp:acta-firma-excepcion:v2): número de radicado y SHA-256 del anexo de soporte (anexo obligatorio), nombre y cargo de declarante y parte como instantánea resuelta por el servidor. Datos personales de una persona afectada (fallecimiento, incapacidad) quedan en un artefacto WORM perpetuo: es deliberado (la constancia debe identificar a quién no pudo firmar) y su lectura se limita a quien lee el acta. - El fundamento se huella con HMAC-SHA256 por entidad, clave derivada por HKDF en signature-service
(propósito
fundamento_excepcion, sin secreto nuevo); un SHA-256 plano de un texto corto es atacable por diccionario. Se verifica recalculando contra la columna, el XML y el asiento. - Las constancias legadas (pre-061) son ordinal 1 sin firma; se revocan (revocación firmada) y encadenan.
Addendum FIRMA-2, ronda de auditoría (2026-10-09) — verificable por terceros, canónico, anclado¶
Corrige el punto 5 del addendum anterior y completa los puntos 1-2 tras la revisión de
archival-compliance-auditor y tenant-security-auditor. Migración archive 063.
- El fundamento se huella con un compromiso con sal por registro (sustituye al HMAC con clave de
plataforma).
compromiso = SHA-256(sal_32B || texto_utf8), sal aleatoria (secrets.token_bytes(32)) por registro, guardada en la base junto al fundamento (columnafundamento_sal, no WORM). El compromiso va en la columna, en el XML (<fundamentoCompromiso algoritmo="SHA-256-salted">) y en el asiento; la sal nunca va aaudit_log, al XML ni a una respuesta que no sea de administración nivel 3. Razón: el HMAC con clave raíz de la plataforma no lo puede verificar un tercero (el AGN) sin entregarle la clave de toda la entidad, y renombrar el tenant o perder el secreto raíz lo rompía; con sal por registro, la entidad entrega texto + sal de un registro y cualquiera recalcula consha256sum. Sin sal el compromiso no se ataca por diccionario. La verificación del fundamento (administración nivel 3) recalcula localmente contra columna, XML y asiento; signature-service deja de intervenir. - Versionado (decisión). Constancia v3 (
urn:orpycamcp:acta-firma-excepcion:v3) y revocación v3 (...-revocacion:v3, concausa). Las filas v2 existentes se escribieron con HMAC y la v2 nunca salió de desarrollo (datos de prueba): se conservan como legadas (columnasfundamento_hmac*de la 062 intactas, la tabla es append-only), no se reescriben ni se verifican por compromiso, y su verificación respondeverificable=false(constancia_sin_compromiso_salado). Los CHECK v2 de la 063 exigen HMAC para v2 y compromiso+sal (32 bytes)+algoritmo para v3. Los endpointshmac-fundamento[/verificar]y los 503huella_fundamento_no_disponibledesaparecen. - Canonicalización. Lo que se firma, se guarda y se huella es la C14N exclusiva
(
http://www.w3.org/2001/10/xml-exc-c14n#, lxml) del XML sin firmar;constancia_sha256/revocacion_sha256son el SHA-256 de esos bytes y el XAdES firma ese mismo contenido. Un tercero que reciba el XML firmado, le quite lads:Signatureenvolvente y lo canonicalice obtiene la misma huella.GET /acta/verificacioncoteja los enlaces de la cadena —huella guardada vs. XML, contenido de cada artefacto firmado vs. huella,sustituyeA/revocacionSha256vs. el eslabón anterior,constanciaSha256/causa/compromiso de la revocación vs. la constancia— y reporta el estado WORM de cada eslabón; un enlace roto fuerzavalida=falsecon su motivo (cadena_motivos,enlace_motivo). - Ancla común de retención. Todos los eslabones de la cadena de un (transferencia, rol) se protegen con el mayor entre el sellado del acta y el último acto de la cadena (no con la fecha de cada acto), y al protegerse un eslabón nuevo se alinean (renovación monotónica, con asiento) los anteriores ya protegidos. Así el eslabón 1 no caduca antes que su revocación ni que la constancia que lo sustituye.
- Salida de
agotado.POST .../acta/firma-inobtenible/{excepcion_id}/worm/reintentar(constancia) y.../{excepcion_id}/revocacion/worm/reintentar(revocación): administración nivel 3, repone a pendiente los artefactos agotados (protección o renovación) con asiento atómico y reintenta la protección; devuelve el estado real. En desarrollo, con la preservación apagada, seguirá fallando: es lo correcto. - Privacidad (Ley 1581 de 2012).
incapacidades un dato de salud (art. 5, dato sensible): dentro del XML y del asiento se escribe el término neutroimpedimento_sobreviniente; la categoría exacta queda solo en la base. Base legal del tratamiento: art. 10 lit. a (información requerida por una entidad pública en ejercicio de sus funciones legales), con la finalidad de dejar constancia de por qué una parte no pudo firmar el acta (Ley 594/2000 art. 11; Ac. AGN 003/2015). Clasificación Ley 1712 de 2014: información clasificada por protección de datos de salud (art. 18 lit. a). Estos artefactos (constancia, revocación, fundamento, sal) están excluidos de OAI-PMH, EAD y CMIS: ningún módulo de interoperabilidad los lee (lo fijatests/test_firma2_ronda_auditoria.py) y el XML nunca se sirve por archive-service. - Revocación con
causade vocabulario cerrado (error_de_declaracion,parte_puede_firmar,otro), obligatoria en el cuerpo y dentro del XML firmado; el detalle libre sigue enfundamento. - Trazabilidad de la lectura. Leer el fundamento en claro (
GET /acta,GET /acta/verificacionpara administración nivel 3, y la verificación del fundamento) deja el asientotransferencia.acta_fundamento_leido(quién, qué constancias, por qué puerta; sin texto ni sal), antes de servirlo: sin asiento no hay lectura. El asiento de declarar ya no lleva el número del radicado de soporte (quedan el id y la huella del anexo). - Salvedades declaradas.
- El nombre con
@fuente="usuario"(sin certificado de firma) es el username del catálogo: no identifica a una persona natural; solo el CN de un certificado activo (fuente="certificado") lo hace. - El anexo de soporte es un anexo ordinario y puede eliminarse por su TRD; la constancia conserva su id y su SHA-256. Propuesta futura: bloqueo de disposición mientras exista una constancia vigente que lo cite.
- Una revocación legada (anterior a la 062) sigue definitiva. Una «regularización» que permita re-declarar sobre ella queda como propuesta (roadmap), no implementada.
Addendum FIRMA-2, ronda 3 (2026-10-09) — sobre de revelación del fundamento¶
Cierra la reserva Media de archival-compliance-auditor: el texto del fundamento y su sal vivían
solo en la base (tabla append-only, sin Object-Lock, fuera del AIP y de la transferencia). Perder la
base perdía el fundamento entero, no solo la posibilidad de verificarlo. Migraciones archive 064 y
storage 021.
- Sobre de revelación. Por cada constancia v3 y cada revocación v3, archive-service genera un XML
urn:orpycamcp:acta-firma-excepcion-sobre:v1con{id, acto, texto, salHex, compromiso, algoritmo}(C14N exclusiva;acto=constancia|revocacion,id= el del acto), lo sella con el sello institucional (XAdES-T) y lo guarda como objeto propio en el almacén. En la fila del acto quedan solosobre_firma_id,sobre_minio_key,sobre_sha256ysobre_xades_level(el XML del sobre no se duplica en la base). Quien reciba el sobre recalculaSHA-256(sal || texto)y lo coteja con elfundamentoCompromisodel XML principal. - Mismo flujo, antes de la transacción. Se sella justo después del sello de la constancia y antes
de consumir el TOTP de la firma personal. Si el sello falla: 503
sellado_sobre_no_obtenido(osello_institucional_no_disponible) y nada queda escrito (ni fila ni asiento). Un carácter de control no admitido por XML 1.0 en el fundamento responde 422fundamento_caracteres_invalidos(el sobre se construye con lxml para conservar un\rinterno como
: el compromiso cubre los bytes exactos). - Object-Lock con la retención de la cadena. Tercer
artefacto(sobre) de las tablastransferencia_acta_firma_excepcion[_revocacion]_worm(CHECK ampliado en la064): mismo ciclo que sello y firma personal —protección inicial post-commit best-effort, reconciliación con backoff, renovación CT,.../worm/reintentar— y misma ancla común de la cadena del rol (_ancla_cadena/_alinear_cadena). En storage son seistiponuevos (acta_firma_{excepcion,revocacion}_sobre_<rol>), instrumentos de control y multi-artefacto por(expediente, tipo, file_id). Para re-declarar tras revocar, el trigger y el servicio exigen además el sobre de la revocación protegido. - Acceso solo mediado.
GET /transferencias/{id}/acta/firma-inobtenible/{excepcion_id}/sobre?acto=constancia|revocacion: administración nivel 3, 404 neutro por no-read-up del expediente, y el asientotransferencia.acta_fundamento_leidoconvia="sobre"se escribe antes de servirlo (sin asiento no hay lectura). Nunca por OAI-PMH, EAD, CMIS ni la verificación pública;GET /actay/acta/verificacionsolo exponen metadatos (sobre,sobre_sha256,worm_sobre_protegido). La verificación del fundamento añade el sobre como cuarta copia (sobre,revocacion_sobre): si existe y no coincide o no se pudo leer,coincide=false. - Cómo viaja en la transferencia secundaria al AGN. El sobre es un componente clasificado (Ley 1712
de 2014, art. 18 lit. a: datos de salud / fallecimiento): se entrega junto a la constancia y la
revocación, en el mismo paquete, con nota en el FUID y en el acta de entrega que lo declare clasificado,
acceso restringido a la entidad receptora; el AGN recibe texto y sal de ese registro (no la clave
de toda la entidad) y puede verificar el compromiso con
sha256sum. El empaquetado del sobre en el paquete de transferencia secundaria es un acto del procedimiento de entrega (la lectura mediada del punto 4 deja el asiento); no se publica ni se indexa. - La copia de respaldo de la base NO sustituye al sobre. Es una copia operativa, rotativa y sin
Object-Lock ni sello; el sobre es la copia de conservación, inmutable y verificable. Las
constancias y revocaciones v2/legadas y las v3 anteriores a la
064quedan sin sobre (columnasNULL; la vista lo dice consobre=false): no se sellan retroactivamente. - Revocar ya no devuelve el fundamento ajeno. La respuesta de
POST .../revocardevolvía elfundamentoy la sal de la constancia original (texto de otro declarante) sin asiento de lectura. Ahora los devuelvenull; la vista completa queda enGET /acta, que sí asienta. (Elfundamentode la propia revocación, que acaba de escribir quien revoca, se mantiene.) - Orden y huérfanos. El sobre se sube DESPUÉS de validar la firma personal (un TOTP inválido no deja
copias del texto). Si la fila no llega a referenciarlo, archive lo purga por
/internal/purgar-huerfanosde storage (que deja su asientofile.purgado_huerfano); si la purga falla, un barrido de reconciliación (misma barrida que el WORM de la constancia) recoge los ficherosacta-transferencia-sobre-…sin referencia ensobre_minio_keypasada una gracia (SOBRE_HUERFANO_GRACIA_HORAS, 1 h). - Lectura a petición.
GET /actay/acta/verificacionya no devuelven el texto ni la sal (ni a admin nivel 3):GET .../{excepcion_id}/fundamento?acto=lo sirve con asientovia=lectura. La sal solo viaja dentro del sobre.
Addendum B4 (2026-10-09) — punto 5: el paquete de entrega existe¶
El punto 5 decía que el sobre viaja «junto a» la constancia en la transferencia secundaria, pero no había paquete que lo llevara: el AIP se sella al cerrar el expediente (antes de que existan constancias, revocaciones y sobres, y ya está bajo COMPLIANCE), y el acta FUID congelada es probatoria y no se reescribe.
Decisión. Un paquete de entrega aparte, generado a petición —POST /transferencias/{id}/paquete-entrega—
y reproducible desde componentes que ya están bajo Object-Lock. Solo transferencias secundarias y recibidas;
administración nivel 3; no-read-up en el WHERE (404 neutro).
- Contenido (exacto).
acta/(acta congelada tal cual = el inventario FUID, + sello XAdES + firmas personales),indice/indice-electronico.xades.xml(el índice electrónico firmado de la versión que entregó el acta, bajo Object-Lock),clasificado/(toda la cadena de constancias y revocaciones, también las revocadas, con sus firmas personales y los sobres),nota-entrega.xmlychecksums.txt. El AIP del expediente (BagIt + PREMIS con los documentos) no viaja dentro del ZIP: su volumen lo hace inviable y ya está sellado bajo COMPLIANCE; la nota lo referencia porbucket,object_key,sha256,retain_untilyretention_mode(<aip incluido_en_paquete="no">) y quien recibe lo obtiene del almacén y lo coteja con esa huella. Sin AIP empaquetado con retención confirmada por el almacén, el paquete no se genera. - La «nota en el FUID» es
nota-entrega.xml(urn:orpycamcp:nota-entrega-transferencia:v1). El acta no se edita: la nota la referencia poracta_fuid_sha256y declara cada componente (ruta,sha256,acto,rol,ordinal) comoclasificacion="clasificada",fundamento="Ley 1712/2014 art. 18 lit. a",acceso="restringido a la entidad receptora". Constancias v2/legadas llevansobre="ausente": no se sellan retroactivamente. La nota es determinista (sin hora ni autor, que viven en el asiento). - Fail-closed. Si algún componente (incluidos índice y AIP) no tiene Object-Lock confirmado,
409 componentes_sin_object_locky no se descarga nada; las rutasclasificado/...no se enumeran (se agregan encomponentes_clasificados, para no revelar cuántas excepciones hay). Un entorno sin preservación nunca produce paquete. Fixity de egreso. Antes de empaquetar se recalculasha256(acta_fuid_xml)contraacta_fuid_sha256, el sello del acta debe firmar esa misma acta (C14N-SHA256 sin la firma envolvente) y cada componente se lee como bytes y se coteja con la huella que storage registró al subirlo. Una discrepancia es409 componente_integridad_discrepante+ asientotransferencia.paquete_entrega_incidente; sin huellas de captura,503. Los checksums del ZIP certifican la copia entregada; esta comprobación certifica el original. - Asiento antes de servir.
transferencia.acta_fundamento_leido(via="paquete", por cada constancia cuyo sobre viaja) ytransferencia.paquete_entrega_generado(huella de la nota y de cada componente), en una sola transacción y antes del primer byte. - Exclusión. Ni OAI-PMH, ni EAD, ni CMIS leen las tablas de excepción (guardián por fuente en las pruebas).
Límites declarados. nota-entrega.xml no está sellada con XAdES (tocaría signature-service); su huella queda
anclada en audit_log. El paquete se arma en un fichero temporal (son XML pequeños).
Nota ADR-034 (2026-10-11)¶
La conformidad XMLDSig de la firma (ADR-034) no cambia constancia_sha256 ni huella_de_firmado: son huellas de la aplicación
sobre el XML sin ds:Signature. Cambia solo la ds:Signature que lo sella (con Transforms declarados). Las constancias selladas
antes de ADR-034 conservan sus huellas, pero su sello XAdES no verifica con un validador independiente y se regenera si hace falta.