Guía de Implementación Core de Costa Rica
0.1.0 - ci-build Costa Rica bandera

Guía de Implementación Core de Costa Rica - Versión en desarrollo (v0.1.0): borrador de trabajo de la Iniciativa HL7® Costa Rica, que puede cambiar sin aviso.

Firma digital con GAUDÍ

Estado de las normas de la página: Informative

Esta página dice cómo se firman el documento clínico (un Bundle de tipo document de FHIR R5) y la procedencia con GAUDÍ, el gestor de autenticación y firma digital del Banco Central de Costa Rica (BCCR), cómo se relaciona el documento con su firma y cómo verifica la firma quien lo recibe. Se basa en el estándar electrónico de GAUDÍ (Norma Técnica: Estándar electrónico - Firmador, Validador y Autenticador GAUDI, edición 6) y en la implementación de referencia en Python (cliente gaudi-fhir), que sella, firma y valida documentos FHIR contra GAUDÍ en producción.

Resumen

Tema Regla
Dónde va la firma Separada del documento (detached). El documento se guarda sin firma (Bundle.signature no se usa). Cada firma es una firma del documento (Provenance) que apunta a la versión exacta del documento: Bundle/<id>/_history/<versión>.
Quién firma El responsable de la validación legal del documento, con su firma digital de persona física, y la organización que lo emite, con su sello electrónico de persona jurídica. Cada una es una procedencia aparte; el sello es opcional cuando firma un profesional.
Quién produce la firma GAUDÍ. El sistema emisor no firma con una llave propia: envía a GAUDÍ un Bundle de trabajo (el documento con Bundle.signature preparada) y copia la firma que GAUDÍ le agrega.
Formato JAdES B-LT: un JWS en serialización JSON ({"signatures":[…]}), RS256, con el contenido separado (sin payload), el sello de tiempo y los datos para validarla a largo plazo. sigFormat = application/jose+json.
Canonicalización El Bundle de trabajo se envía canonicalizado con JCS (RFC 8785), para que quien verifica pueda reconstruirlo byte a byte.
Cómo viajan En la misma transacción de envío: el documento con PUT a Bundle/<id> y cada firma con POST.
Quién verifica El validador de documentos de GAUDÍ. Los validadores de FHIR no verifican esta firma.
Qué se acepta Veredicto 2 (firma avanzada, válida en el tiempo) y un firmante que coincide con Provenance.signature.who.

Servicios de GAUDÍ que se usan

GAUDÍ tiene un método propio para documentos FHIR en cada servicio, y cada servicio tiene su propio servidor. Son los únicos métodos que usa esta guía.

Para qué Servicio Método (estándar GAUDÍ) URL base (producción) Respuesta
Sellar con el sello electrónico de la organización Sellador SelladorElectronico/RecibaLaSolicitudDeSelladoElectronicoJsonParaArchivosFHIR (8.1.7) https://servicios-rest-sellador.gaudi.sinpe.fi.cr/FVA/Bccr.Firma.Fva.Entidades.Sello.API Síncrona: devuelve el documento sellado
Firmar con la firma digital de una persona Firmador FirmadorDocumento/RecibaLaSolicitudDeFirmaJsonParaArchivosFHIR (5.3.9) https://servicios-rest-firmador.gaudi.sinpe.fi.cr/FVA/Bccr.Firma.Fva.Entidades.FirmarDocumento.API Asíncrona: la persona aprueba en su celular y GAUDÍ notifica el resultado
Verificar las firmas Validador ValidarDocumento/ValideElDocumentoJsonParaArchivosFHIR (9.1.7) https://servicios-rest-validador.gaudi.sinpe.fi.cr/FVA/Bccr.Firma.Fva.Entidades.ValidarDocumento.API Síncrona: el veredicto y el detalle de cada firma

Todos son REST sobre HTTPS (TLS 1.2 o superior) con autenticación mutua: la entidad se identifica ante GAUDÍ con su certificado de agente electrónico. Ese certificado solo sirve para la conexión; no firma documentos: una firma hecha con él sale como no válida (código 10 de la tabla 11.9). El sello lo aplica GAUDÍ con el certificado de sello electrónico custodiado de la entidad, y la firma, con el certificado de firma digital de la persona. El tamaño máximo de un documento es de 20 MB.

