digest: constant-time comparison (rest of WEB F7) #96

Closed
opened 2026-10-05 21:07:19 +00:00 by orion · 7 comments
Collaborator

Goal

std/digest can compare two byte sequences (a MAC, a digest, a token) without the running time depending on where they first differ, so signed cookies, CSRF tokens and snapshots (WEB.md sections 3.6 and 6.4) can verify a Hmac result safely. Today SHA-256, HMAC and PBKDF2 exist in std/digest; Digest == is the derived list equality, which is not specified as constant-time.

Source (design record)

  • WEB.md F7, the "constant-time comparison" part. The rest of F7 (SHA-256, HMAC) is done; the secure random source is not part of this issue.
  • Note: F7 speaks of std/crypto and new natives. In the tree the primitives are std/digest, ordinary TorbScript with no native (docs/standard-library/digest.md), so this issue follows the code, not the F7 wording.

Acceptance criteria

  • A public function in std/digest comparing two Bytes for equality that reads every byte of the shorter-or-equal common length and combines differences by XOR/OR accumulation, with no early exit on the first difference; unequal lengths answer false
  • A Digest can be compared the same way (so hmac.finished() against a received MAC needs no conversion)
  • Tests (std/digest test file): equal inputs, a difference in the first byte, in the last byte, in a middle byte, unequal length, empty against empty, empty against non-empty; a pure-function check that the result matches == on all of them
  • docs/standard-library/digest.md documents the function with a trb check example and states what is promised (no data-dependent early exit in the source) and what is not (no guarantee against a compiler or CPU timing effect)
  • WEB.md F7 marked done for the comparison part (list update only, the design record stays the specification)

Seed

Does not touch the seed, provided it stays ordinary TorbScript in std/digest: no syntax change, no new native, no driver flag, and no std name the compiler looks up by string. If the implementation turns out to need a native, stop and re-plan (two commits, seed refresh).

Size

One PR, one Conventional Commit (feat(digest): constant-time comparison), docs and tests in the same commit.

Dependencies

None.

Open owner question

The public name and shape of the new API is not settled in a design record; see the owner decision comment.

## Goal `std/digest` can compare two byte sequences (a MAC, a digest, a token) without the running time depending on where they first differ, so signed cookies, CSRF tokens and snapshots (WEB.md sections 3.6 and 6.4) can verify a `Hmac` result safely. Today SHA-256, HMAC and PBKDF2 exist in `std/digest`; `Digest ==` is the derived list equality, which is not specified as constant-time. ## Source (design record) - [WEB.md F7](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/WEB.md#L929), the "constant-time comparison" part. The rest of F7 (SHA-256, HMAC) is done; the secure random source is not part of this issue. - Note: F7 speaks of `std/crypto` and new natives. In the tree the primitives are `std/digest`, ordinary TorbScript with no native (docs/standard-library/digest.md), so this issue follows the code, not the F7 wording. ## Acceptance criteria - [ ] A public function in `std/digest` comparing two `Bytes` for equality that reads every byte of the shorter-or-equal common length and combines differences by XOR/OR accumulation, with no early exit on the first difference; unequal lengths answer `false` - [ ] A `Digest` can be compared the same way (so `hmac.finished()` against a received MAC needs no conversion) - [ ] Tests (`std/digest` test file): equal inputs, a difference in the first byte, in the last byte, in a middle byte, unequal length, empty against empty, empty against non-empty; a pure-function check that the result matches `==` on all of them - [ ] `docs/standard-library/digest.md` documents the function with a `trb check` example and states what is promised (no data-dependent early exit in the source) and what is not (no guarantee against a compiler or CPU timing effect) - [ ] WEB.md F7 marked done for the comparison part (list update only, the design record stays the specification) ## Seed Does not touch the seed, provided it stays ordinary TorbScript in `std/digest`: no syntax change, no new native, no driver flag, and no std name the compiler looks up by string. If the implementation turns out to need a native, stop and re-plan (two commits, seed refresh). ## Size One PR, one Conventional Commit (`feat(digest): constant-time comparison`), docs and tests in the same commit. ## Dependencies None. ## Open owner question The public name and shape of the new API is not settled in a design record; see the `owner decision` comment.
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop). Matches WEB.md F7 (constant-time comparison); SHA-256, HMAC and random already exist. No collision with a design record or CONCEPT "Open Questions". Likely a new native in std/digest, hence the two-commit seed rule; to settle in refine. next: refine.

