170 lines
6.4 KiB
Python
170 lines
6.4 KiB
Python
"""``aliro-bench-test`` — run a single Aliro AUTH0+AUTH1 transaction against
|
|
a personalized card via a PC/SC reader.
|
|
|
|
Drives the Aliro EXPEDITED phase end-to-end:
|
|
|
|
SELECT EXPEDITED -> AUTH0 -> AUTH1 -> decrypt -> verify UD signature
|
|
|
|
Exits 0 on success, non-zero with a clear error on any failure.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import click
|
|
|
|
from aliro_harness.reader.step_up import verify_step_up_m2
|
|
from aliro_harness.reader.transaction import TrustBundle, run_aliro_transaction
|
|
|
|
|
|
@click.command()
|
|
@click.option(
|
|
"--trust-dir",
|
|
required=False,
|
|
default=None,
|
|
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
help="Directory written by 'aliro-trustgen init'. Must contain "
|
|
"reader.pem, access_credential.pem, reader_group_id.bin, "
|
|
"reader_group_sub_id.bin. Required unless --list-readers is given.",
|
|
)
|
|
@click.option(
|
|
"--reader",
|
|
"reader_index",
|
|
type=int,
|
|
default=0,
|
|
show_default=True,
|
|
help="Index into the list of PC/SC readers (use --list-readers to see).",
|
|
)
|
|
@click.option(
|
|
"--list-readers",
|
|
is_flag=True,
|
|
help="List PC/SC readers visible to the host and exit.",
|
|
)
|
|
@click.option(
|
|
"--step-up",
|
|
is_flag=True,
|
|
help="After AUTH1, also exercise Step-Up Milestone 2: SELECT ACCE5502, "
|
|
"INS=0xC9 EXCHANGE, INS=0xC3 ENVELOPE (DeviceRequest) + GET RESPONSE "
|
|
"chaining. Decrypts the DeviceResponse under StepUpSKDevice and asserts "
|
|
"documents[0].issuerSigned.issuerAuth == access_document.bin from --trust-dir.",
|
|
)
|
|
def main(
|
|
trust_dir: Path | None, reader_index: int, list_readers: bool, step_up: bool
|
|
) -> None:
|
|
"""Run a single Aliro AUTH0+AUTH1 transaction against a personalized
|
|
card via PC/SC. Decrypts the response and verifies the UD signature.
|
|
Exits 0 on success, non-zero with a clear error on any failure."""
|
|
# pyscard is imported lazily so --help and unit tests don't require pcscd.
|
|
from smartcard.System import readers
|
|
|
|
available = readers()
|
|
if list_readers:
|
|
if not available:
|
|
click.echo("No PC/SC readers found.", err=True)
|
|
raise SystemExit(1)
|
|
for i, r in enumerate(available):
|
|
click.echo(f"[{i}] {r}")
|
|
return
|
|
|
|
if trust_dir is None:
|
|
raise click.UsageError(
|
|
"--trust-dir is required unless --list-readers is given."
|
|
)
|
|
|
|
if not available:
|
|
raise click.ClickException("No PC/SC readers found. Is pcscd running?")
|
|
if reader_index < 0 or reader_index >= len(available):
|
|
raise click.ClickException(
|
|
f"--reader {reader_index} out of range; only {len(available)} reader(s) available."
|
|
)
|
|
|
|
reader = available[reader_index]
|
|
click.echo(f"Using reader: {reader}")
|
|
|
|
connection = reader.createConnection()
|
|
connection.connect()
|
|
try:
|
|
click.echo(f"Connected. ATR: {bytes(connection.getATR()).hex()}")
|
|
|
|
bundle = TrustBundle.from_trust_dir(trust_dir)
|
|
click.echo(f"Loaded trust artifacts from {trust_dir}")
|
|
|
|
# Lazy: only load the AD if --step-up is set — otherwise this would
|
|
# gratuitously require access_document.bin for the AUTH1-only path.
|
|
expected_ad: bytes | None = None
|
|
if step_up:
|
|
expected_ad = (trust_dir / "access_document.bin").read_bytes()
|
|
|
|
def transmit(apdu: bytes) -> tuple[bytes, int]:
|
|
data, sw1, sw2 = connection.transmit(list(apdu))
|
|
return bytes(data), (sw1 << 8) | sw2
|
|
|
|
result = run_aliro_transaction(transmit=transmit, bundle=bundle)
|
|
step_up_verdict: tuple[bool, str] | None = None
|
|
if step_up and result.ok and result.step_up_sk is not None:
|
|
assert expected_ad is not None # set above whenever step_up is true
|
|
step_up_verdict = verify_step_up_m2(
|
|
transmit, result.step_up_sk, expected_ad
|
|
)
|
|
finally:
|
|
connection.disconnect()
|
|
|
|
if not result.ok:
|
|
# If decrypt succeeded but verify (or another late step) failed,
|
|
# the orchestrator populates the TLV fields anyway — print them so
|
|
# the user at the bench can see WHERE in the pipeline the failure
|
|
# happened rather than just the last error message.
|
|
if result.plaintext is not None:
|
|
click.echo(
|
|
f"RESULT: FAIL \u2014 {result.error}", err=True
|
|
)
|
|
_echo_tlv_breakdown(result)
|
|
_echo_latencies(result)
|
|
click.echo(
|
|
"Hint: the AUTH1 response decrypted, so session keys are "
|
|
"correct. The signature verification failed — check that the "
|
|
"credential public key in the trust dir matches the one "
|
|
"provisioned on the card.",
|
|
err=True,
|
|
)
|
|
raise click.exceptions.Exit(1)
|
|
# Early failure (e.g. non-9000 SW on SELECT/AUTH0/AUTH1) — print
|
|
# whatever per-APDU latencies we collected before bailing so the
|
|
# operator can see how long the failing step actually took.
|
|
click.echo(f"RESULT: FAIL — {result.error}", err=True)
|
|
_echo_latencies(result)
|
|
raise click.exceptions.Exit(1)
|
|
|
|
click.echo("RESULT: OK \u2014 applet round-trip on real hardware.")
|
|
_echo_tlv_breakdown(result)
|
|
_echo_latencies(result)
|
|
|
|
if step_up_verdict is not None:
|
|
ok, msg = step_up_verdict
|
|
if ok:
|
|
click.echo(f"STEP-UP M2: OK \u2014 {msg}")
|
|
else:
|
|
click.echo(f"STEP-UP M2: FAIL \u2014 {msg}", err=True)
|
|
raise click.exceptions.Exit(1)
|
|
|
|
|
|
def _echo_tlv_breakdown(result) -> None:
|
|
cred_pub_len = len(result.credential_pub) if result.credential_pub else 0
|
|
ud_sig_len = len(result.ud_sig) if result.ud_sig else 0
|
|
bitmap_hex = result.signaling_bitmap.hex() if result.signaling_bitmap else ""
|
|
click.echo(f" 0x5A credential_PubK: {cred_pub_len}B")
|
|
click.echo(f" 0x9E UD signature: {ud_sig_len}B")
|
|
click.echo(f" 0x5E signaling_bitmap: 0x{bitmap_hex}")
|
|
|
|
|
|
def _echo_latencies(result) -> None:
|
|
"""Per-APDU wall-clock latencies. Surfaces card-side processing time \u2014
|
|
the AUTH1 number is what matters for the RFAL WTX-cap analysis."""
|
|
if not result.latencies_ms:
|
|
return
|
|
click.echo(" APDU latencies (ms):")
|
|
for step in ("select", "auth0", "auth1"):
|
|
if step in result.latencies_ms:
|
|
click.echo(f" {step:<6} {result.latencies_ms[step]:7.1f}")
|
|
total = sum(result.latencies_ms.values())
|
|
click.echo(f" {'total':<6} {total:7.1f}")
|