El documento y su firma

DocumentReference(cr-documentreference)content.attachment.url = [base]/Bundle/<id>Bundle <id>, versión 1(cr-bundle-documento)type = documententry = Composition y los recursos que referenciasignature = (no se usa)Provenance: firma del radiólogo(cr-provenance-firma-documento)target = Bundle/<id>/_history/1activity = attestagent[attester] = PractitionerRole del radiólogosignature.who = PractitionerRole del radiólogosignature.data = JAdES B-LT de GAUDÍ (firmador)Provenance: sello del hospital(cr-provenance-firma-documento)target = Bundle/<id>/_history/1activity = attestagent[attester] = Organization del hospitalsignature.who = Organization del hospitalsignature.data = JAdES B-LT de GAUDÍ (sellador)apunta al documentofirma la versión 1sella la versión 1
Relación Cómo se hace
Firma → documento Provenance.target.reference = Bundle/<id>/_history/<versión>: la referencia literal y con versión (invariante cr-firma-documento-target-version). La firma cubre esa versión.
Documento → firmas El documento no apunta a sus firmas. Se buscan con GET [base]/Provenance?target=Bundle/<id> (todas las versiones) o ?target=Bundle/<id>/_history/1 (una versión).
Referencia → documento DocumentReference.content.attachment.url = la URL del Bundle, como en IHE MHD.
Paciente Provenance.patient = el subject de la Composition: con GET [base]/Provenance?patient=Patient/<id> se traen las firmas de los documentos de un paciente.
Varias firmas Una procedencia por firma: la del profesional y el sello de la organización son dos procedencias con el mismo target. Cada una sale de su propio Bundle de trabajo con el envase vacío, así que su JOSE lleva una sola firma y se verifica sola.
Id del documento Lo asigna el emisor (un UUID) y no cambia. El documento se envía con PUT a Bundle/<id>: la primera versión es la 1, y las firmas pueden apuntar a Bundle/<id>/_history/1 en la misma transacción.
Inmutabilidad El repositorio no actualiza documentos ni firmas. Una corrección es un documento nuevo, con su propio id y sus propias firmas (decisión 7 del documento clínico).

Qué cubre la firma

La firma de GAUDÍ protege los recursos del documento, es decir, todo lo que va en Bundle.entry: el encabezado (Composition), el paciente, los autores, quien valida, el custodio y las entradas de las secciones. Cambiar un dato clínico, el nombre del paciente o quitar una entrada invalida la firma.

El sobre del Bundle no queda cubierto: Bundle.id, identifier, type, timestamp, meta y Bundle.signature (type, when y who) se pueden cambiar sin que el validador de GAUDÍ lo detecte, y la cabecera del JWS no declara el alcance (no trae sigD). Se midió así contra el validador de producción. Tampoco cubre la procedencia que lleva la firma. Por eso:

  • Las decisiones de acceso se toman con las etiquetas de seguridad de la Composition, que sí están firmadas. El Bundle las repite para que el HIE las vea sin abrir el documento, y la invariante cr-bundle-documento-etiquetas exige que coincidan.
  • El identificador de negocio del documento es el de la Composition (Composition.identifier y su versión), que está firmado.
  • Quién firmó lo dice el certificado, no Provenance.signature.who: el receptor compara el firmante que reporta GAUDÍ con el recurso de who (ver Verificación).
  • El momento oficial de la firma es el del sello de tiempo (la fechaEstampaDeTiempo que reporta el validador), no signature.when.

Ejemplo paso a paso

El ejemplo es el informe de resonancia magnética de esta guía (documento), que firma el radiólogo que lo valida (firma) y sella el hospital que lo emite (sello), y que el hospital envía al HIE en una transacción. Los valores de GAUDÍ (códigos de negocio, firmas, hashes) son ilustrativos.

Paso 1. Ensamblar y validar el documento

El expediente del hospital ensambla el documento conforme a cr-bundle-documento, sin signature, con el id que le asigna, y lo valida contra el perfil ($validate). Un documento que no cumple la guía no se envía a firmar: después de firmado no se puede corregir sin volver a firmar.

