tools/presence_git.py
openaliro/openaliro
Module

presence_git.py

Presence-signed git tags: prove a human was physically present at a release.

tools/presence_git.py18 documented symbols

Overview

Presence-signed git tags: prove a human was physically present at a release.

A GPG signature proves WHO made a tag. It does not prove they were there: a stolen key, a compromised CI runner or a coerced automated pipeline all produce perfectly valid signatures. This adds the orthogonal claim -- that a provisioned credential was physically within a few tens of centimetres of the machine when the tag was made -- measured by UWB time-of-flight. The protocol structure resists a simple relay shortening distance, but this project's relay resistance has not been experimentally measured.

    presence_git.py enroll --port /dev/tty.usbmodem... --name my-dongle
    presence_git.py sign   --tag presence/1.2.0 --port /dev/tty.usbmodem...
    presence_git.py verify --tag presence/1.2.0          # what CI runs

HOW THE NONCE WORKS, AND WHAT THAT COSTS Everywhere else in this protocol the verifier mints a random nonce and remembers it, which makes replay impossible. CI cannot do that: it was not present when the tag was made, so it has no nonce to remember. The nonce is therefore DERIVED from what is being signed:

    nonce = SHA-256("openaliro-presence-tag-v1\0" + tag + "\0" + commit)[:16]

so any verifier can recompute it. The assertion is then cryptographically bound to exactly one (tag name, commit) pair and is worthless for any other.

The honest cost, which must not be buried: a derived nonce binds the proof to an ARTEFACT, not to a MOMENT. The dongle has no trusted wall clock and reports unix_ms = TIME_NONE, so a verifier cannot tell whether the presence happened today or last year. Someone who obtained an assertion for this exact (tag, commit) pair before it was published could publish that tag themselves without being present. That window is narrow -- it requires the assertion before the tag exists -- but it is real. It closes the day the dongle carries attested time, which is precisely why unix_ms is a separate field in the wire format rather than something derived from uptime.

Enrolled keys and allowed credential ids live in .presence/enrolled, committed to the repo so both the trusted dongle and named human are reviewable in history like any other change. The tag carries only a key id; policy always comes from that file, because a tag that carried its own keys would authorize itself.

Stdlib only, except that enroll/sign import pyserial lazily to talk to a real dongle. verify -- the half CI runs -- needs no serial port and no extra package.

depends on presence_verify.py serial.ts  ·  used by presence_service.py

API

Cclass PresenceError(RuntimeError)

tools/presence_git.py:110

Exception raised when a board communication error, timeout, or protocol violation occurs during enrollment, signing, or identity operations.

called by ask, cmd_enroll, cmd_sign, dongle_pubkey, git, import_identity, open_port, read_bare_hex

Fkey_id(point: bytes) -> bytes

tools/presence_git.py:115

Stable 8-byte id for a public key: first 8 bytes of SHA-256(point).

Same construction as aliro_assert_cred_id() uses for credentials, so the two identifier schemes in this system read alike.

called by cmd_enroll, cmd_probe, cmd_sign, read_enrolled

Fbinding_nonce(tag: str, commit: str) -> bytes

tools/presence_git.py:124

The 16-byte challenge nonce a tag's assertion must echo.

called by cmd_nonce, cmd_sign, verify_tag

Ftag_commit(tag: str, cwd=None) -> str

tools/presence_git.py:142

The commit a tag resolves to, dereferencing annotated tags.

called by cmd_nonce, verify_tag  ·  calls git

Ftag_trailer(tag: str, key: str, cwd=None) -> str

tools/presence_git.py:147

One trailer value from a tag message, or "" if absent.

Uses git's own trailer parser rather than scanning the message, because an annotated tag may also carry a PGP signature block and hand-rolled scanning would have to know to step around it.

called by verify_tag  ·  calls git

Fread_enrolled(path=ENROLLED_PATH, root=None) -> dict

tools/presence_git.py:157

Load trusted dongles. Returns {key_id_hex: (name, point, cred_id)}.

