Newer
Older
veriq / backend / signing.py
from typing import Optional
"""VerIQ — result signing (Ed25519)."""
import base64
import json
import os
from pathlib import Path

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization

KEY_PATH = Path(os.environ.get("VERIQ_KEY_PATH", "/var/lib/veriq/ed25519.key"))

_cached_priv: Optional[Ed25519PrivateKey] = None


def _load_or_create_private_key() -> Ed25519PrivateKey:
    """Load private key from disk; generate and persist on first run."""
    global _cached_priv
    if _cached_priv is not None:
        return _cached_priv
    if KEY_PATH.exists():
        raw = KEY_PATH.read_bytes()
        _cached_priv = Ed25519PrivateKey.from_private_bytes(raw)
    else:
        _cached_priv = Ed25519PrivateKey.generate()
        KEY_PATH.parent.mkdir(parents=True, exist_ok=True)
        KEY_PATH.write_bytes(_cached_priv.private_bytes(
            serialization.Encoding.Raw,
            serialization.PrivateFormat.Raw,
            serialization.NoEncryption(),
        ))
        KEY_PATH.chmod(0o600)
    return _cached_priv


def public_key_b64() -> str:
    """Base64 of raw 32-byte public key — the value published for verification."""
    pub = _load_or_create_private_key().public_key()
    raw = pub.public_bytes(
        serialization.Encoding.Raw,
        serialization.PublicFormat.Raw,
    )
    return base64.b64encode(raw).decode()


def canonical_payload(result_id: str, score: int, percentile: float,
                      bank_version: str, identity: Optional[str],
                      completed_at: str) -> str:
    """Deterministic JSON string that gets signed."""
    obj = {
        "result_id": result_id,
        "score": score,
        "percentile": percentile,
        "bank_version": bank_version,
        "identity": identity,
        "completed_at": completed_at,
    }
    return json.dumps(obj, sort_keys=True, separators=(",", ":"))


def sign_payload(payload: str) -> str:
    sig = _load_or_create_private_key().sign(payload.encode("utf-8"))
    return base64.b64encode(sig).decode()


def verify_payload(payload: str, signature_b64: str) -> bool:
    """Verify with our own public key (server-side re-check on verify page)."""
    try:
        pub = _load_or_create_private_key().public_key()
        pub.verify(base64.b64decode(signature_b64), payload.encode("utf-8"))
        return True
    except Exception:
        return False