presence_git.py
Presence-signed git tags: prove a human was physically present at a release.
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)
Exception raised when a board communication error, timeout, or protocol violation occurs during enrollment, signing, or identity operations.
ask, cmd_enroll, cmd_sign, dongle_pubkey, git, import_identity, open_port, read_bare_hexFkey_id(point: bytes) -> bytes
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.
cmd_enroll, cmd_probe, cmd_sign, read_enrolledFbinding_nonce(tag: str, commit: str) -> bytes
The 16-byte challenge nonce a tag's assertion must echo.
cmd_nonce, cmd_sign, verify_tagFtag_commit(tag: str, cwd=None) -> str
The commit a tag resolves to, dereferencing annotated tags.
cmd_nonce, verify_tag · calls gitFtag_trailer(tag: str, key: str, cwd=None) -> str
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.
verify_tag · calls gitFread_enrolled(path=ENROLLED_PATH, root=None) -> dict
Load trusted dongles. Returns {key_id_hex: (name, point, cred_id)}.
cmd_enroll, cmd_sign, verify_tag · calls PresenceError, key_idFverify_tag(tag: str, max_cm=40, root=None, enrolled_path=ENROLLED_PATH, openssl='openssl')
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.
cmd_verify · calls PresenceError, binding_nonce, read_enrolled, tag_commit, tag_trailerFwait_ready(ser) -> bool
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.
open_portFread_line(ser) -> str
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.
ask, import_identity, read_bare_hex · calls PresenceErrorFask(ser, command: str, tag: str, what: str) -> str
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.
dongle_credential, dongle_prove, dongle_pubkey · calls PresenceError, read_lineFexport_identity(ser) -> str
Read a reader identity + trust blob from a provisioned board, as hex.
cmd_clone · calls read_bare_hexFimport_identity(ser, blob_hex: str) -> str
Load an identity blob into a board. Returns the console's confirmation line.
cmd_clone · calls PresenceError, read_lineFcmd_nonce(args) -> int
Print the hex-encoded 16-byte binding nonce for the given tag and commit (or the tag's commit if not specified).
binding_nonce, tag_commitFcmd_probe(args) -> int
Run one complete fresh transaction, range, signature and verification.
dongle_credential, dongle_prove, dongle_pubkey, key_id, open_portFcmd_clone(args) -> int
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.
export_identity, import_identity, open_portFvalidate_tag_name(tag: str) -> None
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.
cmd_sign · calls PresenceErrorFcmd_verify(args) -> int
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.
verify_tagFbuild_parser()
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.
mainUndocumented (10)
git, open_port, unhex, read_bare_hex, dongle_pubkey, dongle_credential, dongle_prove, cmd_enroll, cmd_sign, main