Skip to content

Commit 0b50463

Browse files
committed
Add ntag verify-sdm-mac CLI command
1 parent 968e0d3 commit 0b50463

4 files changed

Lines changed: 579 additions & 73 deletions

File tree

src/schnee/controllers/cli/commands.py

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,37 @@
11
from __future__ import annotations
22

3-
from typing import Annotated
3+
from typing import Annotated, cast
44

55
import typer
66

77
from schnee.controllers.cli.errors import exit_for_service_error
88
from schnee.controllers.cli.output import echo_json, echo_text
9+
from schnee.controllers.cli.parsing import parse_hex
10+
from schnee.ndef import NdefUriPrefix
911
from schnee.services.backend import ListBackendNamesService
1012
from schnee.services.base import ServiceError
1113
from schnee.services.ntag_profile import (
1214
ReadNtagProfileService,
15+
VerifyNtag424SdmMacService,
1316
WriteNdefUrlService,
1417
)
1518
from schnee.utils.ntag.constants import NtagByteLength
1619

1720

18-
def _parse_optional_hex(value: str | None, *, option_name: str) -> bytes | None:
19-
"""Convert an optional CLI hex value to bytes."""
20-
if value is None:
21-
return None
22-
try:
23-
parsed = bytes.fromhex(value)
24-
except ValueError as exc:
25-
msg = "must be valid hexadecimal"
26-
raise typer.BadParameter(msg, param_hint=option_name) from exc
27-
if len(parsed) != NtagByteLength.AES_KEY:
28-
msg = "must be 32 hex characters that decode to 16 bytes"
29-
raise typer.BadParameter(
30-
msg,
31-
param_hint=option_name,
32-
)
33-
return parsed
34-
35-
3621
def register_commands(app: typer.Typer) -> None:
3722
"""Register CLI commands."""
38-
ntag_app = typer.Typer(help="NTAG profile commands.", no_args_is_help=True)
39-
app.add_typer(ntag_app, name="ntag")
4023

4124
@app.callback(invoke_without_command=True)
4225
def root() -> None:
4326
"""NFC/RFID tag authentication and encryption tools."""
4427

28+
_register_root_commands(app)
29+
_register_ntag_commands(app)
30+
31+
32+
def _register_root_commands(app: typer.Typer) -> None:
33+
"""Register root-level CLI commands."""
34+
4535
@app.command("backends")
4636
def backends() -> None:
4737
"""List selectable backend names."""
@@ -53,6 +43,12 @@ def backends() -> None:
5343
for name in names:
5444
echo_text(name)
5545

46+
47+
def _register_ntag_commands(app: typer.Typer) -> None:
48+
"""Register NTAG CLI commands."""
49+
ntag_app = typer.Typer(help="NTAG profile commands.", no_args_is_help=True)
50+
app.add_typer(ntag_app, name="ntag")
51+
5652
@ntag_app.command("read")
5753
def read_ntag_profile(
5854
backend_name: Annotated[
@@ -100,9 +96,10 @@ def write_ndef_url(
10096
] = None,
10197
) -> None:
10298
"""Write a URL NDEF record."""
103-
ntag424_master_key = _parse_optional_hex(
99+
ntag424_master_key = parse_hex(
104100
ntag424_master_key_hex,
105101
option_name="--ntag424-master-key-hex",
102+
byte_length=NtagByteLength.AES_KEY,
106103
)
107104
try:
108105
WriteNdefUrlService.call(
@@ -114,3 +111,98 @@ def write_ndef_url(
114111
)
115112
except ServiceError as exc:
116113
exit_for_service_error(exc)
114+
115+
@ntag_app.command("verify-sdm-mac")
116+
def verify_sdm_mac( # noqa: PLR0913
117+
signed_text: Annotated[
118+
str,
119+
typer.Option(
120+
"--signed-text",
121+
help="Signed text from the mirrored URL.",
122+
),
123+
],
124+
mac_hex: Annotated[
125+
str,
126+
typer.Option(
127+
"--mac",
128+
help="Observed 8-byte SDM MAC as 16 hex characters.",
129+
),
130+
],
131+
sdm_key_hex: Annotated[
132+
str,
133+
typer.Option(
134+
"--sdm-key-hex",
135+
help="SDM file read key as 32 hex characters.",
136+
),
137+
],
138+
ndef_prefix: Annotated[
139+
NdefUriPrefix,
140+
typer.Option(
141+
"--ndef-prefix",
142+
help="NDEF URI prefix token, for example no_prefix or https.",
143+
),
144+
] = NdefUriPrefix.NO_PREFIX,
145+
uid_hex: Annotated[
146+
str | None,
147+
typer.Option(
148+
"--uid",
149+
help="Optional 7-byte mirrored UID as 14 hex characters.",
150+
),
151+
] = None,
152+
counter_hex: Annotated[
153+
str | None,
154+
typer.Option(
155+
"--counter",
156+
help="Optional 3-byte mirrored read counter as 6 hex characters.",
157+
),
158+
] = None,
159+
) -> None:
160+
"""Verify an NTAG 424 DNA SDM MAC."""
161+
sdm_key = cast(
162+
"bytes",
163+
parse_hex(
164+
sdm_key_hex,
165+
option_name="--sdm-key-hex",
166+
byte_length=NtagByteLength.AES_KEY,
167+
),
168+
)
169+
mac = cast(
170+
"bytes",
171+
parse_hex(
172+
mac_hex,
173+
option_name="--mac",
174+
byte_length=NtagByteLength.SDM_MAC,
175+
),
176+
)
177+
uid = parse_hex(
178+
uid_hex,
179+
option_name="--uid",
180+
byte_length=NtagByteLength.UID,
181+
)
182+
counter = parse_hex(
183+
counter_hex,
184+
option_name="--counter",
185+
byte_length=NtagByteLength.SDM_COUNTER,
186+
)
187+
try:
188+
result = VerifyNtag424SdmMacService.call(
189+
VerifyNtag424SdmMacService.Request(
190+
signed_text=signed_text,
191+
mac=mac,
192+
sdm_key=sdm_key,
193+
uid=uid,
194+
counter=counter,
195+
ndef_prefix=ndef_prefix,
196+
),
197+
)
198+
except ServiceError as exc:
199+
exit_for_service_error(exc)
200+
201+
echo_json(
202+
{
203+
"valid": result.valid,
204+
"calculated_mac": result.calculated_mac.hex(),
205+
"ndef_prefix": result.ndef_prefix.value,
206+
"prefix_removed": result.prefix_removed,
207+
}
208+
)
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
from __future__ import annotations
22

3+
import typer
34

4-
def normalize_backend_name(value: str) -> str:
5-
"""Normalize a backend name from CLI input."""
6-
return value.strip()
5+
6+
def parse_hex(
7+
value: str | None,
8+
*,
9+
option_name: str,
10+
byte_length: int,
11+
) -> bytes | None:
12+
"""Convert CLI hex input to bytes with exact length validation."""
13+
if value is None:
14+
return None
15+
try:
16+
parsed = bytes.fromhex(value)
17+
except ValueError as exc:
18+
msg = "must be valid hexadecimal"
19+
raise typer.BadParameter(msg, param_hint=option_name) from exc
20+
if len(parsed) != byte_length:
21+
msg = (
22+
f"must be {byte_length * 2} hex characters that decode to "
23+
f"{byte_length} bytes"
24+
)
25+
raise typer.BadParameter(msg, param_hint=option_name)
26+
return parsed

0 commit comments

Comments
 (0)