mirror of
https://github.com/luxfi/audits.git
synced 2026-10-30 17:02:11 +00:00
LaTeX is the canonical format for audit reports per workspace policy. Conversion via pandoc --from=gfm --to=latex --standalone preserves content verbatim. Meta files (README/INDEX/LLM/CHANGELOG) remain .md.
626 lines
26 KiB
TeX
626 lines
26 KiB
TeX
% Options for packages loaded elsewhere
|
|
\PassOptionsToPackage{unicode}{hyperref}
|
|
\PassOptionsToPackage{hyphens}{url}
|
|
\documentclass[
|
|
]{article}
|
|
\usepackage{xcolor}
|
|
\usepackage{amsmath,amssymb}
|
|
\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
|
|
\usepackage{iftex}
|
|
\ifPDFTeX
|
|
\usepackage[T1]{fontenc}
|
|
\usepackage[utf8]{inputenc}
|
|
\usepackage{textcomp} % provide euro and other symbols
|
|
\else % if luatex or xetex
|
|
\usepackage{unicode-math} % this also loads fontspec
|
|
\defaultfontfeatures{Scale=MatchLowercase}
|
|
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
|
|
\fi
|
|
\usepackage{lmodern}
|
|
\ifPDFTeX\else
|
|
% xetex/luatex font selection
|
|
\fi
|
|
% Use upquote if available, for straight quotes in verbatim environments
|
|
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
|
|
\IfFileExists{microtype.sty}{% use microtype if available
|
|
\usepackage[]{microtype}
|
|
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
|
|
}{}
|
|
\makeatletter
|
|
\@ifundefined{KOMAClassName}{% if non-KOMA class
|
|
\IfFileExists{parskip.sty}{%
|
|
\usepackage{parskip}
|
|
}{% else
|
|
\setlength{\parindent}{0pt}
|
|
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
|
|
}{% if KOMA class
|
|
\KOMAoptions{parskip=half}}
|
|
\makeatother
|
|
\usepackage{longtable,booktabs,array}
|
|
\newcounter{none} % for unnumbered tables
|
|
\usepackage{calc} % for calculating minipage widths
|
|
% Correct order of tables after \paragraph or \subparagraph
|
|
\usepackage{etoolbox}
|
|
\makeatletter
|
|
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
|
|
\makeatother
|
|
% Allow footnotes in longtable head/foot
|
|
\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
|
|
\makesavenoteenv{longtable}
|
|
\setlength{\emergencystretch}{3em} % prevent overfull lines
|
|
\providecommand{\tightlist}{%
|
|
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
|
|
\usepackage{bookmark}
|
|
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
|
|
\urlstyle{same}
|
|
\hypersetup{
|
|
hidelinks,
|
|
pdfcreator={LaTeX via pandoc}}
|
|
|
|
\author{}
|
|
\date{}
|
|
|
|
\begin{document}
|
|
|
|
\section{Red Team Session Audit ---
|
|
2026-04-12}\label{red-team-session-audit--2026-04-12}
|
|
|
|
\textbf{Auditor}: Red Team (adversarial security review) \textbf{Scope}:
|
|
Commits 2fb2cc7, 06e1fc3, VRF (recent), plus indexer/graph/explorer
|
|
changes \textbf{Method}: Static analysis, attack construction, crypto
|
|
lens, systems lens
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsection{Executive Summary}\label{executive-summary}
|
|
|
|
Reviewed 9 distinct change areas across precompile, indexer, graph, and
|
|
explorer. Found \textbf{1 critical}, \textbf{2 high}, \textbf{3 medium},
|
|
\textbf{2 low}, \textbf{3 info} findings.
|
|
|
|
The critical finding is a consensus-safety violation in the attestation
|
|
precompile. The high findings are a cache poisoning vector in FROST and
|
|
an unbounded query amplification in graph resolvers.
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsection{Findings}\label{findings}
|
|
|
|
\subsubsection{{[}CRITICAL{]} Attestation precompile uses time.Now() ---
|
|
consensus-unsafe
|
|
non-determinism}\label{critical-attestation-precompile-uses-timenow--consensus-unsafe-non-determinism}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/attestation/attestation.go:115,201,366,397,417}
|
|
\textbf{Adversary}: Any validator operator observing state divergence
|
|
\textbf{Attack}: The attestation precompile calls \texttt{time.Now()} at
|
|
lines 115, 201, 366, 397, and 417. Each validator processes the same
|
|
transaction at different wall-clock times. The \texttt{Timestamp} field
|
|
in \texttt{GPUAttestation} and \texttt{AttestationQuote} structs, and
|
|
the \texttt{expiresAt} calculation at line 417, all produce different
|
|
values per validator.
|
|
|
|
Concrete scenario:
|
|
|
|
\begin{enumerate}
|
|
\def\labelenumi{\arabic{enumi}.}
|
|
\tightlist
|
|
\item
|
|
User calls \texttt{CreateAttestation} (selector 0x04000000)
|
|
\item
|
|
Validator A processes at T=1712937600, computes
|
|
\texttt{expiresAt=1712941200}
|
|
\item
|
|
Validator B processes at T=1712937601, computes
|
|
\texttt{expiresAt=1712941201}
|
|
\item
|
|
Output bytes differ -\textgreater{} state root diverges
|
|
-\textgreater{} chain split
|
|
\end{enumerate}
|
|
|
|
This is the same class of bug as the ECIES/ML-KEM non-determinism
|
|
(C-03/M-11) that was fixed in commit 617c285 for \texttt{pqcrypto} and
|
|
18e0a95 for \texttt{hpke}. The attestation precompile was not covered by
|
|
that fix sweep.
|
|
|
|
\textbf{Impact}: Chain split. Any validator processing an attestation
|
|
transaction at a different millisecond produces a different state root.
|
|
This is exploitable by simply sending any attestation transaction.
|
|
\textbf{Fix}: Replace all \texttt{time.Now()} calls with
|
|
\texttt{accessibleState.GetBlockContext().Timestamp()} to use the
|
|
deterministic block timestamp. The \texttt{Run} method in
|
|
\texttt{module.go} receives \texttt{accessibleState} but never passes it
|
|
to the attestation functions. Refactor \texttt{VerifyNVTrust},
|
|
\texttt{VerifyTPM}, \texttt{VerifyCompute}, \texttt{CreateAttestation}
|
|
to accept a \texttt{blockTimestamp\ uint64} parameter. \textbf{Status}:
|
|
Open
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}HIGH{]} FROST LiftX cache unbounded under concurrent
|
|
load --- cache poisoning via
|
|
eviction}\label{high-frost-liftx-cache-unbounded-under-concurrent-load--cache-poisoning-via-eviction}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/frost/contract.go:147-179}
|
|
\textbf{Adversary}: Attacker with transaction submission capability
|
|
\textbf{Attack}: The \texttt{liftXCache} is a
|
|
\texttt{map{[}{[}32{]}byte{]}curve.Point} with a hard cap at 1024
|
|
entries (line 175: \texttt{if\ len(liftXCache)\ \textless{}\ 1024}).
|
|
Once the cache is full, no new entries are added, but existing entries
|
|
are never evicted.
|
|
|
|
Attack scenario:
|
|
|
|
\begin{enumerate}
|
|
\def\labelenumi{\arabic{enumi}.}
|
|
\tightlist
|
|
\item
|
|
Attacker sends 1024 FROST verify transactions with 1024 distinct
|
|
public keys (cost: 1024 * \textasciitilde55,000 gas =
|
|
\textasciitilde56M gas, feasible in a single block with 100M gas
|
|
limit)
|
|
\item
|
|
Cache is now permanently full with attacker-chosen keys
|
|
\item
|
|
Legitimate validator public keys can never enter the cache
|
|
\item
|
|
All subsequent legitimate FROST verifications pay the full LiftX cost
|
|
(80\% of verify)
|
|
\item
|
|
This is a targeted DoS: attacker pays once, defenders pay forever
|
|
\end{enumerate}
|
|
|
|
The cache also has a TOCTOU between \texttt{RLock} check and
|
|
\texttt{Lock} write (lines 164-178): two goroutines can both see
|
|
\texttt{cached=false}, both proceed to \texttt{Lock}, and both write the
|
|
same key. While not exploitable for corruption (last-write-wins is fine
|
|
for a cache), it causes unnecessary duplicate computation under
|
|
contention.
|
|
|
|
\textbf{Impact}: Permanent performance degradation of FROST verification
|
|
for all legitimate users. In a block with many FROST verifications
|
|
(every block from validators), this forces worst-case execution time.
|
|
\textbf{Fix}: Use an LRU cache with bounded size instead of a grow-only
|
|
map. The \texttt{golang.org/x/exp/maps} or a simple LRU ring suffices.
|
|
Evict on LRU policy, not refuse-to-insert. \textbf{Status}: Open
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}HIGH{]} Graph resolvers --- unbounded pagination
|
|
allows memory
|
|
exhaustion}\label{high-graph-resolvers--unbounded-pagination-allows-memory-exhaustion}
|
|
|
|
\textbf{File}: All 29 resolver packages under
|
|
\texttt{/Users/z/work/lux/graph/resolvers/*/} \textbf{Adversary}: Any
|
|
GraphQL query submitter (unauthenticated) \textbf{Attack}: Every
|
|
resolver\textquotesingle s \texttt{pl()} function parses the
|
|
\texttt{first} argument via
|
|
\texttt{fmt.Sscanf(fmt.Sprint(l),\ "\%d",\ \&limit)}. There is no upper
|
|
bound on \texttt{limit}. A query like
|
|
\texttt{\{\ utxos(first:\ 999999999)\ \{\ ...\ \}\ \}} passes
|
|
\texttt{limit=999999999} to \texttt{s.ListByType("UTXO",\ 999999999)}.
|
|
|
|
If the storage backend returns all matching records up to the limit,
|
|
this causes:
|
|
|
|
\begin{enumerate}
|
|
\def\labelenumi{\arabic{enumi}.}
|
|
\tightlist
|
|
\item
|
|
Massive memory allocation for serializing 999M records
|
|
\item
|
|
CPU exhaustion in JSON marshaling
|
|
\item
|
|
Network saturation sending the response
|
|
\end{enumerate}
|
|
|
|
The default \texttt{limit=100} is fine, but the override path has no
|
|
cap. This is present in all 29 resolver packages identically (copy-paste
|
|
pattern).
|
|
|
|
\textbf{Impact}: Remote DoS of the graph engine. A single
|
|
unauthenticated GraphQL query can OOM the graph server. \textbf{Fix}:
|
|
Cap \texttt{limit} to a sane maximum (e.g., 1000) in the \texttt{pl()}
|
|
function:
|
|
\texttt{if\ limit\ \textgreater{}\ 1000\ \{\ limit\ =\ 1000\ \}}. Apply
|
|
across all 29 resolver packages. \textbf{Status}: Open
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}MEDIUM{]} pqcrypto deterministic seed --- seed reuse
|
|
produces identical ciphertext (by design, but no documentation of caller
|
|
responsibility)}\label{medium-pqcrypto-deterministic-seed--seed-reuse-produces-identical-ciphertext-by-design-but-no-documentation-of-caller-responsibility}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/pqcrypto/contract.go:331-392}
|
|
\textbf{Adversary}: Contract developer who reuses seeds \textbf{Attack}:
|
|
The ML-KEM encapsulate function accepts a caller-provided 32-byte seed
|
|
for deterministic randomness. This is correct for consensus safety.
|
|
However, if a caller reuses the same seed with the same public key, the
|
|
output ciphertext and shared secret are identical.
|
|
|
|
This is IND-CPA insecure when seeds are reused: an observer who sees two
|
|
identical ciphertexts knows the same plaintext was encrypted. For ML-KEM
|
|
specifically, seed reuse does not leak the private key (unlike ECDSA
|
|
nonce reuse), but it does break ciphertext indistinguishability.
|
|
|
|
The gas cost is appropriate (6,000-10,000 depending on mode). The seed
|
|
parameter itself is correctly extracted and bounded.
|
|
|
|
\textbf{Impact}: Privacy violation for callers who reuse seeds. Not a
|
|
chain-split or fund-loss issue, but violates IND-CPA security for the
|
|
affected transactions. \textbf{Fix}: Add a check that the seed has not
|
|
been used before (e.g., via state storage), or document prominently that
|
|
callers MUST use unique seeds. A revert on duplicate seed would be the
|
|
safest approach. \textbf{Status}: Open (documentation issue at minimum)
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}MEDIUM{]} HPKE seal --- GPU path computes Kyber encap
|
|
then discards
|
|
result}\label{medium-hpke-seal--gpu-path-computes-kyber-encap-then-discards-result}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/hpke/contract.go:407-454}
|
|
\textbf{Adversary}: N/A (waste, not exploitable) \textbf{Attack}: The
|
|
\texttt{singleShotSealGPU} function calls \texttt{lattice.KyberEncaps()}
|
|
on the GPU (lines 441-447), synchronizes (line 449), then falls through
|
|
to \texttt{singleShotSealCPU(params)} which repeats the entire HPKE
|
|
pipeline including KEM encapsulation on CPU (line 453).
|
|
|
|
The GPU result (\texttt{ctTensor}, \texttt{ssTensor}) is never read back
|
|
or used. The function allocates GPU memory, performs the KEM, syncs,
|
|
then discards everything and runs the CPU path. This doubles the
|
|
computation time when GPU is available.
|
|
|
|
This is not a security vulnerability but a gas accounting concern: the
|
|
user pays gas for one encapsulation but the node performs two. On nodes
|
|
with GPUs, this wastes compute resources and slows block processing.
|
|
|
|
\textbf{Impact}: Wasted computation (2x KEM operations). No consensus
|
|
issue since both paths are deterministic and the CPU path produces the
|
|
final result. \textbf{Fix}: Either (a) extract the GPU KEM result and
|
|
use it in the HPKE key schedule, or (b) remove the GPU path entirely
|
|
until it can produce the full HPKE output, or (c) use GPU only for batch
|
|
acceleration in the parallel block executor. \textbf{Status}: Open
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}MEDIUM{]} Attestation module.go --- Run() does not
|
|
check readOnly for state-mutating
|
|
operations}\label{medium-attestation-modulego--run-does-not-check-readonly-for-state-mutating-operations}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/attestation/module.go:79-102}
|
|
\textbf{Adversary}: Smart contract calling attestation precompile via
|
|
STATICCALL \textbf{Attack}: The \texttt{Run()} method in
|
|
\texttt{attestationPrecompile} dispatches to \texttt{CreateAttestation}
|
|
(selector 0x04000000) and \texttt{VerifyCompute} (selector 0x03000000),
|
|
both of which mutate global state (the verifier\textquotesingle s device
|
|
registry and job history). However, the \texttt{readOnly} parameter is
|
|
never checked.
|
|
|
|
In a STATICCALL context (readOnly=true), the EVM expects the precompile
|
|
to not modify state. While the attestation precompile\textquotesingle s
|
|
mutations are to in-memory Go state (not EVM storage), the principle is
|
|
violated. If the verifier\textquotesingle s state is later serialized or
|
|
if the precompile is upgraded to write to EVM state, this becomes a
|
|
consensus bug.
|
|
|
|
More concretely: \texttt{VerifyCompute} at line 313 calls
|
|
\texttt{globalVerifier.RecordJobCompletion()} which modifies the global
|
|
verifier even in a \texttt{readOnly} call. This means a
|
|
\texttt{STATICCALL} to VerifyCompute has a side effect, which is
|
|
incorrect.
|
|
|
|
\textbf{Impact}: State pollution from read-only calls. Could cause
|
|
incorrect device status reports after STATICCALL-invoked verifications.
|
|
\textbf{Fix}: Check \texttt{readOnly} in the dispatch switch. For
|
|
selectors 0x03000000 (VerifyCompute) and 0x04000000 (CreateAttestation),
|
|
return error if \texttt{readOnly\ ==\ true}. \textbf{Status}: Open
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}LOW{]} VRF hashToCurveELL2 --- variable-time loop
|
|
leaks alpha length via
|
|
timing}\label{low-vrf-hashtocurveell2--variable-time-loop-leaks-alpha-length-via-timing}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/vrf/contract.go:242-277}
|
|
\textbf{Adversary}: Co-located process measuring precompile execution
|
|
time \textbf{Attack}: The \texttt{hashToCurveELL2} function uses
|
|
try-and-increment: it iterates \texttt{ctr} from 0 to 254, attempting to
|
|
decompress a hash output as an Edwards point. The number of iterations
|
|
before success depends on the input \texttt{alpha}. A co-located
|
|
attacker measuring execution time can determine how many iterations were
|
|
needed, leaking information about \texttt{alpha}.
|
|
|
|
For VRF verification this is low severity because \texttt{alpha} is
|
|
typically public (it\textquotesingle s the VRF input). However, if this
|
|
function is used in a privacy-preserving context where \texttt{alpha}
|
|
should be hidden, the timing channel is exploitable.
|
|
|
|
The gas cost is fixed at \texttt{GasVerify\ =\ 20,000} regardless of
|
|
iteration count, which means the attacker cannot observe the timing via
|
|
gas metering. They would need wall-clock observation.
|
|
|
|
\textbf{Impact}: Minor timing side channel. Exploitable only with
|
|
co-located measurement and only leaks information about public VRF
|
|
inputs. \textbf{Fix}: Use constant-time Elligator2 mapping (CFRG
|
|
hash-to-curve spec, section 6.8.2) instead of try-and-increment. The
|
|
Edwards25519 library supports \texttt{HashToCurve} directly.
|
|
\textbf{Status}: Open
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}LOW{]} Indexer WebSocket --- no authentication on
|
|
subscription}\label{low-indexer-websocket--no-authentication-on-subscription}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/indexer/multichain/platform\_indexer.go:184-243}
|
|
\textbf{Adversary}: Network attacker with access to the WS endpoint
|
|
\textbf{Attack}: The \texttt{runRealtimeLoop} function connects to the
|
|
chain\textquotesingle s WebSocket endpoint and subscribes to block
|
|
notifications. There is no authentication header, no API key, and no TLS
|
|
certificate verification on the WebSocket connection.
|
|
|
|
If the chain\textquotesingle s WS endpoint is internal (expected), this
|
|
is low risk. If the WS endpoint is exposed publicly, an attacker could:
|
|
|
|
\begin{enumerate}
|
|
\def\labelenumi{\arabic{enumi}.}
|
|
\tightlist
|
|
\item
|
|
Connect and subscribe to the same feed (information leak of block
|
|
data, which is public anyway)
|
|
\item
|
|
Cannot inject blocks because the indexer only reads
|
|
\item
|
|
MITM the connection if not using WSS (inject fake block notifications)
|
|
\end{enumerate}
|
|
|
|
The \texttt{Dialer} at line 190 uses default TLS settings which will
|
|
verify certificates for WSS URLs.
|
|
|
|
\textbf{Impact}: If WS URLs use \texttt{ws://} (not \texttt{wss://}),
|
|
block data could be MITM\textquotesingle d. The indexer would process
|
|
attacker-controlled block data. \textbf{Fix}: Enforce \texttt{wss://}
|
|
for all non-localhost WS endpoints. Add a check in
|
|
\texttt{runRealtimeLoop} that rejects \texttt{ws://} unless the host is
|
|
\texttt{localhost} or \texttt{127.0.0.1}. \textbf{Status}: Open
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}INFO{]} Explorer ChainSwitcher ---
|
|
dangerouslySetInnerHTML for SVG
|
|
logos}\label{info-explorer-chainswitcher--dangerouslysetinnerhtml-for-svg-logos}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/explore/ui/snippets/topBar/ChainSwitcher.tsx:89,183}
|
|
\textbf{Adversary}: Admin who controls chain registry config
|
|
\textbf{Attack}: The ChainSwitcher renders SVG logos using
|
|
\texttt{dangerouslySetInnerHTML=\{\{\ \_\_html:\ current.branding.logoContent\ \}\}}.
|
|
If \texttt{logoContent} contained
|
|
\texttt{\textless{}script\textgreater{}} or event handlers
|
|
(\texttt{onload}), this would be XSS.
|
|
|
|
However, after reviewing the chain registry
|
|
(\texttt{configs/app/chainRegistry.ts}), all \texttt{logoContent} values
|
|
are hardcoded SVG path strings in the source code (lines 145-220 of the
|
|
registry). They are not derived from user input, API responses, or URL
|
|
parameters.
|
|
|
|
The \texttt{explorerUrl} values are also hardcoded strings (lines
|
|
242-391) or built from \texttt{NEXT\_PUBLIC\_APP\_HOST} env var (line
|
|
428). The \texttt{window.location.href\ =\ this.targetUrl} assignment at
|
|
line 23 only navigates to these hardcoded URLs, so there is no open
|
|
redirect vector.
|
|
|
|
The \texttt{highlightText} function used in search results correctly
|
|
sanitizes via the \texttt{xss} library (confirmed at
|
|
\texttt{lib/highlightText.ts:7}).
|
|
|
|
\textbf{Impact}: No current vulnerability. The dangerouslySetInnerHTML
|
|
is safe because the content is developer-controlled. However, if the
|
|
chain registry is ever populated from an API or database, this becomes a
|
|
stored XSS vector. \textbf{Fix}: No fix needed now. Add a comment noting
|
|
that \texttt{logoContent} must remain developer-controlled. Consider
|
|
using a sanitizer if the registry source changes. \textbf{Status}:
|
|
Verified-safe (with caveat)
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}INFO{]} SLH-DSA 12-mode tests --- timing side channels
|
|
not
|
|
tested}\label{info-slh-dsa-12-mode-tests--timing-side-channels-not-tested}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/slhdsa/modes\_test.go} (from commit
|
|
2fb2cc7) \textbf{Adversary}: N/A (test coverage gap) \textbf{Attack}:
|
|
The SLH-DSA precompile supports 12 parameter sets (6 SHA2 + 6 SHAKE).
|
|
The modes test verifies that all 12 modes produce correct verification
|
|
results. However, it does not measure execution time variance across
|
|
modes or inputs.
|
|
|
|
SLH-DSA verification is hash-based with many internal hash evaluations
|
|
(WOTS+ chains, Merkle tree traversal). The number of hash evaluations is
|
|
data-dependent for some parameter sets. A timing test would measure
|
|
whether verification time varies with signature content.
|
|
|
|
However, since the gas cost is fixed per mode (not
|
|
per-signature-content), and the EVM does not expose wall-clock time to
|
|
callers, exploiting this requires co-located measurement. The test gap
|
|
is informational only.
|
|
|
|
\textbf{Impact}: None currently. Test coverage improvement opportunity.
|
|
\textbf{Fix}: Add benchmark tests (\texttt{go\ test\ -bench}) for each
|
|
mode with varying message sizes to document performance characteristics.
|
|
\textbf{Status}: Verified-safe (gas model prevents EVM-visible timing)
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}INFO{]} Adapter rename (achain-\textgreater ai etc)
|
|
--- no RPC method names in
|
|
logs}\label{info-adapter-rename-achain-ai-etc--no-rpc-method-names-in-logs}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/indexer/multichain/platform\_indexer.go}
|
|
\textbf{Adversary}: Log scraper \textbf{Attack}: The adapter rename
|
|
changed chain type names (e.g., \texttt{native} to \texttt{platform},
|
|
\texttt{achain} to \texttt{ai}). The RPC method names are constructed
|
|
from the chain type (e.g., \texttt{ai.getLatestBlock},
|
|
\texttt{mpc.getLatestBlock}).
|
|
|
|
After reviewing the indexer code, RPC method names appear in error
|
|
messages (via \texttt{fmt.Errorf}) but not in \texttt{log.Printf} calls.
|
|
The log messages at lines 77 and 208 use \texttt{idx.config.ID} and
|
|
\texttt{idx.config.Name}, not the RPC method string.
|
|
|
|
If the RPC method names leak (e.g., in error responses returned to API
|
|
callers), they reveal internal chain architecture. However, the method
|
|
names (\texttt{ai.getLatestBlock}, \texttt{bridge.getLatestBlock}) are
|
|
not sensitive --- they describe public chain functionality.
|
|
|
|
\textbf{Impact}: None. RPC method names are not sensitive and are not
|
|
logged. \textbf{Fix}: No fix needed. \textbf{Status}: Verified-safe
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{{[}INFO{]} FROST real-sig test --- coverage
|
|
assessment}\label{info-frost-real-sig-test--coverage-assessment}
|
|
|
|
\textbf{File}:
|
|
\texttt{/Users/z/work/lux/precompile/frost/real\_sig\_test.go}
|
|
\textbf{Adversary}: N/A (test quality review) \textbf{Attack}: The new
|
|
test suite (\texttt{real\_sig\_test.go}, 243 lines) replaces the prior
|
|
security-theater test that used sequential byte patterns as signatures.
|
|
The new tests:
|
|
|
|
\begin{enumerate}
|
|
\def\labelenumi{\arabic{enumi}.}
|
|
\tightlist
|
|
\item
|
|
\texttt{TestFROSTVerify\_RealValidSignature} --- generates real
|
|
Schnorr sig, asserts \texttt{result{[}31{]}==1} (line 127)
|
|
\item
|
|
\texttt{TestFROSTVerify\_RealCorruptedSignature} --- flips bytes in R
|
|
and z, asserts \texttt{result{[}31{]}==0} (lines 148-162)
|
|
\item
|
|
\texttt{TestFROSTVerify\_RealWrongMessage} --- correct sig, different
|
|
message hash (line 175)
|
|
\item
|
|
\texttt{TestFROSTVerify\_RealWrongPublicKey} --- correct sig,
|
|
different public key (line 188)
|
|
\item
|
|
\texttt{TestFROSTVerify\_Determinism} --- 100 iterations produce
|
|
identical output (line 208)
|
|
\item
|
|
\texttt{TestFROSTVerify\_ConcurrentSafety} --- 100 goroutines (line
|
|
227)
|
|
\end{enumerate}
|
|
|
|
Coverage gaps identified:
|
|
|
|
\begin{itemize}
|
|
\tightlist
|
|
\item
|
|
No test for \texttt{threshold\ \textgreater{}\ totalSigners} (invalid
|
|
threshold rejection)
|
|
\item
|
|
No test for \texttt{totalSigners\ =\ 0} edge case
|
|
\item
|
|
No test for \texttt{threshold\ =\ 0} edge case (covered by
|
|
contract.go:111 but not tested from \texttt{real\_sig\_test.go})
|
|
\item
|
|
No test for gas exhaustion (suppliedGas \textless{} requiredGas)
|
|
\item
|
|
The concurrent test at line 233 checks \texttt{result{[}31{]}\ !=\ 1}
|
|
but sends the result to the error channel even when
|
|
\texttt{err\ ==\ nil} --- this means a "valid but result{[}31{]}==0"
|
|
concurrent race is reported as \texttt{nil} error, which
|
|
\texttt{require.NoError} will pass. The test should assert
|
|
\texttt{result{[}31{]}\ ==\ 1} inside the goroutine.
|
|
\end{itemize}
|
|
|
|
These are test quality issues, not security vulnerabilities.
|
|
|
|
\textbf{Impact}: The test is vastly better than the prior
|
|
security-theater version. The identified gaps are edge cases that are
|
|
handled correctly in the contract code but lack test coverage.
|
|
\textbf{Fix}: Add threshold/gas edge case tests. Fix the concurrent test
|
|
to assert \texttt{result{[}31{]}==1}. \textbf{Status}: Verified-safe
|
|
(test quality improvement recommended)
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsubsection{gas=0 Sentinel Migration
|
|
Assessment}\label{gas0-sentinel-migration-assessment}
|
|
|
|
\textbf{Scope}: 8 packages added \texttt{gas\_zero\_test.go} files in
|
|
commit 2fb2cc7.
|
|
|
|
I verified the gas=0 handling across all precompiles that were part of
|
|
the migration. The pattern is:
|
|
|
|
\begin{verbatim}
|
|
RequiredGas returns 0 for invalid/short input
|
|
-> Run checks suppliedGas < requiredGas
|
|
-> When requiredGas == 0, the check passes (0 < 0 is false)
|
|
-> Run proceeds with remainingGas = suppliedGas - 0 = suppliedGas
|
|
-> Run returns error for invalid input, preserving gas
|
|
\end{verbatim}
|
|
|
|
This is correct behavior: gas=0 from RequiredGas is a sentinel meaning
|
|
"I cannot determine the cost because the input is malformed." The Run
|
|
method then validates the input and returns an error. The
|
|
caller\textquotesingle s gas is preserved (not consumed).
|
|
|
|
The precompiles that were NOT part of this migration but follow the same
|
|
pattern:
|
|
|
|
\begin{itemize}
|
|
\tightlist
|
|
\item
|
|
\texttt{math/contract.go} --- uses its own \texttt{ErrOutOfGas} (line
|
|
35), not \texttt{contract.ErrOutOfGas}. This is a minor inconsistency
|
|
but functionally equivalent.
|
|
\item
|
|
\texttt{bls12381/contract.go} --- uses EIP-2537 gas formula, returns 0
|
|
for \textless{} 2 pairs. Correct.
|
|
\end{itemize}
|
|
|
|
No precompile was found that returns a local error string instead of
|
|
\texttt{contract.ErrOutOfGas}. The migration is complete.
|
|
|
|
\textbf{Status}: Verified-safe
|
|
|
|
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
|
|
|
|
\subsection{Summary}\label{summary}
|
|
|
|
{\def\LTcaptype{none} % do not increment counter
|
|
\begin{longtable}[]{@{}lll@{}}
|
|
\toprule\noalign{}
|
|
Severity & Count & Findings \\
|
|
\midrule\noalign{}
|
|
\endhead
|
|
\bottomrule\noalign{}
|
|
\endlastfoot
|
|
CRITICAL & 1 & Attestation time.Now() consensus non-determinism \\
|
|
HIGH & 2 & FROST cache poisoning, Graph resolver unbounded pagination \\
|
|
MEDIUM & 3 & ML-KEM seed reuse, HPKE GPU waste, Attestation readOnly
|
|
violation \\
|
|
LOW & 2 & VRF timing side channel, Indexer WS no auth \\
|
|
INFO & 3 & ChainSwitcher SVG (safe), SLH-DSA timing tests, FROST test
|
|
gaps \\
|
|
\end{longtable}
|
|
}
|
|
|
|
\textbf{Recommendation}: \textbf{fix-then-ship} -\/- the CRITICAL
|
|
attestation \texttt{time.Now()} must be fixed before any chain that
|
|
enables the attestation precompile. The FROST cache and Graph pagination
|
|
should be fixed before production deployment.
|
|
|
|
\end{document}
|