{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cel",
   "metadata": {},
   "source": [
    "# Schnorr w Pythonie: liczby, punkty i dwa zapisy\n",
    "\n",
    "**Kryptografia, II stopień. Laboratoria L2–L4.**\n",
    "\n",
    "Najpierw prześledzimy identyfikację na małych liczbach. Następnie wykonamy\n",
    "ten sam rachunek na BLS12-381, osobno w grupach $G_1$ i $G_2$.\n",
    "Na końcu zmienimy interfejs programu z addytywnego na potęgowy.\n",
    "Zobaczymy też, co zmienia przejście od interaktywnej identyfikacji do podpisu.\n",
    "\n",
    "To **wykonywalny punkt wyjścia**, nie biblioteka produkcyjna ani rozwiązanie\n",
    "wszystkich zadań. Mała grupa modulo 23 jest celowo niebezpieczna.\n",
    "Kod `py_ecc` jest eksperymentalny i nieaudytowany. Nasz Python nie gwarantuje\n",
    "stałego czasu wykonania. Wszystkie klucze i wiadomości są testowe.\n",
    "\n",
    "## Uruchomienie\n",
    "\n",
    "Zainstaluj pakiety z towarzyszącego pliku\n",
    "`kryptografia-ii-python-requirements.txt` w osobnym środowisku.\n",
    "Uruchom JupyterLab z tego środowiska. Wybierz kernel Python tego środowiska,\n",
    "a następnie **Restart Kernel and Run All Cells**.\n",
    "Wyniki zapisane w tym pliku są przykładem poprzedniego wykonania.\n",
    "Nie zastępują wykonania kodu po własnych zmianach."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "srodowisko",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:04.377596Z",
     "iopub.status.busy": "2026-09-18T17:52:04.377346Z",
     "iopub.status.idle": "2026-09-18T17:52:04.399841Z",
     "shell.execute_reply": "2026-09-18T17:52:04.399155Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "py-ecc: 8.0.0\n",
      "gmpy2: 2.3.0\n",
      "cryptography: 50.0.1\n",
      "Python: 3.10.9\n"
     ]
    }
   ],
   "source": [
    "import sys\n",
    "import secrets\n",
    "import hashlib\n",
    "from importlib.metadata import version\n",
    "from IPython.display import display, HTML\n",
    "from html import escape\n",
    "\n",
    "for package in (\"py-ecc\", \"gmpy2\", \"cryptography\"):\n",
    "    print(f\"{package}: {version(package)}\")\n",
    "print(\"Python:\", sys.version.split()[0])\n",
    "\n",
    "checks = []\n",
    "\n",
    "def check(name, condition):\n",
    "    if not condition:\n",
    "        raise AssertionError(name)\n",
    "    checks.append(name)\n",
    "\n",
    "def rejects(name, operation, error=ValueError):\n",
    "    try:\n",
    "        operation()\n",
    "    except error:\n",
    "        check(name, True)\n",
    "    else:\n",
    "        raise AssertionError(name)\n",
    "\n",
    "def table(headers, rows):\n",
    "    def row(values, tag):\n",
    "        return \"<tr>\" + \"\".join(f\"<{tag}>{escape(str(v))}</{tag}>\" for v in values) + \"</tr>\"\n",
    "    display(HTML('<table style=\"font-size:14px;font-variant-numeric:tabular-nums\">'\n",
    "                 + '<thead>' + row(headers, \"th\") + '</thead><tbody>'\n",
    "                 + \"\".join(row(r, \"td\") for r in rows) + '</tbody></table>'))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "modulo-opis",
   "metadata": {},
   "source": [
    "## 1. Identyfikacja na liczbach całkowitych\n",
    "\n",
    "W podgrupie $\\langle g\\rangle\\subset\\mathbb Z_p^*$ rzędu pierwszego $q$\n",
    "klucz publiczny to $A=g^a\\bmod p$. Dowodzący losuje $u$ i wysyła\n",
    "$X=g^u\\bmod p$. **Dopiero potem** weryfikator losuje $c\\in\\mathbb Z_q$.\n",
    "Odpowiedź ma postać $s=(u+a\\cdot c)\\bmod q$.\n",
    "Weryfikator sprawdza $g^s=X\\cdot A^c\\pmod p$.\n",
    "\n",
    "Poniżej ustalamy testowe liczby, aby dało się sprawdzić je ręcznie.\n",
    "To ilustracja równania, nie źródło losowości do rzeczywistego protokołu.\n",
    "Mnożenie w grupie liczymy modulo $p$. Skalary liczymy modulo $q$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "modulo-identyfikacja",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:04.401610Z",
     "iopub.status.busy": "2026-09-18T17:52:04.401480Z",
     "iopub.status.idle": "2026-09-18T17:52:04.877803Z",
     "shell.execute_reply": "2026-09-18T17:52:04.876792Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<table style=\"font-size:14px;font-variant-numeric:tabular-nums\"><thead><tr><th>A</th><th>X</th><th>c</th><th>s</th><th>g**s mod p</th><th>X*A**c mod p</th></tr></thead><tbody><tr><td>8</td><td>16</td><td>5</td><td>8</td><td>3</td><td>3</td></tr></tbody></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import gmpy2\n",
    "\n",
    "p, q_small, g_small = 23, 11, 2\n",
    "check(\"pierwsze parametry\", gmpy2.is_prime(p) and gmpy2.is_prime(q_small))\n",
    "check(\"rząd generatora\", (p - 1) % q_small == 0 and g_small != 1\n",
    "      and gmpy2.powmod(g_small, q_small, p) == 1)\n",
    "\n",
    "def mod_pow(base, exponent):\n",
    "    return int(gmpy2.powmod(base, exponent, p))\n",
    "\n",
    "def mod_member(value):\n",
    "    return type(value) is int and 1 < value < p and mod_pow(value, q_small) == 1\n",
    "\n",
    "def scalar(value, order):\n",
    "    return type(value) is int and 0 <= value < order\n",
    "\n",
    "def mod_verify(A, X, c, s):\n",
    "    return (mod_member(A) and mod_member(X)\n",
    "            and scalar(c, q_small) and scalar(s, q_small)\n",
    "            and mod_pow(g_small, s) == X * mod_pow(A, c) % p)\n",
    "\n",
    "a_small, u_small, c_small = 3, 4, 5\n",
    "A_small, X_small = mod_pow(g_small, a_small), mod_pow(g_small, u_small)\n",
    "s_small = (u_small + a_small * c_small) % q_small\n",
    "check(\"mały transkrypt\", mod_verify(A_small, X_small, c_small, s_small))\n",
    "check(\"mały błędny transkrypt\", not mod_verify(A_small, X_small, c_small, (s_small + 1) % q_small))\n",
    "table([\"A\", \"X\", \"c\", \"s\", \"g**s mod p\", \"X*A**c mod p\"],\n",
    "      [[A_small, X_small, c_small, s_small, mod_pow(g_small, s_small),\n",
    "        X_small * mod_pow(A_small, c_small) % p]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "krzywa-opis",
   "metadata": {},
   "source": [
    "## 2. To samo równanie na krzywej\n",
    "\n",
    "Dla $i\\in\\{1,2\\}$ generator oznaczamy $P_i$.\n",
    "Klucz to $A=a\\cdot P_i$, zobowiązanie to $X=u\\cdot P_i$,\n",
    "a sprawdzane równanie to $s\\cdot P_i=X+c\\cdot A$.\n",
    "`G1` i `G2` są nazwami generatorów w API `py_ecc`.\n",
    "Nie są nazwami zmiennych przechowujących całą grupę.\n",
    "\n",
    "Obie grupy mają ten sam rząd `curve_order`, ale różne typy punktów.\n",
    "`field_modulus` dotyczy współrzędnych, nie odpowiedzi $s$.\n",
    "Do porównania punktów używamy `eq`, nie porównania krotek współrzędnych.\n",
    "Współrzędne projektowe tego samego punktu nie muszą być identyczne.\n",
    "\n",
    "Weryfikator dostaje tylko publiczny transkrypt. Poniższy profil odrzuca\n",
    "neutralny klucz i zobowiązanie, ponieważ sekrety $a,u$ losujemy jako niezerowe.\n",
    "Sam skalar odpowiedzi może być zerowy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "grupy-i-walidacja",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:04.880083Z",
     "iopub.status.busy": "2026-09-18T17:52:04.879863Z",
     "iopub.status.idle": "2026-09-18T17:52:05.229934Z",
     "shell.execute_reply": "2026-09-18T17:52:05.229406Z"
    }
   },
   "outputs": [],
   "source": [
    "from py_ecc.optimized_bls12_381 import (\n",
    "    G1, G2, Z1, Z2, FQ, FQ2, b, b2, curve_order, field_modulus,\n",
    "    add, multiply, neg, eq, is_inf, is_on_curve, pairing,\n",
    ")\n",
    "from py_ecc.bls.point_compression import (\n",
    "    compress_G1, compress_G2, decompress_G1, decompress_G2,\n",
    ")\n",
    "\n",
    "q = curve_order\n",
    "groups = {\"G1\": (G1, Z1, b, FQ), \"G2\": (G2, Z2, b2, FQ2)}\n",
    "\n",
    "def valid_point(group, point, allow_identity=False):\n",
    "    if group not in groups:\n",
    "        return False\n",
    "    _, _, coefficient, coordinate_type = groups[group]\n",
    "    if not isinstance(point, tuple) or len(point) != 3:\n",
    "        return False\n",
    "    if not all(type(v) is coordinate_type for v in point):\n",
    "        return False\n",
    "    return (is_on_curve(point, coefficient)\n",
    "            and is_inf(multiply(point, q))\n",
    "            and (allow_identity or not is_inf(point)))\n",
    "\n",
    "def encode_point(group, point):\n",
    "    if not valid_point(group, point, allow_identity=True):\n",
    "        raise ValueError(\"Punkt nie należy do zadanej podgrupy\")\n",
    "    words = (compress_G1(point),) if group == \"G1\" else compress_G2(point)\n",
    "    return b\"\".join(int(v).to_bytes(48, \"big\") for v in words)\n",
    "\n",
    "def decode_point(group, data, allow_identity=False):\n",
    "    if group not in groups or type(data) is not bytes:\n",
    "        raise ValueError(\"Nieprawidłowa grupa lub typ kodowania\")\n",
    "    expected = 48 if group == \"G1\" else 96\n",
    "    if len(data) != expected:\n",
    "        raise ValueError(\"Nieprawidłowa długość kodowania\")\n",
    "    words = tuple(int.from_bytes(data[i:i+48], \"big\") for i in range(0, expected, 48))\n",
    "    point = decompress_G1(words[0]) if group == \"G1\" else decompress_G2(words)\n",
    "    if not valid_point(group, point, allow_identity):\n",
    "        raise ValueError(\"Punkt neutralny lub spoza podgrupy\")\n",
    "    if encode_point(group, point) != data:\n",
    "        raise ValueError(\"Kodowanie niekanoniczne\")\n",
    "    return point\n",
    "\n",
    "def id_verify(group, A, X, c, s):\n",
    "    if not (valid_point(group, A) and valid_point(group, X)\n",
    "            and scalar(c, q) and scalar(s, q)):\n",
    "        return False\n",
    "    P = groups[group][0]\n",
    "    return eq(multiply(P, s), add(X, multiply(A, c)))\n",
    "\n",
    "check(\"moduł współrzędnych nie jest rzędem\", field_modulus != q)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "uczciwe-przebiegi",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:05.231313Z",
     "iopub.status.busy": "2026-09-18T17:52:05.231235Z",
     "iopub.status.idle": "2026-09-18T17:52:06.076137Z",
     "shell.execute_reply": "2026-09-18T17:52:06.075640Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<table style=\"font-size:14px;font-variant-numeric:tabular-nums\"><thead><tr><th>Grupa</th><th>Rząd w bitach</th><th>Punkt w bajtach</th><th>Poprawny</th><th>s + 1</th></tr></thead><tbody><tr><td>G1</td><td>255</td><td>48</td><td>True</td><td>False</td></tr><tr><td>G2</td><td>255</td><td>96</td><td>True</td><td>False</td></tr></tbody></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "transcripts = {}\n",
    "for group, (P, _, _, _) in groups.items():\n",
    "    a = 1 + secrets.randbelow(q - 1)\n",
    "    u = 1 + secrets.randbelow(q - 1)\n",
    "    A, X = multiply(P, a), multiply(P, u)\n",
    "    c = secrets.randbelow(q)  # Wyzwanie po utworzeniu zobowiązania.\n",
    "    s = (u + a * c) % q\n",
    "    transcripts[group] = (A, X, c, s)\n",
    "    check(group + \" identyfikacja\", id_verify(group, A, X, c, s))\n",
    "    check(group + \" zmiana odpowiedzi\", not id_verify(group, A, X, c, (s + 1) % q))\n",
    "    check(group + \" zmiana wyzwania\", not id_verify(group, A, X, (c + 1) % q, s))\n",
    "    check(group + \" skalar poza zakresem\", not id_verify(group, A, X, c, s + q))\n",
    "    check(group + \" neutralny klucz\", not id_verify(group, groups[group][1], X, c, s))\n",
    "    check(group + \" serializacja\", eq(decode_point(group, encode_point(group, A)), A))\n",
    "\n",
    "table([\"Grupa\", \"Rząd w bitach\", \"Punkt w bajtach\", \"Poprawny\", \"s + 1\"],\n",
    "      [[name, q.bit_length(), len(encode_point(name, values[0])),\n",
    "        id_verify(name, *values), id_verify(name, *values[:3], (values[3]+1) % q)]\n",
    "       for name, values in transcripts.items()])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "podpis-opis",
   "metadata": {},
   "source": [
    "## 3. Identyfikacja a podpis Schnorra\n",
    "\n",
    "W identyfikacji wyzwanie wysyła weryfikator po zobaczeniu $X$.\n",
    "W podpisie nie ma tej interakcji. Podpisujący oblicza\n",
    "$c=\\mathcal H(\\operatorname{enc}(\\mathrm{suite},A,X,m))\\in\\mathbb Z_q$\n",
    "i $s=(u+a\\cdot c)\\bmod q$. Podpis to $(X,s)$.\n",
    "Weryfikator musi sam odtworzyć $c$. Nie przyjmuje go na wiarę od podpisującego.\n",
    "\n",
    "`frame` poprzedza każde pole jego czterobajtową długością.\n",
    "Identyfikator dziedziny odróżnia to użycie hasza od innych protokołów.\n",
    "Identyfikator grupy odróżnia $G_1$, $G_2$ i małą grupę modularną.\n",
    "Wiadomość jest ciągiem bajtów. Tekst kodujemy jawnie jako UTF-8.\n",
    "\n",
    "W tym **własnym profilu dydaktycznym** używamy 64 bajtów SHAKE-256\n",
    "i redukcji modulo rząd. Dla rzędu BLS12-381 mniejszego niż $2^{255}$\n",
    "niejednostajność samej redukcji 512-bitowego wyniku jest ograniczona\n",
    "przez $q/2^{512}<2^{-257}$ przy jednostajnym wejściu redukcji.\n",
    "Nie jest to dowód, że rzeczywisty SHAKE jest wyrocznią losową.\n",
    "Nie deklarujemy zgodności z gotowym standardem podpisu Schnorra."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "podpis-funkcje",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:06.077477Z",
     "iopub.status.busy": "2026-09-18T17:52:06.077383Z",
     "iopub.status.idle": "2026-09-18T17:52:07.265528Z",
     "shell.execute_reply": "2026-09-18T17:52:07.265072Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "G1 : podpis przyjęty, kontrolowane zmiany odrzucone\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "G2 : podpis przyjęty, kontrolowane zmiany odrzucone\n"
     ]
    }
   ],
   "source": [
    "def frame(*fields):\n",
    "    if any(type(f) is not bytes or len(f) >= 2**32 for f in fields):\n",
    "        raise ValueError(\"Pola muszą być bajtami z dopuszczalną długością\")\n",
    "    return b\"\".join(len(f).to_bytes(4, \"big\") + f for f in fields)\n",
    "\n",
    "def hash_scalar(order, suite, A_bytes, X_bytes, message):\n",
    "    transcript = frame(b\"PWr/Schnorr/notebook/v1\", suite, A_bytes, X_bytes, message)\n",
    "    return int.from_bytes(hashlib.shake_256(transcript).digest(64), \"big\") % order\n",
    "\n",
    "def challenge(group, A, X, message):\n",
    "    return hash_scalar(q, (\"BLS12-381/\" + group).encode(\"ascii\"),\n",
    "                       encode_point(group, A), encode_point(group, X), message)\n",
    "\n",
    "def sign(group, a, message):\n",
    "    if group not in groups or not scalar(a, q) or a == 0 or type(message) is not bytes:\n",
    "        raise ValueError(\"Nieprawidłowe wejście podpisywania\")\n",
    "    P = groups[group][0]\n",
    "    A = multiply(P, a)\n",
    "    u = 1 + secrets.randbelow(q - 1)\n",
    "    X = multiply(P, u)\n",
    "    c = challenge(group, A, X, message)\n",
    "    return (X, (u + a * c) % q)\n",
    "\n",
    "def verify(group, A, message, signature):\n",
    "    if type(message) is not bytes or not isinstance(signature, tuple) or len(signature) != 2:\n",
    "        return False\n",
    "    X, s = signature\n",
    "    if not (valid_point(group, A) and valid_point(group, X) and scalar(s, q)):\n",
    "        return False\n",
    "    return id_verify(group, A, X, challenge(group, A, X, message), s)\n",
    "\n",
    "message = \"Wiadomość laboratoryjna\".encode(\"utf-8\")\n",
    "for group, (P, _, _, _) in groups.items():\n",
    "    a = 1 + secrets.randbelow(q - 1)\n",
    "    A = multiply(P, a)\n",
    "    signature = sign(group, a, message)\n",
    "    X, s = signature\n",
    "    check(group + \" podpis\", verify(group, A, message, signature))\n",
    "    check(group + \" inna wiadomość\", not verify(group, A, message + b\"!\", signature))\n",
    "    check(group + \" uszkodzony podpis\", not verify(group, A, message, (X, (s + 1) % q)))\n",
    "    check(group + \" inny klucz\", not verify(group, multiply(P, a % (q-1) + 1), message, signature))\n",
    "    check(group + \" skalar podpisu poza zakresem\", not verify(group, A, message, (X, s + q)))\n",
    "    print(group, \": podpis przyjęty, kontrolowane zmiany odrzucone\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "podpis-modulo",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:07.266979Z",
     "iopub.status.busy": "2026-09-18T17:52:07.266879Z",
     "iopub.status.idle": "2026-09-18T17:52:07.270483Z",
     "shell.execute_reply": "2026-09-18T17:52:07.270012Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ta sama para (X, s) działa też dla: Inna wiadomość 15\n",
      "Jedynie 11 możliwych wyzwań. To nie są bezpieczne parametry.\n"
     ]
    }
   ],
   "source": [
    "def small_challenge(A, X, message):\n",
    "    return hash_scalar(q_small, b\"toy/p=23/q=11/g=2\",\n",
    "                       A.to_bytes(1, \"big\"), X.to_bytes(1, \"big\"), message)\n",
    "\n",
    "def small_verify(A, message, signature):\n",
    "    X, s = signature\n",
    "    if not (mod_member(A) and mod_member(X) and scalar(s, q_small)):\n",
    "        return False\n",
    "    return mod_verify(A, X, small_challenge(A, X, message), s)\n",
    "\n",
    "# Stały nonce tylko dla jawnego przykładu liczbowego.\n",
    "c = small_challenge(A_small, X_small, message)\n",
    "s = (u_small + a_small * c) % q_small\n",
    "small_signature = (X_small, s)\n",
    "check(\"podpis w małej grupie\", small_verify(A_small, message, small_signature))\n",
    "check(\"zmieniona odpowiedź modulo\", not small_verify(A_small, message, (X_small, (s+1) % q_small)))\n",
    "# W małej grupie nie zakładamy odrzucenia każdej innej wiadomości.\n",
    "collision = next(m for i in range(10000)\n",
    "                 if (m := f\"Inna wiadomość {i}\".encode()) != message\n",
    "                 and small_challenge(A_small, X_small, m) == c)\n",
    "check(\"kolizja małego wyzwania\", small_verify(A_small, collision, small_signature))\n",
    "print(\"Ta sama para (X, s) działa też dla:\", collision.decode())\n",
    "print(\"Jedynie 11 możliwych wyzwań. To nie są bezpieczne parametry.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "operatory-opis",
   "metadata": {},
   "source": [
    "## 4. Przeciążenie operatorów nie zmienia grupy\n",
    "\n",
    "W nakładce `g ** a` oznacza `multiply(P, a)`, a `X * Y` oznacza\n",
    "`add(X, Y)`. Dzielenie oznacza dodanie punktu przeciwnego.\n",
    "Potęgowanie przez zero daje element neutralny. Ujemny wykładnik\n",
    "jest redukowany modulo $q$. Symbol `^` w Pythonie oznacza XOR.\n",
    "\n",
    "Nie przeciążamy globalnie klas biblioteki. Każdy obiekt nakładki pamięta grupę.\n",
    "Poniższy przykład sprawdza wejścia także przy tworzeniu obiektów.\n",
    "Powtórzone kontrole podgrupy są celowo proste, lecz kosztowne.\n",
    "Nie używaj pomiaru tej nakładki jako pomiaru pojedynczego dodawania punktów."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "operatory",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:07.271644Z",
     "iopub.status.busy": "2026-09-18T17:52:07.271562Z",
     "iopub.status.idle": "2026-09-18T17:52:07.276638Z",
     "shell.execute_reply": "2026-09-18T17:52:07.276201Z"
    }
   },
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass(frozen=True, eq=False)\n",
    "class GroupElement:\n",
    "    group: str\n",
    "    point: tuple\n",
    "    __hash__ = None\n",
    "\n",
    "    def __post_init__(self):\n",
    "        if not valid_point(self.group, self.point, allow_identity=True):\n",
    "            raise ValueError(\"Nieprawidłowy element grupy\")\n",
    "\n",
    "    def same_group(self, other):\n",
    "        if not isinstance(other, GroupElement) or self.group != other.group:\n",
    "            raise TypeError(\"Działanie wymaga dwóch elementów tej samej grupy\")\n",
    "\n",
    "    def __mul__(self, other):\n",
    "        self.same_group(other)\n",
    "        return GroupElement(self.group, add(self.point, other.point))\n",
    "\n",
    "    def __truediv__(self, other):\n",
    "        self.same_group(other)\n",
    "        return GroupElement(self.group, add(self.point, neg(other.point)))\n",
    "\n",
    "    def __pow__(self, exponent, modulo=None):\n",
    "        if type(exponent) is not int or modulo is not None:\n",
    "            raise TypeError(\"Wykładnik musi być całkowity, bez dodatkowego modułu\")\n",
    "        return GroupElement(self.group, multiply(self.point, exponent % q))\n",
    "\n",
    "    def __eq__(self, other):\n",
    "        if not isinstance(other, GroupElement):\n",
    "            return NotImplemented\n",
    "        return self.group == other.group and eq(self.point, other.point)\n",
    "\n",
    "def sign_power(group, a, message):\n",
    "    if not scalar(a, q) or a == 0:\n",
    "        raise ValueError(\"Sekret musi być niezerowym skalarem\")\n",
    "    g = GroupElement(group, groups[group][0])\n",
    "    A = g ** a\n",
    "    u = 1 + secrets.randbelow(q - 1)\n",
    "    X = g ** u\n",
    "    c = challenge(group, A.point, X.point, message)\n",
    "    return X, (u + a * c) % q\n",
    "\n",
    "def verify_power(A, message, signature):\n",
    "    if not isinstance(A, GroupElement) or type(message) is not bytes:\n",
    "        return False\n",
    "    if not isinstance(signature, tuple) or len(signature) != 2:\n",
    "        return False\n",
    "    X, s = signature\n",
    "    if not isinstance(X, GroupElement) or X.group != A.group or not scalar(s, q):\n",
    "        return False\n",
    "    if is_inf(A.point) or is_inf(X.point):\n",
    "        return False\n",
    "    g = GroupElement(A.group, groups[A.group][0])\n",
    "    c = challenge(A.group, A.point, X.point, message)\n",
    "    return g ** s == X * (A ** c)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "zgodnosc-zapisow",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:07.277815Z",
     "iopub.status.busy": "2026-09-18T17:52:07.277731Z",
     "iopub.status.idle": "2026-09-18T17:52:09.240027Z",
     "shell.execute_reply": "2026-09-18T17:52:09.239639Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Równania i kodowania zgodne. Mieszanie grup oraz błędne wejścia odrzucone.\n"
     ]
    }
   ],
   "source": [
    "for group, (P, O, _, _) in groups.items():\n",
    "    g = GroupElement(group, P)\n",
    "    # Te same jawne dane testowe po obu stronach porównania.\n",
    "    a, u, c = 7, 13, 5\n",
    "    A, X, s = g ** a, g ** u, (u + a * c) % q\n",
    "    check(group + \" identyfikacja potęgowa\", g ** s == X * (A ** c))\n",
    "    check(group + \" zgodność klucza\", eq(A.point, multiply(P, a)))\n",
    "    check(group + \" zgodność bajtów\", encode_point(group, X.point) == encode_point(group, multiply(P, u)))\n",
    "    check(group + \" element neutralny\", is_inf((g ** 0).point))\n",
    "    check(group + \" iloraz\", A / A == g ** 0)\n",
    "    check(group + \" ujemny wykładnik\", (g ** -1) * g == g ** 0)\n",
    "    c_fs = challenge(group, A.point, X.point, message)\n",
    "    s_fs = (u + a * c_fs) % q\n",
    "    check(group + \" ten sam podpis w obu zapisach\",\n",
    "          verify(group, A.point, message, (X.point, s_fs))\n",
    "          and verify_power(A, message, (X, s_fs)))\n",
    "    signature = sign_power(group, a, message)\n",
    "    check(group + \" podpis przez nakładkę\", verify_power(A, message, signature))\n",
    "    check(group + \" zgodność weryfikatorów\", verify(group, A.point, message, (signature[0].point, signature[1])))\n",
    "    check(group + \" zmiana wiadomości przez nakładkę\", not verify_power(A, message+b\"!\", signature))\n",
    "    # Równoważna reprezentacja projektowa: mnożymy wszystkie współrzędne.\n",
    "    alternate = tuple(coordinate * 2 for coordinate in P)\n",
    "    check(group + \" równość projektowa\", GroupElement(group, alternate) == g)\n",
    "    check(group + \" kanoniczna serializacja\", encode_point(group, alternate) == encode_point(group, P))\n",
    "    rejects(group + \" odrzucenie neutralnego klucza\", lambda: decode_point(group, encode_point(group, O)))\n",
    "\n",
    "rejects(\"mieszanie grup\", lambda: GroupElement(\"G1\", G1) * GroupElement(\"G2\", G2), TypeError)\n",
    "rejects(\"współrzędne złej grupy\", lambda: GroupElement(\"G1\", G2))\n",
    "rejects(\"błędna długość punktu\", lambda: decode_point(\"G1\", b\"bad\"))\n",
    "rejects(\"błędne kodowanie punktu\", lambda: decode_point(\"G1\", b\"\\xff\" * 48))\n",
    "rejects(\"niecałkowity wykładnik\", lambda: GroupElement(\"G1\", G1) ** 0.5, TypeError)\n",
    "torsion = (FQ(0), FQ(2), FQ(1))\n",
    "check(\"punkt na krzywej poza podgrupą\", is_on_curve(torsion, b) and not valid_point(\"G1\", torsion))\n",
    "print(\"Równania i kodowania zgodne. Mieszanie grup oraz błędne wejścia odrzucone.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "reset-opis",
   "metadata": {},
   "source": [
    "## 5. Dwa transkrypty pozostają widoczne\n",
    "\n",
    "Przy tym samym $u$ otrzymujemy $s_1=u+a\\cdot c_1$ i $s_2=u+a\\cdot c_2$.\n",
    "Jeśli $c_1\\ne c_2$ modulo $q$, to\n",
    "$$a=(s_1-s_2)\\cdot(c_1-c_2)^{-1}\\pmod q.$$\n",
    "Odwrotność jest modularna. Operator `/` na liczbach Pythona nie oblicza tej odwrotności.\n",
    "Nie potrzebujemy wartości $u$, aby wykorzystać reset.\n",
    "Gdy samo $u$ zostanie ujawnione, pojedynczy transkrypt z niezerowym $c$\n",
    "daje $a=(s-u)\\cdot c^{-1}\\pmod q$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "reset-dwa-przebiegi",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:09.241467Z",
     "iopub.status.busy": "2026-09-18T17:52:09.241376Z",
     "iopub.status.idle": "2026-09-18T17:52:09.425086Z",
     "shell.execute_reply": "2026-09-18T17:52:09.424600Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<table style=\"font-size:14px;font-variant-numeric:tabular-nums\"><thead><tr><th>Przebieg</th><th>Nonce</th><th>Wyzwanie</th><th>Odpowiedź</th></tr></thead><tbody><tr><td>1</td><td>13</td><td>5</td><td>48</td></tr><tr><td>2</td><td>13</td><td>9</td><td>76</td></tr><tr><td>Kontrola</td><td>14</td><td>9</td><td>77</td></tr></tbody></table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sekret odzyskany z pierwszych dwóch wierszy: 7\n"
     ]
    }
   ],
   "source": [
    "# Małe jawne sekrety tylko po to, aby tabela była czytelna.\n",
    "a, u, c1, c2 = 7, 13, 5, 9\n",
    "s1, s2 = (u + a*c1) % q, (u + a*c2) % q\n",
    "recovered = (s1-s2) * pow((c1-c2) % q, -1, q) % q\n",
    "control_s2 = (u + 1 + a*c2) % q\n",
    "control_recovered = (s1-control_s2) * pow((c1-c2) % q, -1, q) % q\n",
    "for group, (P, _, _, _) in groups.items():\n",
    "    A, X = multiply(P, a), multiply(P, u)\n",
    "    check(group + \" reset pierwszy\", id_verify(group, A, X, c1, s1))\n",
    "    check(group + \" reset drugi\", id_verify(group, A, X, c2, s2))\n",
    "    check(group + \" kontrola innego nonce\", id_verify(group, A, multiply(P, u+1), c2, control_s2))\n",
    "check(\"ekstrakcja sekretu\", recovered == a)\n",
    "check(\"kontrola niezależnego zobowiązania\", control_recovered != a)\n",
    "check(\"znany nonce\", (s1-u)*pow(c1, -1, q) % q == a)\n",
    "table([\"Przebieg\", \"Nonce\", \"Wyzwanie\", \"Odpowiedź\"],\n",
    "      [[1, u, c1, s1], [2, u, c2, s2], [\"Kontrola\", u+1, c2, control_s2]])\n",
    "print(\"Sekret odzyskany z pierwszych dwóch wierszy:\", recovered)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "hash-do-punktu",
   "metadata": {},
   "source": [
    "## 6. Most do BLS: inny hasz, inne równanie\n",
    "\n",
    "$\\mathcal H$ w Schnorrze daje skalar. Natomiast\n",
    "$\\mathcal H_{G_1}:\\{0,1\\}^*\\to G_1$ daje punkt o nieznanym logarytmie\n",
    "względem ustalonego generatora. Nie zastępuj go przez\n",
    "$\\mathcal H_{\\mathrm{bad}}(m)=\\mu(m)\\cdot P_1$ z jawnym $\\mu(m)$.\n",
    "\n",
    "W przykładzie BLS $A_2=a\\cdot P_2$ i $\\sigma=a\\cdot\\mathcal H_{G_1}(m)$.\n",
    "Równanie to $e(\\sigma,P_2)=e(\\mathcal H_{G_1}(m),A_2)$.\n",
    "**API `py_ecc` przyjmuje punkt $G_2$ jako pierwszy argument.**\n",
    "Dlatego poniższy kod odwraca kolejność argumentów względem zapisu matematycznego.\n",
    "Własna etykieta `DST` oznacza własny profil dydaktyczny, nie standardową ciphersuite BLS."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "pairing",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:09.426300Z",
     "iopub.status.busy": "2026-09-18T17:52:09.426203Z",
     "iopub.status.idle": "2026-09-18T17:52:10.887920Z",
     "shell.execute_reply": "2026-09-18T17:52:10.887405Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hash do punktu i równanie BLS obliczone w py_ecc.\n"
     ]
    }
   ],
   "source": [
    "from py_ecc.bls.hash_to_curve import hash_to_G1\n",
    "\n",
    "DST = b\"PWr/Lab/BLS12381G1_XMD:SHA-256_SSWU_RO_v1\"\n",
    "B = hash_to_G1(message, DST, hashlib.sha256)\n",
    "a = 1 + secrets.randbelow(q - 1)\n",
    "A2, sigma = multiply(G2, a), multiply(B, a)\n",
    "check(\"hash do podgrupy G1\", valid_point(\"G1\", B))\n",
    "check(\"BLS algebra\", pairing(G2, sigma) == pairing(A2, B))\n",
    "check(\"BLS zmieniony podpis\", pairing(G2, add(sigma, G1)) != pairing(A2, B))\n",
    "print(\"Hash do punktu i równanie BLS obliczone w py_ecc.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "samodzielnie",
   "metadata": {},
   "source": [
    "## 7. Rozwiń samodzielnie\n",
    "\n",
    "- **L3:** zamiast przesyłania obiektów między funkcjami prześlij bajty.\n",
    "  Użyj `decode_point` na granicy weryfikatora. Dołącz błędne kodowania,\n",
    "  punkt spoza podgrupy i skalar poza zakresem.\n",
    "- **L3–L4:** napisz własną nakładkę bez zaglądania do powyższej klasy.\n",
    "  Zachowaj wyniki realizacji addytywnej i potęgowej obok siebie.\n",
    "  Identyczne sekrety, nonce i wiadomość mają dać identyczne podpisy.\n",
    "- **L4:** przetestuj wiadomość pustą, polskie znaki, długi tekst,\n",
    "  inny klucz i niezgodną etykietę dziedziny. Odróżnij błąd parsera od odrzucenia równania.\n",
    "- **L2:** zaimplementuj przeciwnika, który dostaje tylko dwa transkrypty,\n",
    "  a następnie sam odzyskuje sekret. Kontrola poprawności poza przeciwnikiem może znać sekret.\n",
    "- **L9:** mierz oddzielnie `multiply`, `add`, haszowanie, kontrolę podgrupy\n",
    "  i pełną weryfikację. Wypisywanie wartości nie należy do mierzonej operacji.\n",
    "  Dopiero później porównaj z MCL dla uzgodnionych parametrów.\n",
    "\n",
    "Pomyślny test równania nie jest dowodem niepodrabialności, wiedzy ani prywatności.\n",
    "Gotowy raport ma zawierać zarówno dodatnie, jak i ujemne przypadki.\n",
    "\n",
    "## Źródła\n",
    "\n",
    "- [py_ecc 8.0.0: kod i warunki użycia](https://github.com/ethereum/py_ecc/tree/v8.0.0)\n",
    "- [gmpy2: liczby całkowite i arytmetyka modularna](https://gmpy2.readthedocs.io/en/stable/mpz.html)\n",
    "- [Python: przeciążanie operatorów](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types)\n",
    "- [RFC 9380: hash-to-curve](https://www.rfc-editor.org/rfc/rfc9380.html)\n",
    "- [Python: kryptograficzna losowość](https://docs.python.org/3/library/secrets.html)\n",
    "\n",
    "Biblioteki instalujemy jako zależności, nie kopiujemy ich kodu do notebooka.\n",
    "`py_ecc` jest udostępniane na licencji MIT, `gmpy2` na LGPL-3.0-or-later."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "podsumowanie",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-18T17:52:10.889420Z",
     "iopub.status.busy": "2026-09-18T17:52:10.889313Z",
     "iopub.status.idle": "2026-09-18T17:52:10.891161Z",
     "shell.execute_reply": "2026-09-18T17:52:10.890714Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Zakończono. Liczba pomyślnych kontroli: 74\n",
      "To testy wykonania, nie dowód bezpieczeństwa.\n"
     ]
    }
   ],
   "source": [
    "print(f\"Zakończono. Liczba pomyślnych kontroli: {len(checks)}\")\n",
    "print(\"To testy wykonania, nie dowód bezpieczeństwa.\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