{
  "resourceType": "Bundle",
  "id": "ejemplo-bundle-documento-imagenes",
  "meta": { "security": [ { "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality", "code": "N" } ] },
  "identifier": { "system": "urn:ietf:rfc:3986", "value": "urn:uuid:3f6b1c2a-9d4e-4b7a-8c1f-2e5d6a7b8c90" },
  "type": "document",
  "timestamp": "2026-09-30T10:26:00-06:00",
  "entry": [
    { "fullUrl": "https://hl7.or.cr/fhir/core/Composition/ejemplo-documento-imagenes", "resource": { "resourceType": "Composition", "…": "…" } },
    { "fullUrl": "https://hl7.or.cr/fhir/core/Patient/ejemplo-paciente-dimex", "resource": { "resourceType": "Patient", "…": "…" } },
    "… la atención, el rol del radiólogo, el radiólogo y el hospital …"
  ]
}

Paso 2. Armar el Bundle de trabajo

Para cada firma, el emisor arma un Bundle de trabajo: una copia del documento a la que agrega Bundle.signature. No se guarda, no se envía al HIE y no se valida contra el perfil (el perfil no admite signature): existe solo para enviarlo a GAUDÍ, y quien verifica lo reconstruye con las mismas reglas.

Elemento del Bundle de trabajo Contenido
Todo lo demás El documento tal como se envía al HIE (paso 1). Al verificar, el documento tal como lo devuelve el repositorio en esa versión.
meta Solo meta.security y meta.profile, si el documento los lleva. Sin versionId, lastUpdated ni source, que pone el servidor al guardar.
signature.type 1.2.840.10065.1.12.1.1 (Author's Signature) si firma el autor; 1.2.840.10065.1.12.1.6 (Validation Signature) si firma quien valida y no es el autor; 1.2.840.10065.1.12.1.14 (Source Signature) en el sello de la organización. Sistema urn:iso-astm:E1762-95:2013.
signature.when El momento de la solicitud de firma, con zona horaria
signature.who El PractitionerRole (o Practitioner) de quien firma, o la Organization que sella
signature.targetFormat application/fhir+json
signature.sigFormat application/jose+json
signature.data Al firmar, el envase vacío: eyJzaWduYXR1cmVzIjpbXX0=, el base64 de {"signatures":[]}. Al verificar, la firma real.

GAUDÍ exige el envase vacío: sin signature, o con data ausente o vacío, el sellador responde con el error 9 y el firmador notifica el 6; con cualquier contenido que no sea un objeto con signatures como arreglo, los dos responden 1. GAUDÍ agrega su firma al arreglo.

Después, el emisor canonicaliza el Bundle de trabajo con JCS (RFC 8785) y lo codifica en UTF-8. Esos bytes son el documento que recibe GAUDÍ, y hashDocumento es su SHA-256 en base64. La canonicalización es obligatoria: la firma de GAUDÍ es sensible al orden de las claves dentro de los objetos JSON, y un servidor FHIR reescribe el documento en su propio orden al guardarlo. JCS ordena las claves de forma determinista, así que quien verifica reconstruye los mismos bytes. JCS escribe los números con la regla de ECMAScript (6.200000 queda 6.2): la firma no cubre los ceros de precisión de un decimal.

import base64, copy, hashlib
import rfc8785  # JCS, RFC 8785

EMPTY_ENVELOPE = base64.b64encode(b'{"signatures":[]}').decode()

def build_working_bundle(document, signature, data=EMPTY_ENVELOPE):
    """El documento con Bundle.signature: signature lleva type, when y who."""
    working = copy.deepcopy(document)
    meta = {k: v for k, v in working.pop("meta", {}).items() if k in ("security", "profile")}
    if meta:
        working["meta"] = meta
    working["signature"] = {**signature,
                            "targetFormat": "application/fhir+json",
                            "sigFormat": "application/jose+json",
                            "data": data}
    return working

radiologist_signature = {
    "type": [{"system": "urn:iso-astm:E1762-95:2013", "code": "1.2.840.10065.1.12.1.1",
              "display": "Author's Signature"}],
    "when": "2026-09-30T10:26:30-06:00",
    "who": {"reference": "PractitionerRole/ejemplo-rol-radiologia", "display": "Carlos Jiménez Solano"},
}
document_bytes = rfc8785.dumps(build_working_bundle(document, radiologist_signature))
document_b64 = base64.b64encode(document_bytes).decode()
hash_b64 = base64.b64encode(hashlib.sha256(document_bytes).digest()).decode()

Paso 3a. Sellar con el sello electrónico del hospital

POST https://servicios-rest-sellador.gaudi.sinpe.fi.cr/FVA/Bccr.Firma.Fva.Entidades.Sello.API/SelladorElectronico/RecibaLaSolicitudDeSelladoElectronicoJsonParaArchivosFHIR, con Content-Type: application/json y el certificado de agente electrónico:

{
  "codNegocio": 1234,
  "idFuncionalidad": 5,
  "fechaDeReferenciaDeLaEntidad": "2026-09-30T10:27:00.000",
  "idAlgoritmoHash": 1,
  "documento": "<document_b64: el Bundle de trabajo del sello, en JCS>",
  "hashDocumento": "<hash_b64>"
}
Campo Regla
codNegocio Código de la identidad de marca de la entidad, que se crea en Central Directo (Firma Digital → GAUDI → Identidad de Marca). La entidad debe tener su sello electrónico configurado y habilitado para esa identidad (Firma Digital → GAUDI → Configuración Sello).
idFuncionalidad La funcionalidad de esa identidad de marca. Opcional; se recomienda, para saber qué sistema envió cada solicitud.
fechaDeReferenciaDeLaEntidad Momento del envío, en hora de Costa Rica y sin zona, con el formato AAAA-MM-DDTHH:MM:SS.000. No puede diferir más de 60 segundos del reloj de GAUDÍ (error 3): el servidor del emisor sincroniza su reloj (NTP).
idAlgoritmoHash 1 (SHA-256).
documento Base64 estándar, con relleno, de los bytes JCS del Bundle de trabajo.
hashDocumento Base64 del SHA-256 de los mismos bytes de documento. Si no coincide, GAUDÍ responde con el error 10.

Respuesta, inmediata:

{
  "fueExitosa": true,
  "codigoDeError": 0,
  "documentoFirmado": "<base64 del Bundle de trabajo sellado>",
  "idAlgoritmoHashDocumentoFirmado": 1,
  "hashDocumentoFirmado": "<hash del documento sellado>"
}

El emisor comprueba que fueExitosa sea true y codigoDeError sea 0; decodifica documentoFirmado y comprueba que sus entradas sean las que envió; comprueba que el JOSE de signature.data traiga exactamente una firma (el sellador del ambiente de pruebas responde fueExitosa: true y devuelve el documento sin firmar), y se queda con ese signature.data. El resto del Bundle sellado se descarta.

POST https://servicios-rest-firmador.gaudi.sinpe.fi.cr/FVA/Bccr.Firma.Fva.Entidades.FirmarDocumento.API/FirmadorDocumento/RecibaLaSolicitudDeFirmaJsonParaArchivosFHIR, con su propio Bundle de trabajo (el de la firma del radiólogo, con el envase vacío) y tres campos más:

{
  "codNegocio": 1234,
  "idFuncionalidad": 5,
  "fechaDeReferenciaDeLaEntidad": "2026-09-30T10:26:30.000",
  "idAlgoritmoHash": 1,
  "documento": "<document_b64: el Bundle de trabajo de la firma, en JCS>",
  "hashDocumento": "<hash_b64>",
  "identificacionDelSuscriptor": "02-0456-0789",
  "idReferenciaEntidad": 50871,
  "resumenDocumento": "Informe de resonancia magnética de José Mendoza, 30/09/2026"
}
Campo Regla
identificacionDelSuscriptor La identificación de quien firma, la misma de su Practitioner: cédula 0#-####-#### (aquí la cédula 204560789), DIMEX 1########### o DIDI 5###########.
idReferenciaEntidad Identificador único de la solicitud en el sistema emisor (la llave de su tabla de solicitudes de firma).
resumenDocumento El texto que verá la persona antes de firmar, de 1 a 250 caracteres. Describe el documento real.

Respuesta inmediata: la solicitud se recibió, pero todavía no hay firma.

{ "codigoDeError": 0, "codigoDeVerificacion": "EA6", "tiempoMaximoDeFirmaEnSegundos": 240, "idDeLaSolicitud": 79041335 }

El expediente muestra el código de verificación (EA6) al radiólogo, que lo escoge entre tres opciones en su celular antes de que venza el plazo.

Notificación. Cuando la persona firma, rechaza o se vence el plazo, GAUDÍ llama al servicio que la entidad publica y configura en Central Directo (POST …/NotifiqueLaRespuesta, con autenticación mutua). Los campos llegan en PascalCase, aunque el OpenAPI del BCCR los documenta en camelCase; el receptor los lee sin distinguir mayúsculas:

{
  "IdDeLaSolicitud": 79041335,
  "FueExitosa": true,
  "CodigoDeError": 0,
  "DocumentoFirmado": "<base64 del Bundle de trabajo firmado>",
  "IdAlgoritmoHashDocumentoFirmado": 1,
  "HashDocumentoFirmado": "<hash del documento firmado, en hexadecimal>"
}

El emisor la correlaciona por IdDeLaSolicitud, hace las mismas comprobaciones del sello y se queda con signature.data. Además:

  • El firmador puede devolver signature.data sin el relleno final de base64 (=), y FHIR lo rechaza como base64Binary inválido. El emisor completa el relleno; los bytes decodificados son los mismos, así que la firma no cambia.
  • Cuando vence el plazo, el firmador puede notificar el código genérico 1 en lugar del 3. El emisor compara también el tiempo transcurrido con tiempoMaximoDeFirmaEnSegundos.

Paso 4. Crear la procedencia de cada firma

Cada firma es una cr-provenance-firma-documento con los mismos type, when y who del Bundle de trabajo y, en data, la firma que devolvió GAUDÍ. La del radiólogo:

{
  "resourceType": "Provenance",
  "meta": { "profile": [ "https://hl7.or.cr/fhir/core/StructureDefinition/cr-provenance-firma-documento" ] },
  "target": [ { "reference": "Bundle/ejemplo-bundle-documento-imagenes/_history/1" } ],
  "recorded": "2026-09-30T10:27:05-06:00",
  "patient": { "reference": "Patient/ejemplo-paciente-dimex" },
  "activity": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/iso-21089-lifecycle", "code": "attest" } ] },
  "agent": [
    { "type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type", "code": "assembler" } ] },
      "who": { "reference": "Device/ejemplo-sistema-hce-hospital" } },
    { "type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type", "code": "attester" } ] },
      "who": { "reference": "PractitionerRole/ejemplo-rol-radiologia" } }
  ],
  "signature": [ {
    "type": [ { "system": "urn:iso-astm:E1762-95:2013", "code": "1.2.840.10065.1.12.1.1" } ],
    "when": "2026-09-30T10:26:30-06:00",
    "who": { "reference": "PractitionerRole/ejemplo-rol-radiologia" },
    "targetFormat": "application/fhir+json",
    "sigFormat": "application/jose+json",
    "data": "<el signature.data del Bundle de trabajo firmado, con relleno>"
  } ]
}

