Files
labstream-organizador/firma.py
Labstream 8b482af652 1.6.2: manifiesto firmado, tiempo estimado en tareas, pruebas automáticas
- firma.py: RSA-2048 PKCS#1 v1.5 SHA-256 sin dependencias; empaquetar.py firma
  el manifiesto y actualizador.py exige la firma (CLAVE_PUBLICA incrustada).
- Barra de tareas: tiempo restante estimado a partir del avance.
- Corregido: APLICANDO nunca llegaba al total y «Moviendo» quedaba fijo,
  bloqueando desconectar la carpeta.
- probar.py: reglas, firma y recorrido completo sobre una copia temporal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 14:48:47 -05:00

135 líneas
4.6 KiB
Python

# -*- coding: utf-8 -*-
r"""
Firma del manifiesto de actualización (RSA-2048, PKCS#1 v1.5, SHA-256) sin dependencias.
La clave privada vive fuera del proyecto, en %USERPROFILE%\.labstream\organizador-firma.json, y solo la
usa empaquetar.py al publicar. La clave pública va incrustada en actualizador.py: un .exe instalado solo
acepta manifiestos firmados con ella, así que aunque alguien pudiera cambiar el archivo en el servidor,
no podría hacer que la app instale un paquete que no salió de aquí.
py -3.12 firma.py generar crea la clave (si no existe) y muestra la pública para pegarla
py -3.12 firma.py probar firma y verifica un texto de prueba
"""
import os
import sys
import json
import hashlib
import secrets
RUTA_PRIVADA = os.path.join(os.path.expanduser("~"), ".labstream", "organizador-firma.json")
BITS = 2048
E = 65537
# DigestInfo de SHA-256 (RFC 8017, apéndice B.1)
_DI_SHA256 = bytes.fromhex("3031300d060960864801650304020105000420")
def _es_primo(n, rondas=40):
if n < 2:
return False
for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
if n % p == 0:
return n == p
d, r = n - 1, 0
while d % 2 == 0:
d //= 2
r += 1
for _ in range(rondas):
a = secrets.randbelow(n - 3) + 2
x = pow(a, d, n)
if x in (1, n - 1):
continue
for _ in range(r - 1):
x = pow(x, 2, n)
if x == n - 1:
break
else:
return False
return True
def _primo(bits):
while True:
c = secrets.randbits(bits) | (1 << (bits - 1)) | 1
if _es_primo(c):
return c
def generar(ruta=RUTA_PRIVADA):
"""Crea la pareja de claves y guarda la privada en `ruta`. Devuelve la pública {n, e}."""
if os.path.exists(ruta):
raise FileExistsError(f"ya hay una clave en {ruta}; no se sobrescribe")
while True:
p, q = _primo(BITS // 2), _primo(BITS // 2)
n = p * q
phi = (p - 1) * (q - 1)
if p != q and n.bit_length() == BITS and phi % E:
break
d = pow(E, -1, phi)
os.makedirs(os.path.dirname(ruta), exist_ok=True)
with open(ruta, "w", encoding="utf-8") as fh:
json.dump({"n": n, "e": E, "d": d}, fh)
try:
os.chmod(ruta, 0o600)
except OSError:
pass
return {"n": n, "e": E}
def cargar_privada(ruta=RUTA_PRIVADA):
with open(ruta, encoding="utf-8") as fh:
return json.load(fh)
def _em(mensaje, k):
"""EMSA-PKCS1-v1_5: 00 01 FF..FF 00 DigestInfo || hash"""
t = _DI_SHA256 + hashlib.sha256(mensaje).digest()
return b"\x00\x01" + b"\xff" * (k - len(t) - 3) + b"\x00" + t
def firmar(mensaje, priv):
"""Devuelve la firma en hex del mensaje (bytes)."""
k = (priv["n"].bit_length() + 7) // 8
m = int.from_bytes(_em(mensaje, k), "big")
return pow(m, priv["d"], priv["n"]).to_bytes(k, "big").hex()
def verificar(mensaje, firma_hex, pub):
"""True si la firma corresponde al mensaje y a la clave pública {n, e}."""
try:
k = (pub["n"].bit_length() + 7) // 8
s = bytes.fromhex(firma_hex)
if len(s) != k:
return False
m = pow(int.from_bytes(s, "big"), pub["e"], pub["n"]).to_bytes(k, "big")
return secrets.compare_digest(m, _em(mensaje, k))
except Exception: # noqa
return False
def texto_manifiesto(m):
"""Lo que se firma: los datos del manifiesto que deciden qué se instala. Cambiar cualquiera invalida la firma."""
app = m.get("app") or {}
return "\n".join(["organilab-manifiesto-v1", str(m.get("version", "")), str(app.get("archivo", "")),
str(app.get("sha256", "")).lower(), str(app.get("size", ""))]).encode("utf-8")
if __name__ == "__main__":
orden = sys.argv[1] if len(sys.argv) > 1 else ""
if orden == "generar":
if os.path.exists(RUTA_PRIVADA):
pub = {k: cargar_privada()[k] for k in ("n", "e")}
print("ya existía la clave en", RUTA_PRIVADA)
else:
print("generando clave RSA-2048 (unos segundos)…")
pub = generar()
print("clave privada guardada en", RUTA_PRIVADA, "— haz copia de seguridad: sin ella no se pueden publicar versiones nuevas")
print("\nCLAVE_PUBLICA para actualizador.py:")
print(f'CLAVE_PUBLICA = {{"e": {pub["e"]}, "n": {pub["n"]}}}')
elif orden == "probar":
priv = cargar_privada()
pub = {"n": priv["n"], "e": priv["e"]}
f = firmar(b"hola", priv)
print("firma ok:", verificar(b"hola", f, pub), "· alterada:", verificar(b"holA", f, pub))
else:
print(__doc__)