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>
Este commit está contenido en:
134
firma.py
Archivo normal
134
firma.py
Archivo normal
@@ -0,0 +1,134 @@
|
||||
# -*- 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__)
|
||||
Referencia en una nueva incidencia
Block a user