Perfil Merkle Docuten eArchiving v2

Docuten eArchiving Merkle Profile v2

Metadato Valor
Versión / Version 2
Estado / Status Normativo — vigente para todo root computado desde Phase 05.1.2
URI del perfil / Profile URI https://docs.docuten.com/earchiving/merkle-profile/v2
Dónde se declara / Where it is declared merkleProfile (obligatorio) en aip-hash-attestation-v2.xsd; merkleProfile (aditivo, opcional) en integrity-report-v1.xsd
Implementación / Implementation earchiving-core/src/main/java/com/docuten/earchiving/core/crypto/MerkleTree.java — build(List<Map.Entry<String,String>>)
Función de hash / Hash function SHA-256 únicamente (política criptográfica del proyecto: SHA-384 prohibido para fixity, MD5/SHA-1 prohibidos con valor probatorio)
Decisiones / Decisions D-24, D-25, D-26, D-29, D-49 (05.2-CONTEXT.md) · GATE-02 (Phase 05.1.2)
Sustituye a / Supersedes Perfil v1 (sin prefijos de dominio, nodo impar duplicado) — ver §6
Fecha / Date 2026-08-27

1. Propósito / Purpose

Este documento es la definición normativa del algoritmo con el que Docuten eArchiving computa los Merkle roots que después sella un QTSP y sobre los que se solicita un sello de tiempo cualificado. Es el documento que la URI de la tabla anterior identifica y al que apunta el elemento merkleProfile de la evidencia sellada.

Existe porque, sin él, un peritaje que solo disponga del ZIP del AIP y de su sello no podría recomputar el root: el algoritmo, el orden de las hojas, la concatenación y el tratamiento del nodo impar vivían únicamente en el Javadoc de MerkleTree, que no viaja con el paquete (D-26).

La URI es un identificador, no una URL que deba resolverse. No se dereferencia en tiempo de sellado ni de verificación.

This document is the normative definition of the algorithm Docuten eArchiving uses to compute the Merkle roots that a QTSP later signs and that are submitted for a qualified timestamp. It is the document identified by the URI in the table above and referenced by the merkleProfile element inside the sealed evidence. It exists because otherwise an expert witness holding only the AIP ZIP and its seal could not recompute the root: the algorithm, leaf ordering, concatenation and odd-node handling used to live only in the MerkleTree Javadoc, which does not travel with the package (D-26). The URI is an identifier, not a URL to be dereferenced.


2. Algoritmo / Algorithm

2.1 Entrada / Input

La entrada es una lista ordenada de pares (entryId, entryHashHex):

El orden de la lista es parte de la definición del root: build() la consume posicionalmente. El orden canónico está fijado en §3.

Entradas rechazadas (fail-closed). build() lanza IllegalArgumentException antes de construir cualquier nivel si:

  1. la lista es null o vacía;
  2. algún entryHashHex es null o no tiene exactamente 64 caracteres;
  3. dos entradas comparten el mismo entryId.

La validación es previa por una razón sustantiva, no de estilo: una entrada mal formada o un entryId repetido contamina un root que después se sella, y una evidencia ya emitida no se puede desellar.

The input is an ordered list of (entryId, entryHashHex) pairs, where entryId is the full path of the file inside the AIP ZIP (never the base name) and entryHashHex is the entry's SHA-256 in lowercase hex, exactly 64 characters. List order is part of the root's definition — see §3. build() throws before building any level if the list is null/empty, if any entryHashHex is null or not exactly 64 characters, or if two entries share the same entryId: a malformed entry contaminates a root that will be sealed, and issued evidence cannot be unsealed.

2.2 Nivel hoja / Leaf level

Para cada entrada, en el orden de la lista:

entryHashBytes = hexDecode(entryHashHex)          // 32 bytes
leafHash       = SHA-256( 0x00 || entryHashBytes ) // 32 bytes

0x00 es un único byte anteponido a los 32 bytes del hash de la entrada. El nivel 0 del árbol es la secuencia de leafHash, en el mismo orden que la entrada.

Nótese que el hash de hoja es un hash del hash de la entrada, no del contenido del fichero: el perfil opera sobre entryHashHex, que el llamador ya computó.

2.3 Nodos interiores / Interior nodes

parentHash = SHA-256( 0x01 || leftChild || rightChild )   // 1 + 32 + 32 bytes