El sello del hospital es igual, con Organization/ejemplo-establecimiento como attester y como signature.who, y el tipo 1.2.840.10065.1.12.1.14 (Source Signature). Quien firma es un agente attester (invariante cr-firma-documento-firmante); el assembler es el sistema que pidió la firma.

Paso 5. Enviar el documento y sus firmas al HIE

El documento y sus firmas viajan en la misma transacción de envío, con el lote y la referencia: se guardan todos o ninguno. El documento va con PUT a Bundle/<id>; las firmas, con POST. La invariante cr-envio-firma-documento exige que cada documento final, corregido, con adenda o registrado por error lleve al menos una firma que apunte a Bundle/<id>/_history/1.

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    { "fullUrl": "urn:uuid:8a1d2c3b-…", "resource": { "resourceType": "List", "…": "el lote" },
      "request": { "method": "POST", "url": "List" } },
    { "fullUrl": "urn:uuid:5b6c7d8e-…", "resource": { "resourceType": "DocumentReference", "…": "…",
        "content": [ { "attachment": { "url": "https://hie.example.cr/fhir/Bundle/ejemplo-bundle-documento-imagenes" } } ] },
      "request": { "method": "POST", "url": "DocumentReference" } },
    { "fullUrl": "https://hie.example.cr/fhir/Bundle/ejemplo-bundle-documento-imagenes",
      "resource": { "resourceType": "Bundle", "id": "ejemplo-bundle-documento-imagenes", "type": "document", "…": "…" },
      "request": { "method": "PUT", "url": "Bundle/ejemplo-bundle-documento-imagenes" } },
    { "fullUrl": "urn:uuid:7c1e2d3f-…", "resource": { "resourceType": "Provenance", "…": "la firma del radiólogo" },
      "request": { "method": "POST", "url": "Provenance" } },
    { "fullUrl": "urn:uuid:8d2f3e4a-…", "resource": { "resourceType": "Provenance", "…": "el sello del hospital" },
      "request": { "method": "POST", "url": "Provenance" } }
  ]
}

