# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Anil Jangra
"""
Sign your own evidence claims, so a leaf in the log carries YOUR signature and
not only ours (https://riskrouter.eu/spec#signed).

Standard library only. ECDSA on P-256 over SHA-256; the signature is the raw
64-byte r || s (IEEE P1363), base64. The nonce k comes from the operating
system's CSPRNG (the secrets module).

    from signer import Signer
    signer = Signer.load_or_create("signing-key.json")   # keep this file private
    rr.register_signing_key(signer.public_jwk)            # once
    rr.record_evidence(note, "idd.demands-needs", signer=signer)

The key file is a private JWK ({"kty":"EC","crv":"P-256","x","y","d"}). Publish
the public part (x and y) somewhere you control, so a verifier need not take
our word for whose key it is.
"""
import base64
import hashlib
import json
import os
import secrets
from datetime import datetime, timezone

# NIST P-256 (secp256r1)
P = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
     0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)


def _add(p, q):
    if p is None:
        return q
    if q is None:
        return p
    if p[0] == q[0]:
        if (p[1] + q[1]) % P == 0:
            return None
        lam = (3 * p[0] * p[0] - 3) * pow(2 * p[1], -1, P) % P
    else:
        lam = (q[1] - p[1]) * pow(q[0] - p[0], -1, P) % P
    x = (lam * lam - p[0] - q[0]) % P
    return (x, (lam * (p[0] - x) - p[1]) % P)


def _mul(k, point):
    result = None
    while k:
        if k & 1:
            result = _add(result, point)
        point = _add(point, point)
        k >>= 1
    return result


def _b64url(n):
    return base64.urlsafe_b64encode(n.to_bytes(32, "big")).rstrip(b"=").decode("ascii")


def _from_b64url(s):
    return int.from_bytes(base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)), "big")


def claim_payload(kind, record_digest, signer_key_id, claimed_at):
    """Frozen: riskrouter-evidence-claim|v3|kind|record_digest|signer_key_id|claimed_at."""
    return "|".join(["riskrouter-evidence-claim", "v3", kind, record_digest, signer_key_id, claimed_at])


def key_id(public_jwk):
    """First 16 hex of SHA-256 over the JSON array [x, y], as a witness id is derived."""
    return hashlib.sha256(json.dumps([public_jwk["x"], public_jwk["y"]], separators=(",", ":")).encode("utf-8")).hexdigest()[:16]


def now_claimed_at():
    """The client's own timestamp in the form the log requires: YYYY-MM-DDTHH:MM:SS.ffffffZ."""
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")


class Signer:
    def __init__(self, d):
        if not 1 <= d < N:
            raise ValueError("private key out of range")
        self.d = d
        x, y = _mul(d, G)
        self.public_jwk = {"kty": "EC", "crv": "P-256", "x": _b64url(x), "y": _b64url(y)}
        self.key_id = key_id(self.public_jwk)

    @classmethod
    def generate(cls):
        return cls(secrets.randbelow(N - 1) + 1)

    @classmethod
    def load(cls, path):
        with open(path) as fh:
            jwk = json.load(fh)
        if jwk.get("kty") != "EC" or jwk.get("crv") != "P-256" or "d" not in jwk:
            raise ValueError("%s is not a private P-256 JWK" % path)
        return cls(_from_b64url(jwk["d"]))

    @classmethod
    def load_or_create(cls, path):
        if os.path.exists(path):
            return cls.load(path)
        signer = cls.generate()
        signer.save(path)
        return signer

    def save(self, path):
        jwk = dict(self.public_jwk, d=_b64url(self.d))
        fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)   # private: owner only
        with os.fdopen(fd, "w") as fh:
            json.dump(jwk, fh, indent=2)

    def sign(self, message):
        """ECDSA over SHA-256(message); raw r || s, base64."""
        e = int.from_bytes(hashlib.sha256(message).digest(), "big")
        while True:
            k = secrets.randbelow(N - 1) + 1
            r = _mul(k, G)[0] % N
            if r == 0:
                continue
            s = pow(k, -1, N) * (e + r * self.d) % N
            if s == 0:
                continue
            return base64.b64encode(r.to_bytes(32, "big") + s.to_bytes(32, "big")).decode("ascii")

    def sign_claim(self, kind, record_digest, claimed_at=None):
        """The three fields to send with a digest: signer_key_id, claimed_at, client_signature."""
        claimed_at = claimed_at or now_claimed_at()
        signature = self.sign(claim_payload(kind, record_digest, self.key_id, claimed_at).encode("utf-8"))
        return {"signer_key_id": self.key_id, "claimed_at": claimed_at, "client_signature": signature}
