OpenCyber Academy
Tecnólogo · Capítulo 29 · Español
Código del curso: OCA-092Capítulo 29: OCA-092-C029

Capítulo 29 — Construcción del proyecto final

Aprendizaje gratuito y abierto para quienes desean proteger a las personas y la tecnología. No se necesita una cuenta para estudiar ni consultar las respuestas.

Mapa de la lección

Lección 29 publicada. Mapa, ejemplo, EX085.

flowchart TD
  N0["Resultados de aprendizaje y conexión"]
  N1["29.1 Lo que debe demostrar el proyecto final"]
  N2["29.2 La construcción"]
  N3["29.3 Leer el registro de evidencia"]
  N4["29.4 Un defecto que solo aparece en combinación"]
  N5["29.5 Construir el proyecto final real"]
  N6["Síntesis del capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6
  N6 --> EX["Ejemplo resuelto"]
  EX --> Q["Ejercicio EX085"]

Dibujo lógico

flowchart LR
  N0["Resultados de aprendizaje y conexión"]
  N1["29.1 Lo que debe demostrar el proyecto final"]
  N2["29.2 La construcción"]
  N3["29.3 Leer el registro de evidencia"]
  N4["29.4 Un defecto que solo aparece en combinación"]
  N5["29.5 Construir el proyecto final real"]
  N6["Síntesis del capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> Q["EX085"]
[Resultados de aprendizaje y conexión]
    |
    v
[29.1 Lo que debe demostrar el proyecto final]
    |
    v
[29.2 La construcción]
    |
    v
[29.3 Leer el registro de evidencia]
    |
    v
[29.4 Un defecto que solo aparece en combinación]
    |
    v
[29.5 Construir el proyecto final real]
    |
    v
[Síntesis del capítulo]
    |
    v
[Ejemplo resuelto] --> [Ejercicio EX085]

Resultados de aprendizaje y conexión

Al terminar este capítulo, usted debería ser capaz de:

Este es el resultado práctico del curso: implementar un servicio de extremo a extremo con controles monitorizados. El Lab 29 combina, en un solo archivo, el sellado en la estación del capítulo 15, la validación del capítulo 3, un historiador en un modelo relacional (capítulo 4), la monitorización con una regla de ausencia (capítulo 19) y un registro de evidencia con un resumen criptográfico (capítulos 6 y 20). Es lo bastante pequeño para leerlo entero; el proyecto final le pide construir la misma forma a mayor escala.

29.1 Lo que debe demostrar el proyecto final

  1. La ruta de los datos funciona: las lecturas de todas las estaciones llegan al historiador, validadas y autenticadas.
  2. Los controles funcionan: los fallos inyectados (una reproducción, un mensaje falsificado, un valor fuera de rango y el silencio de una estación) se rechazan, se ponen en cuarentena o generan una alerta, cada uno.
  3. La monitorización funciona: se generan alertas para cada fallo inyectado y para nada más.
  4. Existe evidencia: un registro de la ejecución legible por máquina, con recuentos que cuadran y un resumen criptográfico que permite detectar cambios posteriores.
  5. Se declaran los límites: lo que la ejecución no muestra.

29.2 La construcción

El Lab 29 genera dos horas de lecturas sintéticas de tres estaciones, con PS-09 en silencio del minuto 50 al 84. Inyecta cuatro pruebas: una reproducción de un mensaje anterior de PS-04, un mensaje de PS-07 sellado con la clave equivocada y una lectura de cloro fuera de rango de 7,5 mg/L de PS-07 que ocupa el hueco del minuto 20, además del silencio de PS-09. El retransmisor verifica el MAC, rechaza las reproducciones, pone en cuarentena los valores fuera de rango y almacena el resto; la monitorización añade una regla de silencio; el registro de evidencia resume la ejecución.

Lab 29 (guárdelo como lab29.py y ejecútelo con python3 lab29.py):

# Lab 29 - capstone: an end-to-end monitored telemetry service in one file (offline, standard library only)
# Synthetic stations -> authenticated relay -> validated historian (sqlite3) -> monitoring -> evidence record.
import hashlib, hmac, json, random, sqlite3
random.seed(29)
KEY = b"lantern-lab-key-not-real-0001"
LIMITS = {"LEVEL_M": (0.0, 12.0), "CL2_MGL": (0.0, 5.0)}
def seal(m, key=KEY):
    body = json.dumps(m, sort_keys=True, separators=(",", ":"))
    return body + "." + hmac.new(key, body.encode(), hashlib.sha256).hexdigest()
# 1. stations emit one reading per minute for 120 minutes; PS-09 goes silent 50-84; one injected test of each fault
wire, now0 = [], 1_790_000_000
for st, tag, base in (("PS-04", "LEVEL_M", 3.4), ("PS-07", "CL2_MGL", 0.6), ("PS-09", "LEVEL_M", 5.1)):
    for i in range(120):
        if st == "PS-09" and 50 <= i < 85: continue
        if st == "PS-07" and i == 20: continue  # the injected out-of-range test takes this slot
        wire.append((now0 + 60*i, seal({"st": st, "tag": tag, "v": round(base + random.gauss(0, 0.05), 3), "ts": now0 + 60*i, "seq": i})))
wire.append((now0 + 600, wire[3][1]))                                                         # TEST: replay
wire.append((now0 + 900, seal({"st": "PS-07", "tag": "CL2_MGL", "v": 0.6, "ts": now0 + 900, "seq": 999}, key=b"x")))  # TEST: forged
wire.append((now0 + 1200, seal({"st": "PS-07", "tag": "CL2_MGL", "v": 7.5, "ts": now0 + 1200, "seq": 20})))         # TEST: out of range
wire.sort(key=lambda w: w[0])
# 2. relay + historian
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE reading(st TEXT, tag TEXT, v REAL, ts INTEGER, seq INTEGER, PRIMARY KEY(st, seq))")
events, last = [], {}
for t, w in wire:
    body, mac = w.rsplit(".", 1)
    if not hmac.compare_digest(hmac.new(KEY, body.encode(), hashlib.sha256).hexdigest(), mac):
        events.append((t, "mac_reject", "?")); continue
    m = json.loads(body)
    if m["seq"] <= last.get(m["st"], -1):
        events.append((t, "replay_reject", m["st"])); continue
    lo, hi = LIMITS[m["tag"]]
    last[m["st"]] = m["seq"]
    if not lo <= m["v"] <= hi:
        events.append((t, "range_quarantine", m["st"])); continue
    db.execute("INSERT INTO reading VALUES (?,?,?,?,?)", (m["st"], m["tag"], m["v"], m["ts"], m["seq"]))
# 3. monitoring: security events and silence > 15 min
alerts = [(t, kind, st) for t, kind, st in events]
for st, in db.execute("SELECT DISTINCT st FROM reading"):
    ts = [r[0] for r in db.execute("SELECT ts FROM reading WHERE st=? ORDER BY ts", (st,))]
    alerts += [(a + 900, "silence", st) for a, b in zip(ts, ts[1:]) if b - a > 900]
# 4. evidence record
stored = db.execute("SELECT COUNT(*) FROM reading").fetchone()[0]
record = {"period_utc_s": [now0, now0 + 7140], "messages_on_wire": len(wire), "stored": stored,
          "alerts": sorted({(k, s) for _, k, s in alerts}), "injected_tests": 4,
          "injected_tests_detected": len({k for _, k, _ in alerts} & {"replay_reject", "mac_reject", "range_quarantine", "silence"}),
          "alert_count": len(alerts)}
blob = json.dumps(record, sort_keys=True).encode()
print(json.dumps(record, sort_keys=True))
print("evidence sha256:", hashlib.sha256(blob).hexdigest()[:24], "...")
print("PS-07 mean chlorine stored: %.3f mg/L" % db.execute("SELECT AVG(v) FROM reading WHERE st='PS-07'").fetchone()[0])

Salida esperada (Python 3.13.5):

{"alert_count": 4, "alerts": [["mac_reject", "?"], ["range_quarantine", "PS-07"], ["replay_reject", "PS-04"], ["silence", "PS-09"]], "injected_tests": 4, "injected_tests_detected": 4, "messages_on_wire": 327, "period_utc_s": [1790000000, 1790007140], "stored": 324}
evidence sha256: aa40e3905d178699f1460e1c ...
PS-07 mean chlorine stored: 0.606 mg/L

29.3 Leer el registro de evidencia

Compruebe que los números cuadran. Mensajes en la red: 120 + 119 + 85 + 3 inyectados = 327. Almacenados: 324 (todas las lecturas legítimas). No almacenados: 3 (la reproducción, la falsificación y la lectura fuera de rango). Alertas: cuatro, una de cada tipo, y cuatro de cuatro pruebas inyectadas detectadas. El resumen criptográfico permite a cualquiera confirmar más tarde que el registro no se ha editado; en un servicio real, el registro se guardaría fuera del equipo y el resumen se anotaría en la incidencia del cambio o de la prueba. Si el recuento de almacenados hubiera sido 325, una lectura legítima quedaría sin explicar.

29.4 Un defecto que solo aparece en combinación

Cambie el número de secuencia del mensaje fuera de rango inyectado de 20 a 1000 (como podría hacer una pasarela de estación defectuosa tras un reinicio) y vuelva a ejecutarlo. El resultado cambia de forma drástica: las lecturas almacenadas bajan de 324 a 225 y las alertas suben de 4 a 103. Noventa y nueve lecturas legítimas de PS-07 se rechazan como reproducciones. La causa es que el retransmisor actualiza su último número de secuencia visto después de la autenticación pero antes de la comprobación de rango: el mensaje fuera de rango es auténtico, así que se registra su número de secuencia 1000, y todos los mensajes legítimos posteriores (secuencias 21 a 119) parecen reproducciones.

Cada componente era «correcto» según las pruebas de su propio capítulo. El capítulo 3 advertía de que no se actualizara el estado de reproducción antes de que se superaran todas las demás comprobaciones; el capítulo 15 situó la actualización después de la autenticación. La combinación muestra el agujero que queda: un mensaje auténtico pero inverosímil puede envenenar el estado de reproducción. Las correcciones incluyen: actualizar el estado solo al aceptar o poner en cuarentena un mensaje cuya secuencia sea verosímil (rechazar los saltos de secuencia mayores que un límite, por ejemplo 1.000 o un día de mensajes, salvo en un modo explícito de resincronización); alertar ante saltos grandes; y dar a los operadores un procedimiento de resincronización revisado. Este es exactamente el motivo por el que el proyecto final prueba el servicio integrado y no solo sus partes.

29.5 Construir el proyecto final real

La versión de proyecto del trabajo final, hecha en equipo (capítulo 25), construye la misma forma con componentes reales en un laboratorio: tres máquinas virtuales de retransmisor, una base de datos del historiador, reenvío de registros a un recopilador, el normalizador, tres reglas de monitorización más la automonitorización, configuración como código con una comprobación de deriva, una copia de seguridad y una restauración cronometrada, manuales de operación y un paquete de traspaso. Use solo equipos de laboratorio o estaciones simuladas. El entregable es el registro de evidencia del servicio: para cada requisito de «Lo que debe demostrar el proyecto final», más arriba, la prueba realizada, su fecha, la salida bruta, el resultado frente a una regla de afirmación declarada de antemano (capítulo 23) y los elementos abiertos.

29.6 Errores frecuentes y límites

El resumen del equipo A en el capítulo 30 comete el quinto: no declara ningún límite.

Una falsificación inyectada construida con la clave real de la estación no probaría nada; el equipo debe diseñar pruebas que puedan fallar.

Ejercicios — capítulo 29

EX085 — Reproduzca el defecto. Objetivo: encontrar y explicar un defecto de integración. Entradas: el Lab 29. Restricciones: cambie solo el número de secuencia de la prueba fuera de rango. Pasos: (1) fíjelo en 1000 y ejecútelo; (2) informe del recuento de almacenados, del recuento de alertas y de qué mensajes se perdieron; (3) explique la causa en términos del orden de las comprobaciones. Evidencia esperada: salida y explicación. Rúbrica: 3 = stored 225, alert_count 103, 99 lecturas legítimas de PS-07 rechazadas como reproducciones, explicación de que el estado se actualizó para un mensaje auténtico pero fuera de rango; 2 = números correctos, explicación escasa; 1 = no ejecutado; 0 = nada.

EX086 — Corríjalo. Objetivo: corregir el defecto sin abrir una brecha de reproducción. Entradas: el Lab 29 con la secuencia 1000. Restricciones: solo la biblioteca estándar; las reproducciones deben seguir rechazándose. Pasos: (1) implemente un salto de secuencia máximo de 1.000 (rechazando los saltos mayores con un evento distinto, seq_jump_reject) u otra corrección defendible; (2) ejecútelo con la secuencia 1000 y con la secuencia 20; (3) muestre que la prueba de reproducción sigue rechazándose en ambos casos. Evidencia esperada: código y ambas salidas. Rúbrica: 3 = el salto de 981 (de 19 a 1000) está dentro de 1.000, así que el estudiante debe advertir que un límite de 1.000 no detecta este caso y elegir un límite más estricto o basado en el tiempo (por ejemplo, un salto no mayor que los minutos transcurridos más un margen), con stored 324 restablecido y la reproducción todavía rechazada; 2 = la corrección funciona en este caso, pero falta el razonamiento sobre el límite; 1 = elimina la protección contra la reproducción; 0 = nada.

EX087 — Revisión del registro de evidencia. Objetivo: comprobar un registro como evaluador. Entradas: la salida del Lab 29 con la secuencia 20. Restricciones: comprobaciones solo a partir del registro y del código del laboratorio. Pasos: (1) verifique que los recuentos cuadran; (2) recalcule el resumen a partir del JSON impreso y compare sus primeros 24 caracteres; (3) enumere tres límites que el registro debería declarar. Evidencia esperada: cálculos y lista. Rúbrica: 3 = 327 = 324 + 3 confirmado; resumen recalculado aplicando el hash a la cadena JSON exacta; límites como datos sintéticos, sin dispositivos de campo ni operador de telecomunicaciones, una sola ejecución, sin adversario adaptativo, sin carga a escala; 2 = dos partes; 1 = una; 0 = nada.

Síntesis del capítulo

El proyecto final integra el curso en un único servicio monitorizado y lo demuestra con pruebas inyectadas y un registro de evidencia comprobable. Los recuentos deben cuadrar, el registro debe protegerse con un resumen criptográfico y guardarse fuera del equipo, y sus límites deben declararse. Las pruebas de integración encuentran defectos que las pruebas de componentes no detectan: aquí, un mensaje auténtico pero inverosímil envenenó el estado de reproducción, un agujero visible solo cuando se encontraron la validación, la autenticación y el orden.

Respuestas desarrolladas

EX085 — Reproduzca el defecto. Con la secuencia de la prueba fuera de rango fijada en 1000: "stored": 225, "alert_count": 103, y las alertas incluyen ahora ["replay_reject", "PS-07"]. Perdidas: 99 lecturas legítimas de PS-07 (secuencias 21–119). Causa: el retransmisor registra el último número de secuencia después de la autenticación pero antes de la comprobación de rango; el mensaje auténtico fuera de rango fija en 1000 la última secuencia de PS-07, así que todos los mensajes legítimos posteriores tienen un número menor y se rechazan como reproducciones.

EX086 — Corríjalo. Un límite de 1.000 no sirve aquí: el salto de 19 a 1000 es de 981. Un límite basado en el tiempo funciona: rechazar (evento seq_jump_reject) cuando el salto supera los minutos transcurridos desde el último mensaje aceptado de la estación más un margen de 5. Implementado en el Lab 29 (registrando last_t por estación junto a last), la ejecución con la secuencia 1000 da "stored": 324, "alert_count": 4 con seq_jump_reject para PS-07 en lugar de la cuarentena por rango, y la reproducción de PS-04 sigue rechazándose; con la secuencia 20, la salida no cambia respecto a la original (stored 324, cuatro alertas). El regreso de PS-09 tras sus 35 minutos de silencio se acepta porque su salto de 36 está dentro de 36 + 5. Observe que injected_tests_detected indica 3 con la secuencia 1000 porque el conjunto de detección aún no incluye seq_jump_reject: añádalo.

EX087 — Revisión del registro de evidencia. Recuentos: mensajes en la red 327 = 120 (PS-04) + 119 (PS-07, menos el hueco que ocupa la prueba) + 85 (PS-09) + 3 mensajes inyectados; almacenados 324 = 327 − 3 (reproducción rechazada, falsificación rechazada, fuera de rango en cuarentena). Resumen: aplicar el hash a la línea JSON impresa exactamente (sha256 de los bytes sin el salto de línea final) da un resumen que empieza por aa40e3905d178699f1460e1c, igual que el impreso. Límites que hay que declarar: estaciones y valores sintéticos; una sola ejecución de dos horas; sin dispositivos de campo reales, operador de telecomunicaciones ni carga a escala; sin adversario adaptativo; gestión de claves simplificada (clave en el código fuente).