El repositorio:

  • verifica cada firma con el validador de GAUDÍ (paso 7) antes de aceptar la transacción, y la rechaza entera si una firma no se acepta;
  • rechaza el PUT si el documento ya existe: no actualiza documentos, y así la versión que nombran las firmas es la 1;
  • conserva la versión de Provenance.target (algunos servidores la quitan de las referencias si no se configura; en HAPI, con las rutas de referencias versionadas que se conservan) y no modifica las entradas del documento.

Paso 6. Encontrar las firmas de un documento

Quien lee un documento encuentra sus firmas por target:

GET [base]/Provenance?target=Bundle/ejemplo-bundle-documento-imagenes

La respuesta trae las dos procedencias, la firma del radiólogo y el sello del hospital, cada una con su target versionado. Para verificar una, se lee la versión que nombra:

GET [base]/Bundle/ejemplo-bundle-documento-imagenes/_history/1

Las firmas de los documentos de un paciente se buscan con GET [base]/Provenance?patient=Patient/ejemplo-paciente-dimex.

Paso 7. Verificar la firma

Verifican la firma el repositorio del HIE, cuando la recibe, y cualquier sistema que use el documento. Ningún validador de FHIR verifica esta firma: el de HL7 y el de HAPI reportan que no comprueban el formato application/jose+json. La única autoridad es el validador de GAUDÍ.

  1. Reconstruir el Bundle de trabajo. Con la versión del documento que nombra Provenance.target y la firma de la procedencia con su data real, con las reglas del paso 2 (build_working_bundle(document, signature, data=real_signature_data)), canonicalizado con JCS y codificado en UTF-8 y luego en base64.
  2. Consultar al validador. POST https://servicios-rest-validador.gaudi.sinpe.fi.cr/FVA/Bccr.Firma.Fva.Entidades.ValidarDocumento.API/ValidarDocumento/ValideElDocumentoJsonParaArchivosFHIR, con Content-Type: application/json, el certificado de agente electrónico y, como cuerpo, el base64 como cadena JSON (entre comillas).
  3. Leer la respuesta. Por ejemplo, la de la firma del radiólogo:

    {
      "fueExitosa": true,
      "errorGeneradoAlValidar": null,
      "resumen": {
        "garantiaDeIntegridadYAutenticidad": true,
        "garantiaDeValidezEnElTiempo": true,
        "resultadoValidacionDelDocumentoFirmado": 2,
        "resumenDeFirmas": [ {
          "firmante": "CARLOS JIMENEZ SOLANO",
          "identificacion": "02-0456-0789",
          "tipoIdentificacion": 1,
          "tieneFechaEstampaDeTiempo": true,
          "fechaEstampaDeTiempo": "2026-09-30T10:26:52-06:00",
          "garantiaDeIntegridadYAutenticidad": true,
          "garantiaDeValidezEnElTiempo": true,
          "resultadoValidacion": 2
        } ]
      }
    }
    
  4. Aceptar o rechazar:

    Comprobación Se acepta si
    fueExitosa true. Solo dice que la validación se ejecutó. Si es false, la firma no se pudo verificar (el detalle va en errorGeneradoAlValidar): no es válida ni inválida, y se reintenta.
    resumen.resultadoValidacionDelDocumentoFirmado 2. 1 (válido con advertencias: alguna firma solo básica) no garantiza la validez en el tiempo y no se acepta para un documento clínico, salvo que la política del receptor lo admita para un uso puntual. 0 es no válido.
    garantiaDeIntegridadYAutenticidad, garantiaDeValidezEnElTiempo Las dos true, en el resumen y en la firma.
    resumen.resumenDeFirmas Exactamente una firma, con resultadoValidacion 2.
    Firmante La identificacion coincide con la del recurso de Provenance.signature.who: la cédula, el DIMEX o el DIDI del Practitioner (tipos 1 a 3), o la cédula jurídica de la Organization (tipo 4; aquí sería 3-101-123456).
    Momento La fechaEstampaDeTiempo existe y no es anterior a Composition.date ni a la validación legal (attester.time).
  5. No guardar el veredicto como permanente. Vale para el momento en que se pidió: la revocación de un certificado o el vencimiento de un sello de tiempo pueden cambiarlo sin que cambie el documento.

