Skip to content

Commit

Permalink
Generate rules with Python
Browse files Browse the repository at this point in the history
  • Loading branch information
robin-nitrokey committed Dec 14, 2024
1 parent 48482a5 commit 14362e9
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 10 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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: |
python3 -m venv venv
. venv/bin/activate
pip install pyright ruff
- name: Run checks
run: . venv/bin/activate && make check

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}=="3eb", 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
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.PHONY: check
check:
ruff check
ruff format --diff
pyright

.PHONY: fix
fix:
ruff check --fix
ruff 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:x}"),
("ATTR{idProduct}", "==", f"{self.pid:x}"),
]
attrs_vid_pid = [
("ATTRS{idVendor}", "==", f"{self.vid:x}"),
("ATTRS{idProduct}", "==", f"{self.pid:x}"),
]
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)

0 comments on commit 14362e9

Please sign in to comment.