Ctrl K
ComunidadGuías…

signing.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
Signatures Squidbrake makes and checks, in plain Python (standard library only, so verify.py runs anywhere).

  Ed25519 (RFC 8032)   signs audit checkpoints: every CHECKPOINT_EVERY entries the gateway signs the chain's
                       fingerprint at that point with its ledger key, whose public half it publishes. Anyone holding an
                       export can then prove nobody edited the history, without trusting the server or its database.
  ECDSA P-256          checks the signature a phone's passkey (Face ID, fingerprint) puts on an approval (WebAuthn).

The maths follows the RFC 8032 reference code and SEC 1; it's slow next to a C library (a few milliseconds a
signature), which is fine for one checkpoint per 50 entries and one approval at a time. When the `cryptography`
package is installed (it comes with the `mcp` dependency) the gateway uses it instead for speed; both give the same
bytes.
"""
from __future__ import annotations

import hashlib
import os

CHECKPOINT_EVERY = 50
CHECKPOINT_FORMAT = "squidbrake-checkpoint/1"


def checkpoint_message(seq: int, head_hash: str) -> bytes:
    """What a checkpoint signature covers: the chain's fingerprint as of entry `seq`."""
    return f"{CHECKPOINT_FORMAT}|{seq}|{head_hash}".encode()


# --------------------------------------------------------------------------- Ed25519 (RFC 8032, section 6)