Firma de la procedencia

La procedencia de un dato que se intercambia fuera de un documento también se firma con GAUDÍ, con el sello electrónico de la organización propietaria del sistema o con la firma digital del profesional que hizo la actividad. También es detached: va en Provenance.signature y cubre la procedencia y la versión de cada recurso de target. La diferencia con la firma de un documento es el Bundle de trabajo, porque aquí lo firmado no es un Bundle:

Elemento Contenido
resourceType, type Bundle, collection. Sin id, meta, identifier ni timestamp.
entry[0].resource La procedencia, sin id, meta, text ni signature: los tres primeros los pone el servidor al guardarla, y la firma no se puede firmar a sí misma.
entry[1..n].resource Cada recurso de target con referencia literal, en el orden de target, tal como el servidor devuelve esa versión (GET [base]/<Recurso>/<id>/_history/<versión>), con su meta. Un target por identificador (por ejemplo, el paciente que se envió a PH4H) no agrega entrada: lo cubre la procedencia, que lleva su identificador.
entry.fullUrl No se usa.
signature Los seis elementos del paso 2, con los valores de Provenance.signature y, para firmar, el envase vacío en data.

Por eso cada referencia literal de target nombra la versión (invariante cr-procedencia-target-version). Para firmar, el sistema arma ese Bundle de trabajo, lo envía al sellador o al firmador como en los pasos 3a y 3b (en resumenDocumento describe la actividad; por ejemplo, «Unión de registros en el MPI Nacional») y copia el signature.data que devuelve GAUDÍ a Provenance.signature.data. Para verificar, lo reconstruye desde la procedencia guardada y las versiones de target, con la firma real, y sigue el paso 7, comparando el firmante con Provenance.signature.who. Si una versión de target ya no existe, la firma no se puede verificar.

