Guía de Implementación Core de Costa Rica
0.1.0 - ci-build
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.
| 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.
| 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. |
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.
| 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). |
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:
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.Composition (Composition.identifier y su versión), que está firmado.Provenance.signature.who: el receptor compara el firmante que reporta GAUDÍ con el recurso de who (ver Verificación).fechaEstampaDeTiempo que reporta el validador), no signature.when.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.
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 …"
]
}
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()
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:
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.tiempoMaximoDeFirmaEnSegundos.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.
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:
PUT si el documento ya existe: no actualiza documentos, y así la versión que nombran las firmas es la 1;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.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.
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Í.
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.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).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
} ]
}
}
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). |
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.
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.
| 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.
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 |
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:
sigFormat es application/jose+json: con application/jose, el validador de HL7 intenta leer la firma como compacta y da un error.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.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Í.