Skip to content

Commit

Permalink
Generate rules with Python
Browse files Browse the repository at this point in the history
This patch adds a Python script to generate the udev rules.  The
required data is defined in a TOML file.  This makes the rules easier to
maintain and update.

This causes some small changes in the formatting of the rules.  Also,
the rule for the Nitrokey Storage bootloader is updated to use ATTRS
instead of ATTR for consistency with similar rules.
  • Loading branch information
robin-nitrokey committed Dec 16, 2024
1 parent 48482a5 commit 922e0f9
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 10 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 9 additions & 10 deletions 41-nitrokey.rules
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions devices.toml
Original file line number Diff line number Diff line change
@@ -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
136 changes: 136 additions & 0 deletions generate.py
Original file line number Diff line number Diff line change
@@ -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)
75 changes: 75 additions & 0 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 922e0f9

Please sign in to comment.