Nivel de la firma: JAdES B-LT

La cabecera no protegida (etsiU) de las firmas de GAUDÍ trae los cuatro componentes de JAdES que hacen falta para validarla a largo plazo, tanto en el sello electrónico como en la firma digital de una persona:

Componente de etsiU Contenido Nivel JAdES
sigTst Sello de tiempo de la firma (RFC 3161) B-T
xVals Los certificados de la jerarquía del firmante (3) B-LT
rVals El estado de revocación: CRL y respuesta OCSP B-LT
tstVD El certificado y la CRL de la autoridad del sello de tiempo B-LT

No trae sello de tiempo de archivo (arcTst), así que no es B-LTA. B-LT es el nivel que fija esta guía, y el validador de GAUDÍ lo reporta como firma avanzada con garantía de validez en el tiempo: por eso se exige el veredicto 2. Un documento que se conserve más allá de la vigencia de los certificados de la jerarquía y de la autoridad de sello de tiempo necesitaría un sello de archivo (B-LTA), que GAUDÍ no agrega.

Ambientes

  Producción Pruebas
Sellador …/FVA/Bccr.Firma.Fva.Entidades.Sello.API …/FVA/Bccr.Firma.Fva.AmbienteDePruebas.Entidades.Sello.API
Firmador …/FVA/Bccr.Firma.Fva.Entidades.FirmarDocumento.API …/FVA/Bccr.Firma.Fva.AmbienteDePruebas.Entidades.FirmarDocumento.API
Validador …/FVA/Bccr.Firma.Fva.Entidades.ValidarDocumento.API No sirve para verificar: responde 404
Certificado de agente electrónico El de producción El mismo de producción
¿Firma de verdad? Sí No: el sellador devuelve el documento sin firmar