leftChild y rightChild son los 32 bytes crudos de los hijos (no su representación hex).

2.4 Construcción bottom-up y nodo impar / Bottom-up construction and the odd node

Partiendo del nivel 0, se repite mientras el nivel tenga más de un nodo:

mientras longitud(nivel) > 1:
    impar      = longitud(nivel) es impar
    emparejados = impar ? longitud(nivel) - 1 : longitud(nivel)

    siguiente = []
    para i = 0, 2, 4, …, emparejados - 2:
        siguiente.añadir( SHA-256( 0x01 || nivel[i] || nivel[i+1] ) )

    si impar:
        siguiente.añadir( nivel[longitud(nivel) - 1] )    # PROMOCIÓN, sin re-hashear

    nivel = siguiente

rootHash = hexEncode(nivel[0])                            # 64 caracteres hex minúscula

Promoción del nodo impar. Cuando un nivel tiene un número impar de nodos, el último nodo pasa tal cual al nivel siguiente: no se re-hashea, no se empareja consigo mismo y no se duplica.

Esta regla es el corazón del perfil. El perfil v1 rellenaba el nivel impar duplicando el último nodo, que es el patrón CVE-2012-2459: con ella, dos conjuntos de hojas de cardinalidad distinta producen el MISMO root. La forma exacta del ataque: un conjunto de 6 hojas [e1, e2, e3, e4, e5, e6] y el conjunto de 8 que repite sus dos últimas [e1, e2, e3, e4, e5, e6, e5, e6] devuelven, bajo v1, el mismo root — de modo que el root sellado no distinguía un AIP de 6 ficheros de uno de 8 con la cola repetida. Bajo v2 los dos roots son distintos por construcción, porque el nodo impar nunca se empareja consigo mismo.

La propiedad no depende de esta prosa: la fija de forma permanente la aserción unitaria MerkleTreeTest.oddNodeDuplication_v1Shape_collidesAcrossDifferentLeafSets (D-29), que mide la colisión del perfil viejo y su ausencia en el vigente sin pasar por ningún servicio.

Starting from level 0, while a level has more than one node: pair adjacent nodes into SHA-256(0x01 || left || right); if the level has an odd number of nodes, the last node is promoted verbatim to the next level — not re-hashed, not paired with itself, not duplicated. This rule is the core of the profile: v1 padded odd levels by duplicating the last node, which is the CVE-2012-2459 pattern, making two leaf sets of different cardinality collide on the same root. The property is pinned by the permanent unit assertion MerkleTreeTest.oddNodeDuplication_v1Shape_collidesAcrossDifferentLeafSets (D-29).

2.5 Caso de una sola hoja / Single-leaf case

Con una sola entrada el bucle no se ejecuta y rootHash = hexEncode(leafHash), es decir SHA-256(0x00 || entryHashBytes). El root de un árbol de una hoja sí lleva el prefijo de hoja.

2.6 Pruebas de inclusión / Inclusion proofs

Cada hoja tiene un camino de hermanos de hoja a raíz. Cada entrada del camino lleva el prefijo "L:" o "R:" según el hermano esté a la izquierda o a la derecha del nodo actual, seguido del hash del hermano en hex.

Las hojas que cuelgan de un nodo promovido no reciben ninguna entrada en el nivel donde se produjo la promoción: su camino tiene simplemente una entrada menos. La verificación aplica la misma función de nodo interior (SHA-256(0x01 || …)) y por tanto no puede divergir del escritor en el prefijo — ambos pasan por el mismo helper.

Los caminos de prueba no se persisten del lado AIP: se recomputan. Del lado del audit log el árbol completo sí queda en audit_log_seals.merkle_tree.

Matiz que esta sección no puede omitir (§6.1). Que el árbol completo quede persistido en merkle_tree no significa que quede sellado: el MessageImprint de la TSA del audit log cubre solo los 32 bytes de rootHash, así que hojas, hermanos y el propio merkleProfile viajan CON la evidencia pero FUERA del token. El orden canónico de esas hojas es el ascendente por seq, no LEAF_NAME_ORDER (§3.1).


3. Orden canónico de las hojas / Canonical leaf ordering

El orden de las hojas es parte de la definición del root, así que quien sella y quien verifica deben ordenar igual. El orden lo define un único punto de verdad en el código, MerkleTree.LEAF_NAME_ORDER, cuya expresión es textualmente:

Comparator.comparing((String name) -> name.toLowerCase(Locale.ROOT))
          .thenComparing(Comparator.naturalOrder())

Es decir: comparación primaria por el nombre plegado a minúsculas con Locale.ROOT, y desempate por orden natural (lexicográfico por punto de código UTF-16) del nombre sin plegar.

Los dos detalles son normativos y ninguno es decorativo:

Leaf order is part of the root's definition, so writer and verifier must sort identically. The single point of truth is MerkleTree.LEAF_NAME_ORDER, quoted verbatim above: primary comparison on the Locale.ROOT-folded name, tie-broken by the natural order of the unfolded name. Locale.ROOT is mandatory (locale-dependent folding would make the sealed root a function of the runtime environment), and the tie-breaker is mandatory (without it the comparator is not total and ZIP enumeration order would decide).

3.1 Alcance de LEAF_NAME_ORDER: es el orden del sello #2 del AIP, NO el del audit log

LEAF_NAME_ORDER es normativo únicamente para el escritor y el verificador del sello #2 del AIP, cuyos entryId son rutas de fichero dentro del ZIP — exactamente el alfabeto para el que el comparador está diseñado (ver el Javadoc de MerkleTree.LEAF_NAME_ORDER: plegado de mayúsculas, desempate por orden natural, colisiones a.txt/A.txt).

Para audit_log_seals, el segundo consumidor del perfil (D-49), el orden canónico es el ASCENDENTE por seq. Ahí el entryId de cada hoja es el UUID de una entrada de auditoría, y sobre ese alfabeto LEAF_NAME_ORDER no ordena nada con significado: un UUID no tiene semántica lexicográfica, así que ordenar por él sería tan arbitrario como ordenar por el hash. El orden por seq, en cambio, sí es canónico y reproducible por un tercero sin acceso al código: es el que fija el propio rango sellado (seq_from/seq_to de la fila) y el que ya impone la cadena hash del audit_log.

AuditLogSealSchedulerImpl no aplica ni debe aplicar LEAF_NAME_ORDER a las hojas del audit log: consume el resultado de findByTenantIdAndSeqBetweenOrderBySeq sin reordenar. Cambiar ese orden alteraría en silencio el root de todos los sellos v2 ya emitidos, sin que ninguna migración lo declare.

Esta acotación tiene aserción permanente, no solo prosa. La fija AuditLogSealMerkleProfileTest#auditLogLeafOrder_isAscendingBySeq_notLexicographicByEntryId_pinnedForSpecSection3, cuyo lote está construido para que los entryId estén en orden lexicográfico exactamente INVERSO al de sus seq — de modo que los dos órdenes son distinguibles y el test mide algo. Vive fuera del servicio de verificación, así que un cambio de orden en el scheduler la rompe de inmediato en vez de divergir en silencio de esta sección (que es lo que ocurrió hasta el gap-closure 05.1.2-23: esta sección declaraba LEAF_NAME_ORDER como canónico sin acotar y §2.6 metía el audit log en el alcance, mientras el código ordenaba por seq — WR-04).

LEAF_NAME_ORDER is normative only for seal #2 of the AIP, whose entryIds are ZIP file paths — the alphabet the comparator was designed for. For audit_log_seals (the second consumer, D-49) the canonical order is ascending seq: there the entryId is an audit-entry UUID, over which LEAF_NAME_ORDER orders nothing meaningful, whereas seq is canonical, reproducible from the sealed range (seq_from/seq_to) itself and already imposed by the audit_log hash chain. AuditLogSealSchedulerImpl does not and must not apply LEAF_NAME_ORDER to audit-log leaves; changing that order would silently change the root of every v2 seal already issued. This scoping is pinned by a permanent assertion outside the verification service, not by prose alone.


4. Ámbito: sin exclusiones / Scope: no exclusions

El sello #2 cubre el AIP completo tal como queda en custodia DESPUÉS del merge del paso 10 de la saga de ingesta, sin excluir ningún fichero: contenido, METS.xml, premis.xml y los ficheros de metadata/preservation/ generados por la propia atestación de origen (D-26 de 05.1-CONTEXT.md, reafirmado por D-26 de 05.2-CONTEXT.md).

La consecuencia práctica, que un verificador debe respetar: la lista de hojas es la lista de todos los ficheros del ZIP. Recomputar el root excluyendo, por ejemplo, los artefactos de atestación produce un valor distinto y un FAIL falso.