called by cmd_enroll, cmd_sign, verify_tag  ·  calls PresenceError, key_id

Fverify_tag(tag: str, max_cm=40, root=None, enrolled_path=ENROLLED_PATH, openssl='openssl')

tools/presence_git.py:192

Verify a tag's presence assertion. Returns (verdict, detail-dict).

A verdict of None means the tag carries no assertion at all, which is not a failure by itself -- callers decide whether an unsigned tag is acceptable.

called by cmd_verify  ·  calls PresenceError, binding_nonce, read_enrolled, tag_commit, tag_trailer

Fwait_ready(ser) -> bool

tools/presence_git.py:242

Poll a newline until the board's shell prompts back. True if it did.

Returning rather than raising on the deadline keeps the diagnosis with the caller: a port that never prompts is usually the wrong port or firmware with no shell at all, and ask() already names both.

called by open_port

Fread_line(ser) -> str

tools/presence_git.py:300

Read one line, or raise on a silent port. Undecodable bytes are not fatal.

Console output is not guaranteed to be clean UTF-8 -- a board reset mid-read puts ROM garbage on the wire -- and a decode error there should look like a line that does not match, not like a crash.

called by ask, import_identity, read_bare_hex  ·  calls PresenceError

Fask(ser, command: str, tag: str, what: str) -> str

tools/presence_git.py:313

Send a console command and return the payload of the first line tagged tag.

Answers are located by tag rather than by position because the dongle's console also carries the log stream and the shell's own echo. That is the whole reason this protocol is lines of text: an interleaved log line is a line that does not match, where in a binary framing it was a corrupted response.

called by dongle_credential, dongle_prove, dongle_pubkey  ·  calls PresenceError, read_line

Fexport_identity(ser) -> str

tools/presence_git.py:394

Read a reader identity + trust blob from a provisioned board, as hex.

called by cmd_clone  ·  calls read_bare_hex

Fimport_identity(ser, blob_hex: str) -> str

tools/presence_git.py:408

Load an identity blob into a board. Returns the console's confirmation line.

called by cmd_clone  ·  calls PresenceError, read_line

Fcmd_nonce(args) -> int

tools/presence_git.py:454

Print the hex-encoded 16-byte binding nonce for the given tag and commit (or the tag's commit if not specified).

calls binding_nonce, tag_commit

Fcmd_probe(args) -> int

tools/presence_git.py:474

Run one complete fresh transaction, range, signature and verification.

calls dongle_credential, dongle_prove, dongle_pubkey, key_id, open_port

Fcmd_clone(args) -> int

tools/presence_git.py:547

Copy a provisioned reader identity onto the dongle over two serial ports.

This is what lets a phone's EXISTING Wallet credential transact with the dongle: the credential was issued against a particular reader identity, so the dongle has to present that same identity rather than be enrolled separately. The blob carries the reader private key, which is exactly why the console command behind it is not compiled in by default.

calls export_identity, import_identity, open_port

Fvalidate_tag_name(tag: str) -> None

tools/presence_git.py:575

Reject a tag outside the presence namespace.

Called before the port is opened on purpose. Signing costs a phone wake and a walk to the reader (an asleep phone refuses every transaction), so a name this tool was never going to accept must fail while the user is still at the keyboard, not after they have stood in front of the board.

called by cmd_sign  ·  calls PresenceError

Fcmd_verify(args) -> int

tools/presence_git.py:645

Verify a tag's presence assertion using enrolled dongle public keys and print the verdict with distance, credential, and commit details, or print not-signed and return 0 if unsigned and --require was not given.

calls verify_tag

Fbuild_parser()

tools/presence_git.py:668

Build and return an argument parser for presence-git subcommands: nonce, probe, enroll, clone, sign, and verify, each with its own required arguments and defaults.

called by main
Undocumented (10)

git, open_port, unhex, read_bare_hex, dongle_pubkey, dongle_credential, dongle_prove, cmd_enroll, cmd_sign, main