Cada servicio conserva su servidor en los dos ambientes (servicios-rest-sellador, servicios-rest-firmador y servicios-rest-validador, en gaudi.sinpe.fi.cr); cambia la ruta. El ambiente de pruebas sirve para recorrer los escenarios que el BCCR exige antes de habilitar producción (si no se completaron, GAUDÍ responde con el error 14 en el firmador o 16 en el sellador), no para probar firmas. La conexión confía en la jerarquía nacional (CA Raíz Nacional - Costa Rica v2 y las CA del BCCR), que no traen los almacenes de certificados de muchos sistemas y contenedores: el cliente la configura de forma explícita.

Códigos de GAUDÍ

Errores del sellador (tabla 11.7 del estándar):

Código Significado Qué hace el emisor
0 Solicitud recibida correctamente Continúa
1 Problema al solicitar la firma Revisa que signature.data sea el envase {"signatures":[]}
2 Campos incompletos Corrige la solicitud
3 La hora del cliente difiere más de 60 s de la del servidor Sincroniza el reloj; la fecha va en hora de Costa Rica, sin zona
4, 5, 6 Entidad no registrada, inactiva o identidad de marca ajena Revisa la configuración en Central Directo
7, 8 La identidad de marca no tiene sello electrónico configurado o lo tiene deshabilitado Configura y habilita el sello
9 Documento no válido Agrega signature con el envase en data
10 El hash enviado no coincide con el calculado Calcula el hash sobre los mismos bytes de documento
14 La entidad no está habilitada por su representante legal Gestiona la habilitación
15 El certificado de sello electrónico está vencido Renueva el sello
16 No se completaron los escenarios del ambiente de pruebas Completa los escenarios

Errores del firmador al solicitar (tabla 11.2): 3 (reloj), 9 (la persona está desconectada), 10 (formato de identificación inválido) y 11 y 12 (la identidad de marca no tiene configurada o habilitada la firma de persona física). Al notificar (tabla 11.3): 2 (la persona rechazó la firma), 3 (venció el plazo), 6 (documento inválido: falta el envase en signature.data), 8 (hash incorrecto), 9 y 10 (certificado revocado o vencido), 11 (ya hay una solicitud en curso para esa persona) y 13 (la persona escogió un código de verificación equivocado).

Resultado del validador (tablas 11.8, 11.10 y 11.11):

Código Una firma (resultadoValidacion) El documento (resultadoValidacionDelDocumentoFirmado)
0 No válida No válido: al menos una firma no es válida
1 Firma digital básica Válido con advertencias: al menos una firma es básica
2 Firma digital avanzada Válido: todas las firmas son avanzadas

Relación con la especificación de firmas de FHIR

FHIR R5 recomienda JAdES para las firmas y deja a cada guía el detalle. Esta guía fija el que produce GAUDÍ, que difiere en tres puntos de lo que suponen las herramientas de FHIR:

  • Serialización. GAUDÍ emite la serialización JSON de JWS, no la compacta. Por eso sigFormat es application/jose+json: con application/jose, el validador de HL7 intenta leer la firma como compacta y da un error.
  • Canonicalización. El contenido firmado no es la forma canónica de documentos de FHIR (http://hl7.org/fhir/canonicalization/json#document), sino la que GAUDÍ calcula sobre los recursos de Bundle.entry; el emisor envía el Bundle de trabajo en JCS para que esa forma se pueda reconstruir.
  • Cabecera. La cabecera protegida lleva alg (RS256), cty, typ (jose+json), iat, el certificado del firmante (x5c) y su huella (x5t#o). No lleva sigT (el momento va en iat) ni sigD (no declara qué partes firma).

Mientras el BCCR no publique cómo construye el contenido separado, la firma no se puede verificar fuera de GAUDÍ.