El sello #1 (origin-attestation.xml) es un artefacto distinto y no puede cubrir los ficheros de atestación que él mismo genera; esa asimetría es de diseño, no una exclusión de este perfil.

Seal #2 covers the entire AIP as it stands after the ingest saga's step-10 merge, with no file excluded — content, METS.xml, premis.xml and the metadata/preservation/ files produced by origin attestation itself. A verifier that recomputes the root while excluding, say, the attestation artefacts will get a different value and a false FAIL.


5. Relación con RFC 6962 / Relationship to RFC 6962

Este perfil NO es un renderizado literal de la función MTH() de RFC 6962 §2.1 — esa función particiona recursivamente en la mayor potencia de dos y no tiene concepto de «nodo impar promovido». Este perfil es un HÍBRIDO: conserva la estructura bottom-up existente (necesaria para verifyInclusion y sus siblingPaths incrementales) y toma de RFC 6962 únicamente los BYTES de domain separation (0x00/0x01). Un tercero que intente recomputar el root aplicando la fórmula recursiva de RFC 6962 al pie de la letra obtendrá un valor DISTINTO.

Para que la afirmación sea comprobable, la fórmula de la RFC, literal:

La diferencia es estructural, no de notación: la RFC parte la lista completa en [0:k] y [k:n] con k potencia de dos, de modo que nunca necesita rellenar un nivel; este perfil empareja nivel a nivel y promueve el sobrante. Ambas construcciones coinciden cuando el número de hojas es una potencia de dos, y divergen en cualquier otro caso.

Lo que sí se toma de la RFC —y lo único— son los prefijos de dominio, que dan resistencia a segunda preimagen entre niveles: con ellos, un hash de nodo interior no puede hacerse pasar por un hash de hoja ni al revés.

Ninguna afirmación de este documento, del Javadoc de MerkleTree ni de ningún otro artefacto de gobernanza debe decir «conforme a RFC 6962» sin este matiz.

The bold paragraph above is normative: this profile is not a literal rendering of RFC 6962 §2.1's MTH(). The RFC splits the full list at the largest power of two and never pads; this profile pairs level by level and promotes the leftover node. The two agree only when the leaf count is a power of two. Only the domain-separation bytes 0x00/0x01 are taken from the RFC. No statement in this document, in the MerkleTree Javadoc or in any other governance artefact may claim "RFC 6962 conformance" without this qualification.


6. Coexistencia v1/v2 / v1–v2 coexistence

Perfil v1 (legacy) Perfil v2 (vigente)
Hoja SHA-256(entryHashBytes) — sin prefijo SHA-256(0x00 \|\| entryHashBytes)
Nodo interior SHA-256(left \|\| right) — sin prefijo SHA-256(0x01 \|\| left \|\| right)
Nivel impar último nodo duplicado (CVE-2012-2459) último nodo promovido sin re-hashear
Guardas de entrada ninguna entryHashHex de 64 hex, entryId único
Declarado en la evidencia no sí, en los DOS consumidores — sidecar del AIP (merkleProfile del XSD v2) y audit_log_seals (MerkleTree.merkleProfile, gap-closure 05.1.2-23). Declarado ≠ sellado: ver §6.1

MerkleTree.build() es una única implementación estática, sin dispatch por versión. No existe forma de computar un root v1 con el código vigente: el perfil v1 solo se reconstruye dentro de MerkleTreeTest como referencia permanente del defecto. Por tanto todo root computado desde Phase 05.1.2 —AIP y audit log, los dos únicos consumidores— es v2.

En el lado de la serialización, en cambio, sí hay coexistencia:

Condición de cierre de la ventana (D-25 de 05.2-CONTEXT.md, matiz D-49). El cambio de perfil fue gratuito porque el corpus sellado era local y recomputable. La ventana la cierra el primer entorno inmutable o productivo, no la ausencia de atestación externa: pueden existir tokens TSA reales emitidos por un QTSP sobre roots v1 procedentes de ejecuciones locales, y eso no cierra la ventana. Cuando se confirme que ningún sidecar v1 vivo necesita seguir siendo legible, v1 podrá eliminarse y el dispatch del lector simplificarse.

6.1 Declarado ≠ sellado: dónde viaja el perfil en cada consumidor

La fila «Declarado en la evidencia» dice que los dos consumidores declaran el perfil. No dice que en los dos quede cubierto por el sello, porque no es cierto, y la diferencia es exactamente la que un peritaje necesita saber:

Consumidor ¿Declara el perfil? ¿Queda el perfil DENTRO de lo sellado? Mecanismo
AIP, sello #2 (sidecar aip-hash-attestation-v2.xsd) sí sí El perfil se puebla en buildAipHashAttestationType, es decir ANTES de canonicalize(), así que entra en lo que el QTSP firma y la TSA sella. No existe un segundo punto de mutación posterior
audit_log_seals (sellado por lotes del audit log) sí NO El MessageImprint que AuditLogSealSchedulerImpl.sealTenant(UUID) envía a la TSA son EXCLUSIVAMENTE los 32 bytes de rootHash (parseHex(tree.rootHash()) → tsaPort.timestamp(rootBytes)). El JSONB merkle_tree, que es donde viaja merkleProfile, queda FUERA del token
audit_log_seals desde la Fase 05.6 (Plan 05.6-04) sí sí AuditLogSealSchedulerImpl.sealTenant(UUID) envía a la TSA SHA-256(JCS(forma canónica del árbol)) —root, hojas ordenadas por entryId, caminos de hermanos y merkleProfile—, calculado por MerkleTreeCanonicalForm.sealImprint con JcsCanonicalizer. AuditQueryService.verifySeal recomputa ese imprint desde merkle_tree y lo compara con el del token. Ver la nota de cierre al final de esta sección. Sustituida por la fila siguiente desde la Fase 06.2
audit_log_seals desde la Fase 06.2 (PRE-02, planes 06.2-03 y 06.2-15) sí sí El MessageImprint es SHA-256(JCS(sobre)), con el sobre de siete campos de SealEnvelope.imprint (earchiving-core): sealKind (BATCH | HEARTBEAT), tenantId, seqFrom, seqTo, lastEntryHash, prevSealHash y tree —el árbol de la fila anterior (root, hojas ordenadas por entryId, caminos de hermanos y merkleProfile) anidado, o null en un latido—. prevSealHash = SHA-256 de los bytes tal cual del token del sello anterior (génesis SHA-256("")), así que el encadenado de sellos queda dentro de lo sellado y no solo en una columna. Sigue habiendo una sola fórmula (D-14 de 05.6), que usan el sellado (AuditLogSealSchedulerImpl.sealTenant), AuditQueryService.verifySeal y el verificador de cadena completa (AuditChainVerifier.verifyTenant). Los números del sobre y de las entradas se canonicalizan según RFC 8785 desde 06.2-15 (D-31), con rechazo de todo valor entero de magnitud mayor que 2^53. Ver la segunda nota de cierre

Consecuencia, dicha sin rodeos. Para audit_log_seals el perfil viaja CON el dato, no DENTRO de la evidencia sellada: quien pueda alterar la fila puede alterar el perfil declarado sin invalidar el token TSA. La declaración cierra el problema de legibilidad (un auditor con solo la BD puede saber bajo qué reglas se computó el root sin abrir MerkleTree.java); no cierra el de vinculación criptográfica. Ninguna afirmación de conformidad debe apoyarse en este campo como si estuviera sellado.

La mitad abierta tiene dueño, no es un hueco silencioso. Está registrada como DEF-08 en §6 de docs/governance/gate-chains-inventory.md, con ítem completo en el deferred-items.md de la fase 05.1.2 y destinatario QTSP-02 del ledger de requisitos — una fila que existe HOY y que sobrevive al cierre de la fase. Ruta preferida para cerrarla: sellar el árbol canonicalizado con JCS (mismo patrón canonicalize() → sellar que ya está validado en la ruta del AIP, y que además protege hojas y hermanos, no solo el root). Plan B si la canonicalización JCS no se implementa: MessageImprint sobre SHA-256(rootHash ‖ perfil). Hoy ningún consumidor de src/main recomputa ese imprint —el token solo se produce, se persiste y se expone en hex—, así que ampliar lo que cubre no rompe ningún lector existente. La medida ejecutable del límite es AuditLogSealMerkleProfileTest#persistedEvidence_declaresMerkleProfile_butTheTsaImprintCoversOnlyTheRoot_def08.

