Skip to content

v0.1.2

Latest

Choose a tag to compare

@github-actions github-actions released this 04 Oct 17:00
3258e8d

Unsigned evaluation release. Not approved for high-value custody. This
release fixes review, desktop and packaging problems in v0.1.1. It holds no keys
and does not enforce what a separate wallet signs. Independent post-fix review,
platform code signing and production-hardware validation remain outstanding.
Use public fixtures or disposable test workflows for evaluation.

Release checks - 4 October 2026

  • Add native launch checks for the MSI and AppImage, alongside Debian and NSIS.
    Remove the installed copy before testing the next format.
  • Verify the existing candidate manifest before publishing instead of generating
    new checksums. Reject stale installer versions, unexpected files, incomplete
    checksum lists and mismatched source metadata.
  • Check version consistency on branch builds too, including npm lock metadata.
  • Give oversized desktop IPC requests the same refusal response schema as other
    invalid input, and test this at the native Rust boundary.

Fixed - direct review on 3 October 2026

  • Backport the upstream glib VariantStrIter fix for the Linux desktop's GTK3
    dependency. The original optimized iterator tests crashed; all 11 pass with
    the two-line correction. Verify the vendored source against the original crate.
  • Test installed Debian and Windows NSIS applications through their native
    WebViews, and validate complete release candidates on branch builds.
  • Enable the Tauri JavaScript bridge required by the desktop backend.
  • Declare and generate UTF-8 explicitly: the packaged macOS window previously
    misdecoded text and failed to initialize its controls despite browser tests.
  • Discover Windows Chrome installations and use portable file URLs in the
    installer smoke tests.
  • Bind fetched records to the requested transaction hash, cancel stale network
    requests, and prevent delayed file reads from replacing newer input.
  • Limit network response bytes while streaming, before collecting the body.
  • Reject duplicate JSON keys, malformed chain IDs, and absent signed numeric
    fields rather than replacing them with defaults.
  • Gate publishing on tests and version checks; compare canonical binaries with
    the committed hashes rather than the build script's freshly written file.
  • Include installers and the standalone HTML page in SHA256SUMS, publish build
    metadata, require every platform's artifacts, and use locked dependencies.
  • Reject empty allocator test runs and harness errors instead of inferring
    success from the absence of failure lines in a log.

This was an AI-assisted code review with regression tests, not an independent
human audit. Hardware validation and independent re-review remain outstanding.

Fixed — two items from a re-check

The cap was tested beside the gate, not at it. The test asserted
too_many_to_acknowledge(), the predicate approve consults — so deleting the
check from approve itself would have left it green. There is now a test in
clearsign-keys/tests/approval_boundary.rs that goes through approve, at the
cap and one over, with both an empty and a complete acknowledgement list.
Checked against the mutation it exists for: removing the gate's check fails it.

The selector qualification named only the first destination. It was
deduplicated on its code, so a batch touching four contracts carried a single
notice naming one of them — which reads as though the limitation applies to that
address and not the other three. There is now one notice per distinct
destination.

Changed — how the review passes are described

The v0.1.1 and v0.1.2 entries said "a second independent review" and "the same
reviewer". Those rounds were AI-assisted review passes, not a human security
audit, and the reviewer said so plainly: they cannot accept payment, enter a
consulting contract, or provide a professional auditor's attestation.

The wording is corrected here, and 03-verification-status.md now separates the
one human review in September from the four AI-assisted passes since. The
second human review is still unmet.
It matters that the distinction is made by
us rather than discovered by someone else.

Fixed — the window was broken

MAX_RECORD_BYTES was referenced in three places and declared in none.
Fetching a transaction or dropping a file threw ReferenceError and the page
did nothing. Introduced when input bounding was added: the edit targeted
<script> and the tag is <script type="module">, so it silently never
applied. Every Rust test passed, the build's parse check passed, and the page
shipped broken.

There is now a smoke test — app/smoke-test.mjs — that loads the built page in
a real browser and uses it: reviews the Bybit fixture, drops an oversized file,
edits the input, clears, and fails on anything the page throws. It runs as part
of app/build.sh and in CI. Checked against the bug it exists for: with the
constant removed, three of its checks fail.

Writing it found a second one. The oversized-file branch called show(...),
which is not a function this page has.

A finished review could outlive the input that produced it. The review flow
had no request generation, so a slow fetch could land after a fast one, or after
Clear, and leave a result on screen belonging to a transaction nobody was
looking at. Every review now takes a generation; typing, changing network or
version, clearing and starting another review all move it on, and a reply from
an older generation is dropped.

Changed — the interface says what decoding establishes

The results panel was headed "What it actually does". It is now "What the
bytes say"
, which is the checkable claim. The WARNING verdict said "Nothing
here is irreversible on its face"; it now says that of the part that could be
decoded, and adds that what the code at the other end does is not established by
these bytes. The clear verdict says plainly that nothing flagged is not the same
as safe.

Changed — how a decoded call is described

A four-byte selector says what shape a call has, not what the code at the
other end will do with it.
The reviewer sent a correctly encoded
transfer(address,uint256) to an address that is not a token; ClearSign called
it "ERC-20 transfer", labelled the target "Token contract", and returned exit 0.
Everything it printed was derived from the bytes, but two of the words were not.

The wording now separates the two:

Action ........................ Matches ERC-20 transfer(address,uint256)
Contract called ............... 0x…
Recipient, if it is a token ... 0x…

with an INFO finding, SELECTOR_IS_NOT_BEHAVIOUR, saying that whether the
address is a token and what its code does was not established — a contract can
answer to a familiar selector however it likes, and a proxy can point somewhere
new between one transaction and the next. transfer, approve and
transferFrom all carry it. The exit code is unchanged: refusing every token
transfer would make the tool useless, and the fix here is precision, not alarm.

