What is signed is what the device showed: kind, id, choice, nonce, time and the request text.
msg fields: v=”v1”, kind (answer |
perm | go | order), id (up to 64 characters of [A-Za-z0-9._:@/-]), choice (yes, no, or an option letter; up to 16), nonce (16..64 hex, single use), iat and exp (unix seconds, integers; exp-iat at most 600), text (printable ASCII, 1..156 characters = 6 lines of 26; text that does not fit is a refusal, not a truncation). |
Canonical bytes signed with ed25519: signcheck/v1\n + the lines joined with \n, no trailing newline: v1, kind, id, choice, nonce, iat, exp, text. Every field is validated strictly before signing/verifying, so one message yields exactly one byte string. The signcheck/v1\n prefix separates this signature from any other data signed with the same key.
Device reply (one JSON line): {"msg": {...fields...}, "sig": "<base64, 64 bytes>", "fp": "<16 hex fingerprint>"}.
verify rules: the signature is valid, the key is not revoked, kind/id/text match what the agent asked (otherwise “signed something other than requested”), the time window holds (30 s clock skew tolerance), the nonce is unused (it is recorded only after all other checks pass), and a test key is not accepted for kind go. A “no” decision (choice differs from the expected one) returns DENY, exit code 3.
Exit codes: 0 accepted, 1 rejected (REJECT <code>: <reason>), 3 signature valid but a different choice.