From 295f5553fc21c22f6ffdb9f16f32a1ac717242ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 25 Oct 2026 00:50:24 +0300 Subject: [PATCH] pcsc: guide by PolicyKit actions, not a pcscd group A headless/SSH box can show a world-writable /run/pcscd/pcscd.comm (srw-rw-rw- root root) and still be denied: pcscd enforces its PolicyKit policy, whose defaults allow only active local sessions. The startup report, the start.sh pre-flight and the README troubleshooting now name the exact rule (99-pcscd-allow.rules, org.debian.pcsc-lite.access_pcsc and access_card) and explain why the socket mode does not matter. The pcscd-group instruction is gone: there may be no such group, and it is not the gate. The group route stays only as a note for a pcscd built without PolicyKit. Tests: tests/test_pcsc_startup.py asserts the polkit action IDs, the rule filename and the 0666-socket explanation. 1088 Python + 1075 frontend. --- CHANGELOG.md | 8 +++++--- README.md | 14 +++++++++++++- README_RUS.md | 14 +++++++++++++- pysim_simple_server/__main__.py | 16 ++++++++++++---- start.sh | 9 ++++++--- tests/test_pcsc_startup.py | 13 +++++++++++-- 6 files changed, 60 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0907d3d..eadfd65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,9 +32,11 @@ every change carries a descriptive commit message. the user may not use `pcscd`), the error was swallowed and pySim fell back to the serial transport, failing on `/dev/ttyUSB0` while the server still printed "listening". The startup now probes PC/SC and reports **`PC/SC access - denied`** (with the `pcscd` group/polkit guidance and the `pcsc_scan` - cross-check), **`pcscd is not running`** or **`no PC/SC reader found`**, then - exits 2; `start.sh` runs the same access pre-flight before launching. An + denied`** (naming the pcsc-lite PolicyKit actions and the + `99-pcscd-allow.rules` rule — a world-writable `pcscd.comm` does not bypass + polkit's active-session policy — plus the `pcsc_scan` cross-check), + **`pcscd is not running`** or **`no PC/SC reader found`**, then exits 2; + `start.sh` runs the same access pre-flight before launching. An explicit `-d ` keeps the serial path, and a reader present but no card is still a normal cardless start. - **`setup.sh`/`setup.bat` recover from an incomplete `.venv`.** A failed venv diff --git a/README.md b/README.md index 83d4384..ac05014 100644 --- a/README.md +++ b/README.md @@ -800,7 +800,19 @@ its MSVC build chain) is not pulled in. ### Troubleshooting -- **"Failed to establish context: Access denied"** — the user may not use `pcscd`: add the user to the `pcscd` group (`sudo usermod -a -G pcscd $USER`, then log out/in) or configure the polkit rule; start the service if needed (`sudo systemctl enable --now pcscd`). `start.sh` and the server report this and exit instead of falling back to a serial reader; `pcsc_scan` shows the same error. +- **"Failed to establish context: Access denied"** — the user may not use `pcscd`. On a headless/SSH box the socket is often world-writable (`srw-rw-rw- root root`) yet access is still denied: pcscd checks **PolicyKit**, whose default policy allows only *active local* sessions (`allow_active=yes`, `allow_inactive=no`, `allow_any=no`). Allow the user with `/etc/polkit-1/rules.d/99-pcscd-allow.rules` (replace `YOUR_USER`; any `*.rules` file name works): + + ```js + polkit.addRule(function(action, subject) { + if ((action.id == "org.debian.pcsc-lite.access_pcsc" || + action.id == "org.debian.pcsc-lite.access_card") && + subject.user == "YOUR_USER") { + return polkit.Result.YES; + } + }); + ``` + + then `sudo systemctl restart polkit` (or log out/in). Start the service if needed (`sudo systemctl enable --now pcscd`); `start.sh` and the server report this and exit instead of falling back to a serial reader, and `pcsc_scan` shows the same error until the rule is active. If your `pcscd` was built without PolicyKit and the socket is group-owned, adding the user to that group works instead. - **"device file /dev/ttyUSB0 does not exist"** — only with an explicit `-d `: the device is not connected (or the path is wrong). Without `-d` the server no longer uses the serial transport. - **"no PC/SC reader found"** — connect a reader (`pcsc_scan` lists them) and restart; the server exits instead of starting without a reader. - **"Service not available" (0x8010001D) / reader gone after a `pcscd` restart** — the server recovers by itself: a watchdog revives pyscard's presence monitor and the next equip (automatic after re-insertion, or the **Equip** button) recreates the PC/SC transport (releasing the old link first). No server restart needed. diff --git a/README_RUS.md b/README_RUS.md index daaf7c3..4a8b1d1 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -784,7 +784,19 @@ pysim-simple-server --http-port 8080 ### Устранение неполадок -- **"Failed to establish context: Access denied"** — у пользователя нет прав на `pcscd`: добавьте его в группу `pcscd` (`sudo usermod -a -G pcscd $USER`, затем выйдите и войдите) или настройте правило polkit; при необходимости запустите службу (`sudo systemctl enable --now pcscd`). `start.sh` и сервер сообщают об этом и завершаются вместо отката на serial-ридер; `pcsc_scan` показывает ту же ошибку. +- **"Failed to establish context: Access denied"** — у пользователя нет прав на `pcscd`. На headless/SSH-системе сокет часто доступен всем (`srw-rw-rw- root root`), но доступ всё равно запрещён: pcscd проверяет **PolicyKit**, политика по умолчанию разрешает только *активные локальные* сессии (`allow_active=yes`, `allow_inactive=no`, `allow_any=no`). Разрешите пользователя в `/etc/polkit-1/rules.d/99-pcscd-allow.rules` (замените `YOUR_USER`; имя файла `*.rules` может быть любым): + + ```js + polkit.addRule(function(action, subject) { + if ((action.id == "org.debian.pcsc-lite.access_pcsc" || + action.id == "org.debian.pcsc-lite.access_card") && + subject.user == "YOUR_USER") { + return polkit.Result.YES; + } + }); + ``` + + затем `sudo systemctl restart polkit` (или выйдите и войдите). При необходимости запустите службу (`sudo systemctl enable --now pcscd`); `start.sh` и сервер сообщают об этом и завершаются вместо отката на serial-ридер, а `pcsc_scan` показывает ту же ошибку, пока правило не начнёт действовать. Если `pcscd` собран без PolicyKit и сокет принадлежит отдельной группе, вместо этого можно добавить пользователя в неё. - **"device file /dev/ttyUSB0 does not exist"** — только при явном `-d <устройство>`: устройство не подключено (или путь неверен). Без `-d` сервер больше не использует последовательный транспорт. - **"no PC/SC reader found"** — подключите ридер (`pcsc_scan` покажет список) и перезапустите; сервер завершается, а не стартует без ридера. - **"Service not available" (0x8010001D) / ридер пропал после перезапуска `pcscd`** — сервер восстанавливается сам: сторож перезапускает монитор присутствия pyscard, а следующий equip (автоматически после повторной вставки карты или кнопкой **Equip**) пересоздаёт транспорт PC/SC (сначала освобождая старый линк). Перезапуск сервера не нужен. diff --git a/pysim_simple_server/__main__.py b/pysim_simple_server/__main__.py index 8c4c3c7..26425ce 100644 --- a/pysim_simple_server/__main__.py +++ b/pysim_simple_server/__main__.py @@ -65,10 +65,18 @@ def _report_pcsc_unavailable(kind, exc=None): if kind == 'access': user = os.environ.get('USER') or os.environ.get('LOGNAME') or '$USER' sys.stderr.write('INIT: PC/SC access denied - the current user may not use pcscd.\n') - sys.stderr.write(' Add the user to the pcscd group: sudo usermod -aG pcscd %s\n' % user) - sys.stderr.write(' (then log out and back in), or configure the polkit rule that\n') - sys.stderr.write(' allows the user to use pcscd. `pcsc_scan` shows the same\n') - sys.stderr.write(' "SCardEstablishContext: Access denied" error.\n') + sys.stderr.write(' pcscd gates access through PolicyKit: the default policy allows\n') + sys.stderr.write(' only *active local* sessions, so an SSH/headless session is denied\n') + sys.stderr.write(' even when /run/pcscd/pcscd.comm is world-writable (0666).\n') + sys.stderr.write(' Allow the user in /etc/polkit-1/rules.d/99-pcscd-allow.rules:\n') + sys.stderr.write(' polkit.addRule(function(a, s) {\n') + sys.stderr.write(' if ((a.id == "org.debian.pcsc-lite.access_pcsc" ||\n') + sys.stderr.write(' a.id == "org.debian.pcsc-lite.access_card") &&\n') + sys.stderr.write(' s.user == "%s") return polkit.Result.YES;\n' % user) + sys.stderr.write(' });\n') + sys.stderr.write(' then restart polkit (sudo systemctl restart polkit) or log out/in;\n') + sys.stderr.write(' `pcsc_scan` shows the same "SCardEstablishContext: Access denied"\n') + sys.stderr.write(' error until the rule is active.\n') elif kind == 'service': sys.stderr.write('INIT: pcscd is not running or not reachable.\n') sys.stderr.write(' Start it (sudo systemctl start pcscd) and restart the server.\n') diff --git a/start.sh b/start.sh index 659a12f..7ff403b 100755 --- a/start.sh +++ b/start.sh @@ -54,10 +54,13 @@ PY )" || { echo "Error: PC/SC is not accessible: ${PCSC_ERR:-unknown error}" echo " The server will not fall back to a serial reader silently." - echo " - add the user to the pcscd group: sudo usermod -aG pcscd \"$USER\" (then log out/in)," - echo " or configure the polkit rule that allows the user to use pcscd;" + echo " - pcscd uses PolicyKit (the default allows only active local sessions," + echo " so SSH/headless is denied even with a 0666 socket). Allow the user in" + echo " /etc/polkit-1/rules.d/99-pcscd-allow.rules: return polkit.Result.YES for" + echo " org.debian.pcsc-lite.access_pcsc and access_card, subject.user == \"$USER\";" + echo " then 'sudo systemctl restart polkit' (or log out/in);" echo " - 'sudo systemctl start pcscd' if the service is not running;" - echo " - 'pcsc_scan' shows the same 'Access denied' error when access is denied." + echo " - 'pcsc_scan' shows the same 'Access denied' error until the rule is active." exit 2 } READER_ARGS="-p 0" diff --git a/tests/test_pcsc_startup.py b/tests/test_pcsc_startup.py index 4f69910..74f914b 100644 --- a/tests/test_pcsc_startup.py +++ b/tests/test_pcsc_startup.py @@ -102,7 +102,12 @@ class ReportTests(unittest.TestCase): low = text.lower() self.assertIn('access denied', low) self.assertIn('pcscd', low) - self.assertIn('usermod -aG pcscd'.lower(), low) + # polkit, not the socket group, is the gate (the socket can be 0666) + self.assertIn('polkit', low) + self.assertIn('org.debian.pcsc-lite.access_pcsc', low) + self.assertIn('org.debian.pcsc-lite.access_card', low) + self.assertIn('99-pcscd-allow.rules', low) + self.assertIn('0666', low) self.assertIn('pcsc_scan', low) self.assertIn('0x80100027', text) @@ -136,8 +141,12 @@ class SourcePins(unittest.TestCase): def test_the_start_script_probes_access_and_guides(self): src = (Path(m.__file__).parent.parent / 'start.sh').read_text() self.assertIn('from smartcard.System import readers', src) - self.assertIn('usermod -aG pcscd', src) + self.assertIn('polkit', src) + self.assertIn('org.debian.pcsc-lite.access_pcsc', src) + self.assertIn('99-pcscd-allow.rules', src) self.assertIn('pcsc_scan', src) + # the group route is not the gate (the socket may be 0666) + self.assertNotIn('usermod -aG pcscd', src) # the old "is pcscd running" heuristic is gone self.assertNotIn('pgrep -x pcscd', src)