Added — a support matrix generated from the code

docs/10-what-is-supported.md, written by
signing-core/scripts/support-matrix.sh, separating listed (the code will
act on it), tested (a fixture exercises it) and proven (checked against
something that is not this project).

It exists because the deployment table was documented as 1,404 address-chain
pairs while the code held 2,089. Nobody lied; the table grew and the prose
did not. That is what a hand-written support claim does. The corrected numbers
are 11 deployments, 2,089 pairs across 561 chain IDs, 12 networks in the
application, and Ethereum mainnet as the only chain anything has been proven on
live.

It also records, for the first time in one place, that EVM support does not
mean every Ethereum transaction
: types 0x01, 0x03 and 0x04 are refused,
the last being EIP-7702 authorizations. They are refused by name rather than
guessed at, which is the designed behaviour, but the gap was not written down.

Changes since v0.1.1 for users of the authority engine. A further review pass
went through the v0.1.1 fixes and found that one of them was incomplete and one
of the claims made about it was false.

Fixed

An acknowledgement identifier could name two findings. Finding numbers were
u16, produced by clamping at 65,535. Past that every finding carried the same
number, and in the plan engine — where requirements were held in a set — the
duplicates merged. A 128-step plan producing 82,048 required findings collapsed
to 65,536 distinct ones: 16,512 requirements silently disappeared, and the
short list was accepted while the complete one was refused.

Identifiers are now u32 and are never clamped. A review with more findings
than MAX_ACKNOWLEDGEABLE_FINDINGS is refused outright rather than approved
from whatever fits — a list nobody could read through is not a list anybody
approved. The plan engine compares requirements as lists rather than through a
set, so two that look alike stay two.

The transaction signer was affected differently: it compares lists already, so
clamped numbers made a review impossible to approve rather than approvable on a
partial list. Wrong, but wrong in the safe direction.

The old acknowledgement syntax was silently reinterpreted. v0.1.1 changed
authority --ack from naming a step to naming a finding, and accepted the old
#5:CODE form by stripping the #. That turned "step 5" into "finding 5" — a
different finding, approved without comment. The v0.1.1 changelog said the old
form was "refused loudly". That was not true.
It is now: the tool refuses it
and explains why, and the command's help no longer teaches the old form.

Input is bounded before it is read. The JSON parser built the whole value
before applying any limit, and the window read a dropped file entirely before
looking at its size. A limit applied after the allocation it exists to prevent
is not a limit. All three entry points — the parser, the window and the desktop
command — now check the size first.

Corrected claims

The desktop package description still promised "no network connection", the
website's page description still said "No network access", and the release notes
still called the canonical build "offline". Each now says what the README
already said: reviewing is offline, fetching a queued transaction contacts
Safe's service, and the canonical build compiles offline while its own bootstrap
does not.

The command-line tool was carrying its own copy of the JSON parser.
clearsign-cli had safe_json.rs as a local module rather than using the
shared crate, so the size limit added above reached the window and the desktop
application but not the command line. The duplicate is deleted and the tool uses
the crate everyone else does. It also checks a file's size before opening it and
caps what it takes from standard input, because reading a file whole and then
declining to parse it has already done the allocating.

Also

The plan fuzz target asked the engine for its requirements and handed them
straight back, which checks nothing an implementation can get wrong on its own.
It now asserts independently that there is one requirement per qualifying
finding, that no two share an identifier, and that omitting any one of them
refuses approval. Coverage rose from 1,995 to 2,036 edges.

The review renderer built the numbered list once per step. On a long plan, with
an arena that never frees, that cost as much as the plan had steps — enough to
breach the signer's allocation budget, which is what caught it.


A tool for reading a Safe transaction before you approve it. It holds no
keys and signs nothing.

The decoder never opens a socket. Paste a transaction and nothing
leaves the machine. The one feature that does reach the network is
fetching a queued transaction by its hash, which contacts Safe's
transaction service and nothing else.

Start with README.md in the archive, or
docs/06-using-it-before-you-sign.md in the repository.

clearsign safe-json tx.json --chain-id 1

What is checked, and what is not. This has had one external security
review: ten findings, all fixed. The fixes have not been reviewed by
anyone outside the project. docs/03-verification-status.md lists what
is proven and what is not, including the gaps.

Reproducibility. The aarch64-unknown-linux-musl build is the one
you can check, and it is built here the same way you would rebuild it:
in the canonical container, with build paths remapped out and the
compile itself offline from a copied registry cache. The container's
own bootstrap is not offline — it installs three packages with apt and
downloads rustup, neither pinned by digest — so the environment is
named and reproducible in practice rather than sealed.
Rebuild it with signing-core/scripts/reproducible-cross-check.sh and
compare against signing-core/EXPECTED-HASHES.txt.

Compare the binary, not the archive. BINARY-SHA256 attached here
is the binary's hash; SHA256SUMS covers the archives, which include a
README and the licences and so hash differently. The checksum list also
covers every installer, the standalone HTML page and build metadata.

The macOS and x86_64 builds are produced by GitHub's runners and are
not reproducible in that sense — rustc does not promise identical
output across compiler hosts, and this project would rather say so than
imply otherwise.

The application is not signed yet. macOS and Windows will warn that
it comes from an unidentified developer, because it does: signing
certificates are a paid registration this project has not taken out.
Until then the honest check is the hash, not the badge.

Not for real keys. The signing and seed commands stay behind a
development guard. Anything typed into an everyday computer should be
treated as exposed.