Both consumers declare the profile; only one seals it. On the AIP path the profile is populated before canonicalize(), so it is inside what the QTSP signs and the TSA timestamps. On the audit_log_seals path the TSA MessageImprint covers only the 32 bytes of rootHash, so the JSONB merkle_tree carrying merkleProfile is outside the seal: the profile travels with the data, not inside the sealed evidence, and whoever can alter the row can alter the declared profile without invalidating the token. That open half is tracked as DEF-08 with recipient QTSP-02; preferred fix is to seal the JCS-canonicalized tree, fallback SHA-256(rootHash ‖ profile). No conformance claim may rest on this field as if it were sealed.

Nota de cierre (Fase 05.6, Plan 05.6-04, 2026-09-24): para audit_log_seals el perfil AHORA SÍ está sellado, no solo declarado. La fila histórica de la tabla, los tres párrafos anteriores y el resumen en inglés describen el estado que dejó la fase 05.1.2 y se conservan a propósito: son el registro de qué cerró el gap-closure original de GATE-02 (la legibilidad del perfil) y qué dejó abierto (la vinculación criptográfica). Desde 05.6 se cerró por la ruta preferida que describen (D-13..D-16 de 05.6-CONTEXT.md):

Lo que el cierre NO cambia, dicho igual de claro:

Closure note (Phase 05.6, Plan 05.6-04): the audit-log seal now covers SHA-256(JCS(root + leaves + siblings + merkleProfile)) and verifySeal recomputes it from the stored JSONB with the single formula, so tampering with the profile or a sibling yields valid=false. The historical row and paragraphs above are kept on purpose. Not changed: the verifier checks the profile's integrity, not that it equals MERKLE_PROFILE_V2_URI, and it does not validate the token's trust chain.

Segunda nota de cierre (Fase 06.2, PRE-02/AUDC-04, 2026-10-02): la fórmula de 05.6 YA NO es la vigente. La nota anterior se conserva como registro de lo que cerró 05.6, pero su primera viñeta, su método y dos de sus límites dejaron de ser ciertos:

Second closure note (Phase 06.2): the 05.6 formula is no longer current. The audit-log seal imprint is SHA-256(JCS(seven-field envelope)) — sealKind, tenantId, seqFrom, seqTo, lastEntryHash, prevSealHash (SHA-256 of the previous token's stored bytes) and the nested tree (null for a heartbeat) — computed only by SealEnvelope.imprint; MerkleTreeCanonicalForm.sealImprint was retired. Still a single formula, numbers canonicalized per RFC 8785 with integers beyond 2^53 rejected. valid=true now means intact signature plus a chain up to the configured anchor, nothing more. Audit-log seals are never purged.


7. Fuera de alcance / Out of scope

attestation-v1.xsd (sello #1) NO declara merkleProfile en esta fase. La omisión es deliberada y queda registrada aquí, no silenciada.

Ese esquema también embebe un root computado por MerkleTree.build() (aipMerkleRoot, paso 7 de la saga). Extenderlo exigiría el MISMO trabajo de dispatch v1/v2 que se ha pagado para el sello #2, y su consumidor —autenticidad de origen del SIP, no verificación de integridad post-hoc— no lee merkleProfile en ningún punto. Duplicar el coste de dispatch por un campo sin consumidor es presupuesto abierto, que D-08 de 05.1.2-CONTEXT.md prohíbe para esta fase.

Condición que forzaría extenderlo. Que alguien construya una verificación independiente de origin-attestation.xml sin acceso al resto del AIP —es decir, un consumidor que necesite recomputar aipMerkleRoot a partir del sello #1 solo—. En ese momento attestation-v1.xsd necesitará el mismo tratamiento: campo merkleProfile y dispatch de lectura.

Tampoco entra en este documento la validación criptográfica de la firma cualificada ni del token TSA que envuelven la evidencia: este perfil define cómo se computa el valor sellado, no cómo se acredita el sello.

attestation-v1.xsd (seal #1) does not declare merkleProfile in this phase. It also embeds a root computed by MerkleTree.build() (aipMerkleRoot), but extending it would require the same v1/v2 dispatch work paid for seal #2 for a field whose consumer — SIP origin authenticity, not post-hoc integrity verification — never reads it. What would force the extension: an independent verification of origin-attestation.xml without access to the rest of the AIP. Cryptographic validation of the qualified signature and the TSA token is also out of scope here: this profile defines how the sealed value is computed, not how the seal is proven.


Documento normativo · Phase 05.1.2 (GATE-02) · https://docs.docuten.com/earchiving/merkle-profile/v2