- 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>
135 líneas
4.6 KiB
Python
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__)
|