Skip to content

How commands are signed

A computer running Tealhand Host does only what a command signed by a device on your owner-signed device list asks: your phone with its hardware key, or one of your other computers. This page describes the keys, the command, the checks and the signed device list (the roster), as the code does them.

Key Kind Where Signs
Phone iroh key Ed25519 Keystore-encrypted storage on the phone Nothing: it proves the phone’s network identity in the QUIC handshake
Phone device key P-256 Android Keystore, cannot be exported Every command, every device list, and the connection proof. No prompt
Phone presence key P-256 Android Keystore, signs only after a fingerprint or PIN on each use The commands that need you there, and the connection proof
Computer iroh key Ed25519 The OS credential store, or a 0600 file on Linux without one It proves the computer’s network identity, and signs that computer’s commands to your other computers

Two phone keys are needed because the computer can only know “the person pressed their finger” if the key itself refuses to sign without it. The device key is also the owner key of your device list.

Text forms: a device’s iroh key is thdev: and 64 lowercase hex digits; a P-256 public key is thhw: and 66 lowercase hex digits (the 33-byte compressed point).

A phone’s signatures are ECDSA P-256 over SHA-256, as Android’s SHA256withECDSA makes them. They are stored and sent as 64 raw bytes (r then s), normalised to low-S; DER from Android is accepted and converted.

A computer on your device list signs its commands to your other computers with its iroh key (Ed25519, 64 bytes, checked strictly), the key the list already names for it, so nothing is connected again. It has no presence key, so its commands carry no fingerprint: the computer that receives one skips the fingerprint rule for it and keeps every other check. What it changes is reported to your phone (“Studio PC deleted a.txt on Cloud PC”). Only your phone changes the device list.

CommandBody {
issuer: the iroh key of the phone or computer that sends it (32 bytes)
target: the computer's iroh key (32 bytes)
action: the encoded action (at most 4096 bytes)
nonce: 16 random bytes
issued_at: Unix seconds
expires_at: Unix seconds
}

The keys sign "tealhand-cmd-v1" followed by the encoded body. The signed command carries the body, the device signature and, when present, the presence signature. The phone gives each command a lifetime of 60 seconds; the computer accepts at most 300.

  1. The connection’s peer (the key iroh proved in the handshake) is on the device list and not expired. A phone must have a device key; a computer is checked with its iroh key.
  2. The device signature is valid (a computer’s: its iroh key’s), and the presence signature too if there is one (a computer’s command may not have one). This happens before the body is read, with the key of the connection’s peer, not a key written in the body.
  3. The body decodes strictly (one encoding only, no bytes left over), the action is at most 4096 bytes, and the lifetime is at most 300 seconds.
  4. issuer is the connection’s peer and target is this computer, so a command cannot be replayed from another device or to another computer.
  5. The time: now <= expires_at, issued_at at most 30 seconds in the future, and issued_at not before this Tealhand Host started.
  6. The nonce has not been seen from this issuer. It is then remembered until the command expires. One phone may have at most 512 live nonces and all phones together 16,384; beyond that the answer is Busy.

The nonce is recorded last, so a command that fails any check cannot use one up. A restart empties the nonce memory, which is why a command issued before the start is refused.

Only then does Tealhand Host read the action and check it again: parameters (validate), where it may come from (a phone or another computer may not send local-only actions), the fingerprint rule (PresenceRequired; not for a computer, which has no fingerprint key), and remote control. Each step has its own error: Error codes.

Roster {
version: starts at 1, goes up by one with every change
prev_hash: SHA-256 of the previous version's signed message (all zero for version 1)
owner: the phone's device key
issued_at: Unix seconds
devices: [ { iroh key, name, kind (phone or computer), device key, presence key,
added_at, expires_at (none = no expiry) } ]
}
  • The owner key signs "tealhand-roster-v1" followed by the encoded roster. The hash for the next version’s prev_hash is SHA-256 of that same message, so the signature’s encoding does not change the chain.
  • Connecting a phone and the phone itself use the strict rule: a new version is accepted only if it has the same owner, a higher version and a prev_hash that matches the stored one. An older or forked list is refused.
  • A computer that gets a list pushed by the phone is more forgiving, so that it can catch up after missing versions: any higher version from the same owner is accepted without a prev_hash match (there is one owner key, so there are no forks), after the checks below. The same list again is answered Ok and changes nothing, so the phone can safely retry a push whose answer was lost. A list with a lower or equal version and a different hash is refused (Rollback).
  • At most 64 devices, and at most 32 KiB for the encoded list. Names follow the device name rules (1 to 64 characters, no control or invisible characters).
  • A computer installs its first list only through a completed connection, after the click on the computer.
  • A list pushed later by the phone may remove, rename or expire devices and add computers, but it may not add a phone, change the kind or the keys of a device already listed, or give a new computer any keys. A new phone only comes in through connecting, with a click on the computer, so a thief with an unlocked phone cannot enrol their own key. The computer answers NotAllowed and keeps its list.
  • When a valid next version no longer lists the computer, the computer forgets its saved connection and wipes its rules.
  • When a device leaves the list, the computer closes its connections and cancels the rules it set.

The QR code holds a one-time 16-byte secret.

  • The phone’s hello carries an HMAC-SHA256 under the secret over a transcript of both iroh keys, the QR’s expiry, both of the phone’s hardware keys and its name, starting with "tealhand-pair-v1". The computer’s reply carries an HMAC over the hash of that transcript plus its name and OS.
  • The phone proves it holds its two hardware keys: both sign "tealhand-bind-v1" followed by the hello transcript.
  • The check code is the first 8 characters, in Crockford base32, of SHA-256 over "tealhand-pair-v1", the two iroh keys (smaller first) and the secret. Each device computes it itself; it is never sent or logged.
  • The computer accepts the device list from the phone only if it lists the computer’s own key and its owner is the key the hello announced, and it refuses a second owner.

So a person who photographed the QR code cannot connect silently (the computer asks for a click), and a man in the middle who swapped the QR code ends with a failed connection, not a takeover.