_p = 2 ** 255 - 19
_L = 2 ** 252 + 27742317777372353535851937790883648493
_d = -121665 * pow(121666, _p - 2, _p) % _p
_I = pow(2, (_p - 1) // 4, _p)


def _sha512_int(*parts: bytes) -> int:
    return int.from_bytes(hashlib.sha512(b"".join(parts)).digest(), "little")


def _add(P, Q):
    A, B = (P[1] - P[0]) * (Q[1] - Q[0]) % _p, (P[1] + P[0]) * (Q[1] + Q[0]) % _p
    C, D = 2 * P[3] * Q[3] * _d % _p, 2 * P[2] * Q[2] % _p
    E, F, G, H = B - A, D - C, D + C, B + A
    return E * F % _p, G * H % _p, F * G % _p, E * H % _p


def _mul(s: int, P):
    Q = (0, 1, 1, 0)
    while s > 0:
        if s & 1:
            Q = _add(Q, P)
        P = _add(P, P)
        s >>= 1
    return Q


def _equal(P, Q) -> bool:
    return (P[0] * Q[2] - Q[0] * P[2]) % _p == 0 and (P[1] * Q[2] - Q[1] * P[2]) % _p == 0


def _recover_x(y: int, sign: int) -> int | None:
    if y >= _p:
        return None
    x2 = (y * y - 1) * pow(_d * y * y + 1, _p - 2, _p)
    if x2 == 0:
        return None if sign else 0
    x = pow(x2, (_p + 3) // 8, _p)
    if (x * x - x2) % _p != 0:
        x = x * _I % _p
    if (x * x - x2) % _p != 0:
        return None
    if (x & 1) != sign:
        x = _p - x
    return x


_gy = 4 * pow(5, _p - 2, _p) % _p
_gx = _recover_x(_gy, 0)
_G = (_gx, _gy, 1, _gx * _gy % _p)


def _compress(P) -> bytes:
    zinv = pow(P[2], _p - 2, _p)
    x, y = P[0] * zinv % _p, P[1] * zinv % _p
    return int.to_bytes(y | ((x & 1) << 255), 32, "little")


def _decompress(s: bytes):
    if len(s) != 32:
        return None
    y = int.from_bytes(s, "little")
    sign = y >> 255
    y &= (1 << 255) - 1
    x = _recover_x(y, sign)
    return None if x is None else (x, y, 1, x * y % _p)


def _expand(secret: bytes) -> tuple[int, bytes]:
    h = hashlib.sha512(secret).digest()
    a = int.from_bytes(h[:32], "little")
    a &= (1 << 254) - 8
    a |= 1 << 254
    return a, h[32:]


def _pure_public_key(secret: bytes) -> bytes:
    return _compress(_mul(_expand(secret)[0], _G))


def _pure_sign(secret: bytes, msg: bytes) -> bytes:
    a, prefix = _expand(secret)
    A = _compress(_mul(a, _G))
    r = _sha512_int(prefix, msg) % _L
    R = _compress(_mul(r, _G))
    h = _sha512_int(R, A, msg) % _L
    return R + int.to_bytes((r + h * a) % _L, 32, "little")


def _pure_verify(public: bytes, msg: bytes, sig: bytes) -> bool:
    if len(public) != 32 or len(sig) != 64:
        return False
    A, R = _decompress(public), _decompress(sig[:32])
    if A is None or R is None:
        return False
    s = int.from_bytes(sig[32:], "little")
    if s >= _L:
        return False
    h = _sha512_int(sig[:32], public, msg) % _L
    return _equal(_mul(s, _G), _add(R, _mul(h, A)))


try:  # the same signatures, faster
    from cryptography.exceptions import InvalidSignature
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
    from cryptography.hazmat.primitives import serialization as _ser

    def ed25519_public_key(secret: bytes) -> bytes:
        return Ed25519PrivateKey.from_private_bytes(secret).public_key().public_bytes(
            _ser.Encoding.Raw, _ser.PublicFormat.Raw)

    def ed25519_sign(secret: bytes, msg: bytes) -> bytes:
        return Ed25519PrivateKey.from_private_bytes(secret).sign(msg)

    def ed25519_verify(public: bytes, msg: bytes, sig: bytes) -> bool:
        try:
            Ed25519PublicKey.from_public_bytes(public).verify(sig, msg)
            return True
        except (InvalidSignature, ValueError):
            return False
except Exception:  # pragma: no cover - standard library only
    ed25519_public_key, ed25519_sign, ed25519_verify = _pure_public_key, _pure_sign, _pure_verify


def new_secret() -> bytes:
    return os.urandom(32)


# --------------------------------------------------------------------------- ECDSA P-256 (verify only), for passkeys

_P256_p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
_P256_a = _P256_p - 3
_P256_b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
_P256_n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
_P256_G = (0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296,
           0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5)


def _ec_add(P, Q):
    if P is None:
        return Q
    if Q is None:
        return P
    if P[0] == Q[0] and (P[1] + Q[1]) % _P256_p == 0:
        return None
    if P == Q:
        lam = (3 * P[0] * P[0] + _P256_a) * pow(2 * P[1], -1, _P256_p) % _P256_p
    else:
        lam = (Q[1] - P[1]) * pow(Q[0] - P[0], -1, _P256_p) % _P256_p
    x = (lam * lam - P[0] - Q[0]) % _P256_p
    return x, (lam * (P[0] - x) - P[1]) % _P256_p


def _ec_mul(k: int, P):
    R = None
    while k:
        if k & 1:
            R = _ec_add(R, P)
        P = _ec_add(P, P)
        k >>= 1
    return R


def _on_curve(x: int, y: int) -> bool:
    return 0 <= x < _P256_p and 0 <= y < _P256_p and (y * y - x * x * x - _P256_a * x - _P256_b) % _P256_p == 0


def der_signature(sig: bytes) -> tuple[int, int] | None:
    """r and s from an ASN.1 DER ECDSA signature (what WebAuthn authenticators return)."""
    try:
        if sig[0] != 0x30:
            return None
        i = 2
        out = []
        for _ in range(2):
            if sig[i] != 0x02:
                return None
            n = sig[i + 1]
            out.append(int.from_bytes(sig[i + 2:i + 2 + n], "big"))
            i += 2 + n
        return out[0], out[1]
    except IndexError:
        return None


def p256_verify(x: int, y: int, msg: bytes, der_sig: bytes) -> bool:
    """ECDSA with SHA-256 over `msg`, public key (x, y), signature in DER."""
    rs = der_signature(der_sig)
    if rs is None or not _on_curve(x, y):
        return False
    r, s = rs
    if not (1 <= r < _P256_n and 1 <= s < _P256_n):
        return False
    e = int.from_bytes(hashlib.sha256(msg).digest(), "big")
    w = pow(s, -1, _P256_n)
    point = _ec_add(_ec_mul(e * w % _P256_n, _P256_G), _ec_mul(r * w % _P256_n, (x, y)))
    return point is not None and point[0] % _P256_n == r