#!/bin/sh
# =============================================================================
# veritas-verify — verificación INDEPENDIENTE de un certificado de Veritas UGC
#
# POR QUE EXISTE ESTE ARCHIVO:
#   Mientras la única forma de comprobar un certificado sea nuestra web, la
#   frase "compruébalo sin confiar en nosotros" es falsa. Este script rompe esa
#   dependencia: no contacta con ningún servidor de Veritas, ni siquiera para
#   descargar nada. Todo lo que necesita son los archivos que el usuario ya
#   tiene y herramientas que vienen en cualquier sistema Unix.
#
#   El escenario donde esto se juega la credibilidad del producto entero: el
#   perito de la parte contraria quiere verificar y no se fía de nuestra web.
#
# POR QUE POSIX SH Y NO PYTHON, NODE O RUST:
#   Un perito no va a instalar un entorno para comprobar un archivo. sh, openssl
#   y sha256sum están en cualquier Linux, cualquier macOS y en Git Bash de
#   Windows. Cero instalación, cero confianza en un binario que le demos.
#
# LO QUE NO HACE, DICHO POR ADELANTADO:
#   · No prueba autoría. Ningún sistema criptográfico puede.
#   · No prueba que el contenido sea cierto.
#   · Sin conexión sólo puede verificar pruebas .ots ACTUALIZADAS. Una prueba
#     recién emitida es incompleta por diseño y necesita consultar al
#     calendario. El script lo detecta y lo dice, en vez de fallar en falso.
#
# USO:
#   ./veritas-verify.sh <archivo-original> [directorio-de-pruebas]
#
# CODIGOS DE SALIDA:
#   0  todas las comprobaciones aplicables pasaron
#   1  alguna comprobación FALLO -> la prueba no sostiene lo que afirma
#   2  uso incorrecto o faltan herramientas
# =============================================================================
set -u

VERSION="1.0"
ROJO=''; VERDE=''; AMAR=''; NEG=''
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
  ROJO=$(tput setaf 1); VERDE=$(tput setaf 2); AMAR=$(tput setaf 3); NEG=$(tput sgr0)
fi

FALLOS=0
PASADAS=0
OMITIDAS=0

ok()    { PASADAS=$((PASADAS+1));   printf '  %sOK%s      %s\n'   "$VERDE" "$NEG" "$1"; }
fallo() { FALLOS=$((FALLOS+1));     printf '  %sFALLO%s   %s\n'   "$ROJO"  "$NEG" "$1"; }
omite() { OMITIDAS=$((OMITIDAS+1)); printf '  %somitido%s %s\n'   "$AMAR"  "$NEG" "$1"; }
info()  { printf '           %s\n' "$1"; }

# ---------------------------------------------------------------- utilidades
# sha256 portable: coreutils lo llama sha256sum, BSD/macOS lo llama shasum -a 256.
sha256_de() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | cut -d' ' -f1
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | cut -d' ' -f1
  else
    openssl dgst -sha256 "$1" | sed 's/.*= *//'
  fi
}

# SHA-256 de una secuencia de bytes dada en hexadecimal. Necesario para
# reconstruir el árbol de Merkle: hay que hashear los bytes crudos, no su
# representación en texto hexadecimal.
sha256_de_hex() {
  printf '%s' "$1" | tr -d '\n' | xxd -r -p | openssl dgst -sha256 -binary | xxd -p -c 256 | tr -d '\n'
}

# Lectura de JSON sin depender de jq: el manifiesto lo generamos nosotros y su
# forma es estable. Si jq está disponible, se prefiere.
json_valor() {
  _a=$1; _k=$2
  if command -v jq >/dev/null 2>&1; then
    jq -r --arg k "$_k" '.[$k] // empty' "$_a" 2>/dev/null
  else
    sed -n 's/.*"'"$_k"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_a" | head -1
  fi
}

json_numero() {
  _a=$1; _k=$2
  if command -v jq >/dev/null 2>&1; then
    jq -r --arg k "$_k" '.[$k] // empty' "$_a" 2>/dev/null
  else
    sed -n 's/.*"'"$_k"'"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_a" | head -1
  fi
}

