diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..98a0c6a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,30 @@ +name: Continuous integration +on: [push, pull_request] + +jobs: + check: + name: Run checks + runs-on: ubuntu-latest + container: python:3.11 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install dependencies + run: | + pip install poetry + poetry install + - name: Run checks + run: make check PYRIGHT="poetry run pyright" RUFF="poetry run ruff" + + validate: + name: Validate rules + runs-on: ubuntu-latest + container: python:3.11 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Validate rules + run: | + cp 41-nitrokey.rules original + make generate + diff original 41-nitrokey.rules diff --git a/41-nitrokey.rules b/41-nitrokey.rules index e967893..1898c0f 100644 --- a/41-nitrokey.rules +++ b/41-nitrokey.rules @@ -4,7 +4,7 @@ # Here rules in new style should be provided. Matching devices should be tagged with 'uaccess'. # File prefix number should be lower than 73, to be correctly processed by the Udev. # Recommended udev version: >= 188. -# + ACTION!="add|change", GOTO="u2f_end" # Nitrokey U2F @@ -30,20 +30,19 @@ LABEL="u2f_end" SUBSYSTEM!="usb", GOTO="gnupg_rules_end" ACTION!="add", GOTO="gnupg_rules_end" -# USB SmartCard Readers -## Crypto Stick 1.2 +# CryptoStick 1.2 ATTR{idVendor}=="20a0", ATTR{idProduct}=="4107", ENV{ID_SMARTCARD_READER}="1", ENV{ID_SMARTCARD_READER_DRIVER}="gnupg", TAG+="uaccess" -## Nitrokey Pro +# Nitrokey Pro ATTR{idVendor}=="20a0", ATTR{idProduct}=="4108", ENV{ID_SMARTCARD_READER}="1", ENV{ID_SMARTCARD_READER_DRIVER}="gnupg", TAG+="uaccess" -## Nitrokey Pro Bootloader +# Nitrokey Pro Bootloader ATTRS{idVendor}=="20a0", ATTRS{idProduct}=="42b4", TAG+="uaccess" -## Nitrokey Storage +# Nitrokey Storage ATTR{idVendor}=="20a0", ATTR{idProduct}=="4109", ENV{ID_SMARTCARD_READER}="1", ENV{ID_SMARTCARD_READER_DRIVER}="gnupg", TAG+="uaccess" -## Nitrokey Storage Bootloader -ATTR{idVendor}=="03eb", ATTR{idProduct}=="2ff1", TAG+="uaccess" -## Nitrokey Start +# Nitrokey Storage Bootloader +ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2ff1", TAG+="uaccess" +# Nitrokey Start ATTR{idVendor}=="20a0", ATTR{idProduct}=="4211", ENV{ID_SMARTCARD_READER}="1", ENV{ID_SMARTCARD_READER_DRIVER}="gnupg", TAG+="uaccess" -## Nitrokey HSM +# Nitrokey HSM ATTR{idVendor}=="20a0", ATTR{idProduct}=="4230", ENV{ID_SMARTCARD_READER}="1", ENV{ID_SMARTCARD_READER_DRIVER}="gnupg", TAG+="uaccess" LABEL="gnupg_rules_end" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7d9246b --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +PYTHON3 ?= python3 +PYRIGHT ?= pyright +RUFF ?= ruff + +.PHONY: check +check: + $(RUFF) check + $(RUFF) format --diff + $(PYRIGHT) + +.PHONY: fix +fix: + $(RUFF) check --fix + $(PYRIGHT) format + +.PHONY: generate +generate: + $(PYTHON3) generate.py devices.toml 41-nitrokey.rules diff --git a/devices.toml b/devices.toml new file mode 100644 index 0000000..0b6d9df --- /dev/null +++ b/devices.toml @@ -0,0 +1,89 @@ +[[u2f]] +name = "Nitrokey U2F" +vid = 0x2581 +pid = 0xf1d0 +hidraw = true + +[[u2f]] +name = "Nitrokey FIDO U2F" +vid = 0x20a0 +pid = 0x4287 +hidraw = true + +[[u2f]] +name = "Nitrokey FIDO2" +vid = 0x20a0 +pid = 0x42b1 +hidraw = true + +[[u2f]] +name = "Nitrokey 3A Mini/3A NFC/3C NFC" +vid = 0x20a0 +pid = 0x42b2 +hidraw = true + +[[u2f]] +name = "Nitrokey 3A NFC Bootloader/3C NFC Bootloader" +vid = 0x20a0 +pid = 0x42dd +hidraw = true + +[[u2f]] +name = "Nitrokey 3A Mini Bootloader" +vid = 0x20a0 +pid = 0x42e8 +all = true + +[[u2f]] +name = "Nitrokey Passkey" +vid = 0x20a0 +pid = 0x42f3 +hidraw = true + +[[u2f]] +name = "Nitrokey Passkey Bootloader" +vid = 0x20a0 +pid = 0x42f4 +all = true + +[[ccid]] +name = "CryptoStick 1.2" +vid = 0x20a0 +pid = 0x4107 +gnupg = true + +[[ccid]] +name = "Nitrokey Pro" +vid = 0x20a0 +pid = 0x4108 +gnupg = true + +[[ccid]] +name = "Nitrokey Pro Bootloader" +vid = 0x20a0 +pid = 0x42b4 +all = true + +[[ccid]] +name = "Nitrokey Storage" +vid = 0x20a0 +pid = 0x4109 +gnupg = true + +[[ccid]] +name = "Nitrokey Storage Bootloader" +vid = 0x03eb +pid = 0x2ff1 +all = true + +[[ccid]] +name = "Nitrokey Start" +vid = 0x20a0 +pid = 0x4211 +gnupg = true + +[[ccid]] +name = "Nitrokey HSM" +vid = 0x20a0 +pid = 0x4230 +gnupg = true diff --git a/generate.py b/generate.py new file mode 100644 index 0000000..4388034 --- /dev/null +++ b/generate.py @@ -0,0 +1,136 @@ +import argparse +import dataclasses +import textwrap +import tomllib +import typing + + +@dataclasses.dataclass(frozen=True) +class Device: + name: str + vid: int + pid: int + hidraw: bool = False + gnupg: bool = False + all: bool = False + + def generate(self) -> str: + s = f"# {self.name}\n" + attr_vid_pid = [ + ("ATTR{idVendor}", "==", f"{self.vid:04x}"), + ("ATTR{idProduct}", "==", f"{self.pid:04x}"), + ] + attrs_vid_pid = [ + ("ATTRS{idVendor}", "==", f"{self.vid:04x}"), + ("ATTRS{idProduct}", "==", f"{self.pid:04x}"), + ] + uaccess = [("TAG", "+=", "uaccess")] + if self.hidraw: + s += generate_rule( + [("KERNEL", "==", "hidraw*"), ("SUBSYSTEM", "==", "hidraw")] + + attrs_vid_pid + + uaccess + ) + if self.gnupg: + s += generate_rule( + attr_vid_pid + + [ + ("ENV{ID_SMARTCARD_READER}", "=", "1"), + ("ENV{ID_SMARTCARD_READER_DRIVER}", "=", "gnupg"), + ] + + uaccess + ) + if self.all: + s += generate_rule(attrs_vid_pid + uaccess) + return s + + @classmethod + def from_dict(cls, data: dict[str, typing.Any]) -> "Device": + return cls(**data) + + +def generate_rule(matches: typing.Sequence[tuple[str, str, str]]) -> str: + rules = [f'{key}{op}"{value}"' for (key, op, value) in matches] + return ", ".join(rules) + "\n" + + +def generate_u2f(devices: list[Device]) -> str: + output = 'ACTION!="add|change", GOTO="u2f_end"\n' + output += "\n" + for device in devices: + output += device.generate() + output += "\n" + output += 'LABEL="u2f_end"\n' + return output + + +def generate_ccid(devices: list[Device]) -> str: + output = "" + output += 'SUBSYSTEM!="usb", GOTO="gnupg_rules_end"\n' + output += 'ACTION!="add", GOTO="gnupg_rules_end"\n' + output += "\n" + for device in devices: + output += device.generate() + output += "\n" + output += 'LABEL="gnupg_rules_end"\n' + return output + + +def generate(u2f_devices: list[Device], ccid_devices: list[Device]) -> str: + header = """\ + # Copyright (c) Nitrokey GmbH + # SPDX-License-Identifier: CC0-1.0 + + # Here rules in new style should be provided. Matching devices should be tagged with 'uaccess'. + # File prefix number should be lower than 73, to be correctly processed by the Udev. + # Recommended udev version: >= 188. + + """ + + sections = [] + if u2f_devices: + sections.append(generate_u2f(u2f_devices)) + if ccid_devices: + sections.append(generate_ccid(ccid_devices)) + + output = textwrap.dedent(header) + output += "\n\n".join(sections) + # TODO: can we remove this? + output += textwrap.dedent(""" + + # Nitrokey Storage dev Entry + KERNEL=="sd?1", ATTRS{idVendor}=="20a0", ATTRS{idProduct}=="4109", SYMLINK+="nitrospace" + """) + return output + + +def run(input: str, output: str) -> None: + with open(input, "rb") as f: + data = tomllib.load(f) + + u2f_devices = [] + if "u2f" in data: + assert isinstance(data["u2f"], list) + for device in data["u2f"]: + assert isinstance(device, dict) + u2f_devices.append(Device.from_dict(device)) + + ccid_devices = [] + if "ccid" in data: + assert isinstance(data["ccid"], list) + for device in data["ccid"]: + assert isinstance(device, dict) + ccid_devices.append(Device.from_dict(device)) + + rules = generate(u2f_devices, ccid_devices) + with open(output, "w") as f: + f.write(rules) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("input") + parser.add_argument("output") + + args = parser.parse_args() + run(args.input, args.output) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d9c221b --- /dev/null +++ b/poetry.lock @@ -0,0 +1,75 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "pyright" +version = "1.1.390" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.390-py3-none-any.whl", hash = "sha256:ecebfba5b6b50af7c1a44c2ba144ba2ab542c227eb49bc1f16984ff714e0e110"}, + {file = "pyright-1.1.390.tar.gz", hash = "sha256:aad7f160c49e0fbf8209507a15e17b781f63a86a1facb69ca877c71ef2e9538d"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + +[[package]] +name = "ruff" +version = "0.8.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "ffa1adbfa456ece54e1ece4615f9746cfe02098ab2ca093dc2d939fab9d850f4" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f88fb89 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[tool.poetry] +package-mode = false + +[tool.poetry.dependencies] +python = "^3.11" + +[tool.poetry.group.dev.dependencies] +pyright = "^1" +ruff = "^0.8"