[loop:torbscript] intake: in scope (author orion, label loop). Matches WEB.md F7 (constant-time comparison); SHA-256, HMAC and random already exist. No collision with a design record or CONCEPT "Open Questions". Likely a new native in `std/digest`, hence the two-commit seed rule; to settle in refine. next: refine.
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript until 2026-10-06T06:58:30Z

[loop:torbscript] claim refine by session:loop-torbscript until 2026-10-06T06:58:30Z
Author
Collaborator

[loop:torbscript] owner decision: name and shape of the constant-time comparison API (library design choice, not in a design record).
Options:

  1. Digest.equalsConstantTime(other: Digest): Bool plus a free function constantTimeEqual(a: Bytes, b: Bytes): Bool in std/digest.
  2. Only the free function constantTimeEqual(a: Bytes, b: Bytes): Bool; callers use digest.bytes or Bytes.
  3. A new std/crypto package as F7 words it, with the comparison there (later home for the random source).
    Recommendation: 1. It is where HMAC lives and where WEB.md callers need it, adds one name and no package; std/crypto can still re-export it later. The implementation is plain TorbScript, so no seed refresh.
    Until answered the issue is not labelled ready.
[loop:torbscript] owner decision: name and shape of the constant-time comparison API (library design choice, not in a design record). Options: 1. `Digest.equalsConstantTime(other: Digest): Bool` plus a free function `constantTimeEqual(a: Bytes, b: Bytes): Bool` in `std/digest`. 2. Only the free function `constantTimeEqual(a: Bytes, b: Bytes): Bool`; callers use `digest.bytes` or `Bytes`. 3. A new `std/crypto` package as F7 words it, with the comparison there (later home for the random source). Recommendation: 1. It is where HMAC lives and where WEB.md callers need it, adds one name and no package; `std/crypto` can still re-export it later. The implementation is plain TorbScript, so no seed refresh. Until answered the issue is not labelled `ready`.
Author
Collaborator

[loop:torbscript] refine: body rewritten to the definition of ready (goal, criteria, F7 record, no seed touch, one commit); open owner question on the API name/shape, ready withheld, next: wait for Torben's decision comment, then label ready and implement.

[loop:torbscript] refine: body rewritten to the definition of ready (goal, criteria, F7 record, no seed touch, one commit); open owner question on the API name/shape, `ready` withheld, next: wait for Torben's decision comment, then label `ready` and implement.
Author
Collaborator

[loop:torbscript] owner decision recorded (torben, 2026-10-06): option (a). The primary public API is the method Digest.equalsConstantTime(other) in std/digest; the free function stays as a thin wrapper over the method. Owner direction for the codebase: prefer methods over free functions where they make sense (many free functions predate the current syntax), so implementation, docs and examples lead with the method. Labelled ready.

[loop:torbscript] owner decision recorded (torben, 2026-10-06): option (a). The primary public API is the method `Digest.equalsConstantTime(other)` in `std/digest`; the free function stays as a thin wrapper over the method. Owner direction for the codebase: prefer methods over free functions where they make sense (many free functions predate the current syntax), so implementation, docs and examples lead with the method. Labelled `ready`.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261007 until 2026-10-07T05:32:48Z

[loop:torbscript] claim implement by session:loop-torbscript-20261007 until 2026-10-07T05:32:48Z
argus closed this issue 2026-10-07 01:12:25 +00:00
Author
Collaborator

[loop:torbscript] implement: PR #149 (Digest.equalsConstantTime as the primary method, constantTimeEqual as a thin wrapper, tests, docs, regenerated skills, WEB.md F7 updated; one Conventional Commit, no seed touch). Local sh tools/gates.sh a was red on five gates (format, std index, skills, one doc example), all fixed in the commit and re-verified; forge CI on the PR head was green (state success). The merge gate (argus) approved and landed it as cd9c5500 on main; main push CI was still running when I stopped, so the close stage still has to confirm it. next: close (main CI green on cd9c5500, then close the issue)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/149 (`Digest.equalsConstantTime` as the primary method, `constantTimeEqual` as a thin wrapper, tests, docs, regenerated skills, WEB.md F7 updated; one Conventional Commit, no seed touch). Local `sh tools/gates.sh a` was red on five gates (format, std index, skills, one doc example), all fixed in the commit and re-verified; forge CI on the PR head was green (state success). The merge gate (argus) approved and landed it as cd9c5500 on main; `main` push CI was still running when I stopped, so the close stage still has to confirm it. next: close (main CI green on cd9c5500, then close the issue)
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#96
No description provided.