json_lista() {
  _a=$1; _k=$2
  if command -v jq >/dev/null 2>&1; then
    jq -r --arg k "$_k" '.[$k][]? // empty' "$_a" 2>/dev/null
  else
    tr -d ' \n' < "$_a" \
      | sed -n 's/.*"'"$_k"'":\[\([^]]*\)\].*/\1/p' \
      | tr ',' '\n' | tr -d '"'
  fi
}

# ------------------------------------------------------------------- entrada
if [ $# -lt 1 ]; then
  cat <<'AYUDA'
veritas-verify — verificación independiente de un certificado de Veritas UGC

  Uso:  veritas-verify.sh <archivo-original> [directorio-de-pruebas]

  Archivos que busca (todos opcionales salvo el original):
    <nombre>.ots       prueba de anclaje en Bitcoin (OpenTimestamps)
    <nombre>.tsr       token de sello de tiempo RFC 3161
    manifiesto.json    huella esperada y prueba de inclusión en el árbol
    raiz.txt           raíz del registro obtenida por vía independiente
    ca.pem             certificado raíz de la autoridad de sellado

  Este script NO contacta con servidores de Veritas.
AYUDA
  exit 2
fi

ARCHIVO=$1
[ -f "$ARCHIVO" ] || { printf '%sNo existe el archivo: %s%s\n' "$ROJO" "$ARCHIVO" "$NEG"; exit 2; }

DIR=${2:-$(dirname "$ARCHIVO")}
BASE=$(basename "$ARCHIVO")

command -v openssl >/dev/null 2>&1 || { printf 'Falta openssl. Instálalo y repite.\n'; exit 2; }
command -v xxd     >/dev/null 2>&1 || { printf 'Falta xxd. Instálalo y repite.\n';     exit 2; }

printf '\n=== veritas-verify %s ===\n' "$VERSION"
printf 'Archivo:     %s\n' "$ARCHIVO"
printf 'Pruebas en:  %s\n\n' "$DIR"

MANIFIESTO="$DIR/manifiesto.json"

# ============================================================== 1. HUELLA ====
printf '1. Huella del archivo\n'
HUELLA=$(sha256_de "$ARCHIVO")
info "SHA-256: $HUELLA"

ESPERADA=""
[ -f "$MANIFIESTO" ] && ESPERADA=$(json_valor "$MANIFIESTO" sha256)

if [ -n "$ESPERADA" ]; then
  if [ "$HUELLA" = "$ESPERADA" ]; then
    ok "la huella coincide con la certificada"
  else
    fallo "LA HUELLA NO COINCIDE — este no es el archivo que se certificó"
    info "certificada: $ESPERADA"
    info "calculada:   $HUELLA"
  fi
else
  omite "sin manifiesto: compara la huella a mano con la del certificado"
fi

# ======================================================== 2. ANCLAJE BITCOIN =
printf '\n2. Anclaje en Bitcoin (OpenTimestamps)\n'
OTS=""
for cand in "$DIR/$BASE.ots" "$DIR/${BASE%.*}.ots"; do
  [ -f "$cand" ] && { OTS=$cand; break; }
done
[ -z "$OTS" ] && OTS=$(find "$DIR" -maxdepth 1 -name '*.ots' 2>/dev/null | head -1)

if [ -z "${OTS:-}" ] || [ ! -f "$OTS" ]; then
  omite "no se encontró ningún archivo .ots"
else
  info "prueba: $OTS"
  # Una prueba COMPLETA contiene los bytes identificadores de la atestación de
  # bloque de Bitcoin; una PENDIENTE sólo los del calendario. Ambos están
  # fijados en la especificación de OpenTimestamps.
  VOLCADO=$(xxd -p -c 100000 "$OTS" 2>/dev/null | tr -d '\n')
  TIENE_BTC=$(printf '%s' "$VOLCADO" | grep -c '0588960d73d71901' 2>/dev/null || true)
  TIENE_PEND=$(printf '%s' "$VOLCADO" | grep -c '83dfe30d2ef90c8e' 2>/dev/null || true)
  [ -z "$TIENE_BTC" ] && TIENE_BTC=0
  [ -z "$TIENE_PEND" ] && TIENE_PEND=0

  if [ "$TIENE_BTC" -gt 0 ]; then
    ok "contiene atestación de bloque de Bitcoin: la prueba es autocontenida"
    info "verificable sin conexión y sin Veritas"
  elif [ "$TIENE_PEND" -gt 0 ]; then
    omite "prueba PENDIENTE: sólo contiene atestación de calendario"
    info "aún no es autocontenida. Actualízala cuando Bitcoin confirme:"
    info "  ots upgrade \"$OTS\""
  else
    fallo "el archivo .ots no tiene estructura reconocible"
  fi

  # Si el cliente oficial está instalado, se usa: es la comprobación fuerte.
  if command -v ots >/dev/null 2>&1; then
    if ots verify "$OTS" >/dev/null 2>&1; then
      ok "el cliente OpenTimestamps valida la prueba"
    else
      omite "el cliente OpenTimestamps no la validó (puede requerir red)"
    fi
  else
    info "sugerencia: 'pip install opentimestamps-client' para validación completa"
  fi
fi

# ========================================================= 3. SELLO RFC 3161 =
printf '\n3. Sello de tiempo RFC 3161\n'
TSR=""
for cand in "$DIR/$BASE.tsr" "$DIR/${BASE%.*}.tsr"; do
  [ -f "$cand" ] && { TSR=$cand; break; }
done
[ -z "$TSR" ] && TSR=$(find "$DIR" -maxdepth 1 -name '*.tsr' 2>/dev/null | head -1)

if [ -z "${TSR:-}" ] || [ ! -f "$TSR" ]; then
  omite "no se encontró ningún token .tsr"
else
  info "token: $TSR"
  SALIDA=$(openssl ts -reply -in "$TSR" -text 2>/dev/null)
  if [ -n "$SALIDA" ]; then
    ok "el token RFC 3161 tiene estructura válida"

    FECHA=$(printf '%s\n' "$SALIDA" | sed -n 's/^Time stamp: *//p' | head -1)
    [ -n "$FECHA" ] && info "fecha sellada: $FECHA"

    # El token lleva dentro la huella sellada, en hex con separadores. Se
    # normaliza y se busca la nuestra.
    PLANO=$(printf '%s' "$SALIDA" | tr 'ABCDEF' 'abcdef' | tr -d ' :\n')
    if printf '%s' "$PLANO" | grep -q "$(printf '%s' "$HUELLA" | tr 'ABCDEF' 'abcdef')"; then
      ok "el token sella exactamente la huella de este archivo"
    else
      omite "no se pudo confrontar la huella dentro del token automáticamente"
      info "compruébalo a mano: openssl ts -reply -in \"$TSR\" -text"
    fi

    CA=$(find "$DIR" -maxdepth 1 \( -name 'ca*.pem' -o -name '*cacert*.pem' \) 2>/dev/null | head -1)
    if [ -n "$CA" ]; then
      if openssl ts -verify -in "$TSR" -data "$ARCHIVO" -CAfile "$CA" >/dev/null 2>&1; then
        ok "cadena de confianza de la autoridad de sellado verificada"
      else
        fallo "la cadena de confianza NO verifica contra $CA"
      fi
    else
      omite "sin certificado raíz de la autoridad: no se verifica la cadena"
      info "descárgalo de la autoridad y colócalo como ca.pem en este directorio"
    fi
  else
    fallo "el archivo .tsr no es un token RFC 3161 legible"
  fi
fi

# ============================================== 4. REGISTRO DE TRANSPARENCIA =
printf '\n4. Inclusión en el registro de transparencia\n'
if [ ! -f "$MANIFIESTO" ]; then
  omite "sin manifiesto: no hay prueba de inclusión que validar"
else
  HOJA=$(json_valor "$MANIFIESTO" leafHash)
  RAIZ_ESP=$(json_valor "$MANIFIESTO" rootHash)
  INDICE=$(json_numero "$MANIFIESTO" leafIndex)
  TAMANO=$(json_numero "$MANIFIESTO" treeSize)

  if [ -z "$HOJA" ] || [ -z "$RAIZ_ESP" ] || [ -z "$INDICE" ] || [ -z "$TAMANO" ]; then
    omite "el manifiesto no trae una prueba de inclusión completa"
  else
    info "hoja $INDICE de un árbol de $TAMANO"
    # Reconstrucción de la raíz conforme a RFC 6962 §2.1.1. Los nodos internos
    # llevan el prefijo de dominio 0x01; la hoja ya viene hasheada con 0x00.
    # El caso del último nodo impar (nos promocionamos sin hermano) se maneja
    # con la comparación contra el borde derecho del subárbol.
    ACTUAL=$HOJA
    IDX=$INDICE
    RESTO=$TAMANO
    PASOS=0
    for HERMANO in $(json_lista "$MANIFIESTO" auditPath); do
      [ -z "$HERMANO" ] && continue
      # Mientras seamos el último nodo de un nivel impar, subimos sin combinar.
      while [ $((RESTO % 2)) -eq 1 ] && [ "$IDX" -eq $((RESTO - 1)) ]; do
        IDX=$((IDX / 2))
        RESTO=$(( (RESTO + 1) / 2 ))
      done
      if [ $((IDX % 2)) -eq 0 ]; then
        ACTUAL=$(sha256_de_hex "01${ACTUAL}${HERMANO}")
      else
        ACTUAL=$(sha256_de_hex "01${HERMANO}${ACTUAL}")
      fi
      PASOS=$((PASOS+1))
      IDX=$((IDX / 2))
      RESTO=$(( (RESTO + 1) / 2 ))
    done

    if [ "$ACTUAL" = "$RAIZ_ESP" ]; then
      ok "la prueba de inclusión reconstruye la raíz declarada ($PASOS pasos)"
    else
      fallo "LA PRUEBA DE INCLUSION NO CUADRA"
      info "reconstruida: $ACTUAL"
      info "declarada:    $RAIZ_ESP"
    fi

    # La raíz del manifiesto la escribe Veritas. Para que la comprobación sea
    # de verdad independiente hay que confrontarla con una obtenida por otra
    # vía: el espejo público o el anclaje en Bitcoin de esa raíz.
    if [ -f "$DIR/raiz.txt" ]; then
      RAIZ_IND=$(tr -d ' \n\r' < "$DIR/raiz.txt")
      if [ "$RAIZ_IND" = "$RAIZ_ESP" ]; then
        ok "la raíz coincide con la obtenida por vía independiente"
      else
        fallo "la raíz del manifiesto NO coincide con raiz.txt"
      fi
    else
      omite "sin raiz.txt: la raíz sólo se confronta contra el propio manifiesto"
      info "para independencia total, obtén la raíz del espejo público"
    fi
  fi
fi

# ==================================================================== RESUMEN
printf '\n--------------------------------------------------\n'
printf 'Comprobaciones: %s%d correctas%s, %s%d fallidas%s, %s%d omitidas%s\n' \
  "$VERDE" "$PASADAS" "$NEG" "$ROJO" "$FALLOS" "$NEG" "$AMAR" "$OMITIDAS" "$NEG"

if [ "$FALLOS" -gt 0 ]; then
  printf '\n%sRESULTADO: HAY COMPROBACIONES FALLIDAS.%s\n' "$ROJO" "$NEG"
  printf 'La prueba no sostiene lo que el certificado afirma. Resuelve cada\n'
  printf 'fallo antes de presentarlo en ningún sitio.\n'
  exit 1
fi

if [ "$PASADAS" -eq 0 ]; then
  printf '\n%sRESULTADO: NO SE PUDO COMPROBAR NADA.%s\n' "$AMAR" "$NEG"
  printf 'Faltan los archivos de prueba. Descarga el paquete completo.\n'
  exit 2
fi

printf '\n%sRESULTADO: todas las comprobaciones aplicables pasaron.%s\n' "$VERDE" "$NEG"
cat <<'LIMITES'

Lo que esto acredita:
  · el archivo tiene exactamente la huella certificada
  · esa huella existía antes del momento acreditado
  · el registro no ha sido alterado a posteriori

Lo que NO acredita, y conviene no afirmarlo:
  · quién es el autor
  · que el contenido sea veraz
  · que no infrinja derechos de terceros

Ninguna de estas comprobaciones ha contactado con servidores de Veritas.
LIMITES
exit 0
