"""``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}")