Source: Oxorio final audit report (22 June 2026) — Info-severity findings that had no GitHub issue. Grouped here per the "one issue for the unfiled Infos" decision. Each item was re-verified against dev@359dae4 (current HEAD).
Milestone: Pre-mainnet Audit Fixes
Big-picture map + prerequisites: docs/audit-final-status-and-dependencies.md
Report: Oxorio final 22.06.2026
All eight are present on dev. Severity is Info, so most are hardening/strictness/doc; a couple (I-13, I-14) are larger.
☐ I-01 — SignRawMessage signs arbitrary bytes with no domain / version / nonce
Layer: cross-layer (decision) · present
Handler is live in dispatch (server.rs); it only rejects empty input, wraps bytes in the EIP-191 envelope, and signs with the main bridge key (state.sign_evm) — same key that signs fundsOut EIP-712 digests. EIP-191 ≠ tx/EIP-712 encoding so it can't be replayed as a fundsOut/tx; residual risk is app-level. No production listener caller was found ⇒ likely dead surface.
Fix: confirm whether fundsIn-authorization still consumes it. If dead → remove the handler + proto entry + parent CLI/client. If live → bind to a domain-separated schema (domain/version + monotonic nonce) and reject anything that doesn't parse.
✅ I-02 — dev-mode panic-path on short calldata
✅ Done — fixed on dev-ng via #145 (sign_request_digest no longer asserts on short calldata).
Layer: enclave · self-contained
signing/evm.rs::sign_request_digest uses assert!(call_data.len() >= 4). Under a dev-mode build the length guard in validate_evm_request is #[cfg(not(feature="dev-mode"))]-gated, so a SignEvm with call_data.len() < 4 falls through to the assert. panic = "abort" ⇒ the whole enclave dies, not just the connection.
Fix: make sign_request_digest fallible (return a length error) or add a len >= 4 guard that runs on every build before the signing call.
✅ I-05 — COSE verifier accepts DER signatures and does not check alg == ES384
✅ Done — fixed on dev-ng via #126 (COSE requires raw 96-byte sig + asserts alg == ES384).
Layer: enclave (attestation-verify) · self-contained
parse_cose_ecdsa_signature accepts both raw r‖s (96 bytes) and DER; verify_real_document never decodes the protected header to assert alg == ES384 (-35). Not exploitable (key/Signature hardcoded P-384) — pure RFC-8152 strictness/hardening.
Fix: require the raw 96-byte r‖s (Signature::from_slice), drop the DER branch, and assert the protected-header alg == ES384.
☐ I-06 — Amount-binding is non-strict (≤) rather than the spec's strict ==
Layer: enclave (burn leg self-contained) · present
validate_funds_out_burn rejects only burned < calldata_amount (so ≤ passes); validate_funds_out_transfer likewise rejects only total_output_amount < calldata_amount. Spec wants strict equality.
Fix: tighten the burn comparison to burned != calldata_amount. The transfer leg's exact == is only meaningful with per-output federation-seal binding (tracked separately as #58), since total_output_amount sums recipient + change. Land with the M-02 OpId binding.
☐ I-07 — BIP-86 account extended private keys are not zeroize-on-drop
Layer: enclave · self-contained
KeyManager.account_xpriv_vanilla / account_xpriv_colored (bitcoin::bip32::Xpriv) are plain fields (unlike seed/evm_secret/btc_secret in SecretBox); there's no impl Drop in the enclave crate, and Xpriv carries a sensitive chain_code. The local master + intermediate xprivs in from_seed are also dropped without a wipe. These re-derive every child signing key and live the whole session.
Fix: bring the account xprivs under the same zeroize discipline (store raw secret material in SecretBox and rebuild on demand, or impl Drop calling non_secure_erase on each private_key + overwriting the chain code); zeroize the local master/intermediates in from_seed.
☐ I-11 — Cross-chain metadata is request/calldata-supplied rather than derived from enclave-validated facts
Layer: cross-layer · present · depends on W-01 + W-05
sourceChainId/destinationChainId/sourceAddress are not bound to validated facts; the proto carries only the EIP-712 domain chain_id; there is no single canonical bridge-intent object committed to the signature.
Fix: assemble one canonical bridge-intent (source/dest chain, bridge contract, asset, amount, recipient, op-id, consignment hash), derive each field from one source of truth (pin → BridgeConfig; asset/amount/source-net → validated consignment; the rest → full ABI decode, i.e. W-01), and reject any calldata value that disagrees. Needs proto intent fields + contract-agreed encoding.
☐ I-13 — Master seed held in memory for the whole enclave lifetime + replicated cluster-wide
Layer: enclave (architectural) · present
KeyManager keeps the raw 64-byte seed (SecretBox) resident for the whole lifetime because the cloning donor re-seals it on demand (expose_seed → with_seed → encrypt_seed_for_peer). One seed reconstructs every key and is shared cluster-wide ⇒ any single-enclave memory disclosure has cluster-wide blast radius.
Fix (architectural — likely deferred): minimize residency (unseal only for an active cloning handshake, zeroize after; drop entirely when cloning is disabled by policy), or move to per-enclave key shares / threshold signing. Needs a cloning-handshake redesign — file as tracked-but-deferred, not a quick fix.
☐ I-14 — Enclave cannot operate with tokens using the standard decimals = 18
Layer: cross-layer · present
extract_uint256_as_u64 rejects any amount whose high 24 bytes are non-zero (> u64::MAX ≈ 1.844e19, i.e. > ~18.44 whole tokens at 18 decimals). The whole pipeline is u64: TransitionSummary.total_output_amount, RGB MS_BURNED_ASSET (rgbstd Amount is u64-backed), and proto calldata_amount/rgb_amount (uint64).
Fix / decision: widen the enclave amount path to u128/U256 and the proto amount fields — but note rgbstd's Amount is u64-backed upstream, so genuinely-large 18-decimal amounts are also bounded at the RGB layer. If real amounts stay ≤ u64 base units, prefer documenting the supported max + fail-closed parsing instead of a full widening. Needs a proto/listener decision.
Source: Oxorio final audit report (22 June 2026) — Info-severity findings that had no GitHub issue. Grouped here per the "one issue for the unfiled Infos" decision. Each item was re-verified against
dev@359dae4(current HEAD).Milestone: Pre-mainnet Audit Fixes
Big-picture map + prerequisites:
docs/audit-final-status-and-dependencies.mdReport: Oxorio final 22.06.2026
All eight are present on
dev. Severity is Info, so most are hardening/strictness/doc; a couple (I-13, I-14) are larger.☐ I-01 —
SignRawMessagesigns arbitrary bytes with no domain / version / nonceLayer: cross-layer (decision) · present
Handler is live in dispatch (
server.rs); it only rejects empty input, wraps bytes in the EIP-191 envelope, and signs with the main bridge key (state.sign_evm) — same key that signsfundsOutEIP-712 digests. EIP-191 ≠ tx/EIP-712 encoding so it can't be replayed as afundsOut/tx; residual risk is app-level. No production listener caller was found ⇒ likely dead surface.Fix: confirm whether
fundsIn-authorization still consumes it. If dead → remove the handler + proto entry + parent CLI/client. If live → bind to a domain-separated schema (domain/version + monotonic nonce) and reject anything that doesn't parse.✅ I-02 —
dev-modepanic-path on short calldata✅ I-05 — COSE verifier accepts DER signatures and does not check
alg == ES384☐ I-06 — Amount-binding is non-strict (
≤) rather than the spec's strict==Layer: enclave (burn leg self-contained) · present
validate_funds_out_burnrejects onlyburned < calldata_amount(so≤passes);validate_funds_out_transferlikewise rejects onlytotal_output_amount < calldata_amount. Spec wants strict equality.Fix: tighten the burn comparison to
burned != calldata_amount. The transfer leg's exact==is only meaningful with per-output federation-seal binding (tracked separately as #58), sincetotal_output_amountsums recipient + change. Land with the M-02 OpId binding.☐ I-07 — BIP-86 account extended private keys are not zeroize-on-drop
Layer: enclave · self-contained
KeyManager.account_xpriv_vanilla/account_xpriv_colored(bitcoin::bip32::Xpriv) are plain fields (unlikeseed/evm_secret/btc_secretinSecretBox); there's noimpl Dropin the enclave crate, andXprivcarries a sensitivechain_code. The localmaster+ intermediate xprivs infrom_seedare also dropped without a wipe. These re-derive every child signing key and live the whole session.Fix: bring the account xprivs under the same zeroize discipline (store raw secret material in
SecretBoxand rebuild on demand, orimpl Dropcallingnon_secure_eraseon eachprivate_key+ overwriting the chain code); zeroize the localmaster/intermediates infrom_seed.☐ I-11 — Cross-chain metadata is request/calldata-supplied rather than derived from enclave-validated facts
Layer: cross-layer · present · depends on W-01 + W-05
sourceChainId/destinationChainId/sourceAddressare not bound to validated facts; the proto carries only the EIP-712 domainchain_id; there is no single canonical bridge-intent object committed to the signature.Fix: assemble one canonical bridge-intent (source/dest chain, bridge contract, asset, amount, recipient, op-id, consignment hash), derive each field from one source of truth (pin →
BridgeConfig; asset/amount/source-net → validated consignment; the rest → full ABI decode, i.e. W-01), and reject any calldata value that disagrees. Needs proto intent fields + contract-agreed encoding.☐ I-13 — Master seed held in memory for the whole enclave lifetime + replicated cluster-wide
Layer: enclave (architectural) · present
KeyManagerkeeps the raw 64-byte seed (SecretBox) resident for the whole lifetime because the cloning donor re-seals it on demand (expose_seed→with_seed→encrypt_seed_for_peer). One seed reconstructs every key and is shared cluster-wide ⇒ any single-enclave memory disclosure has cluster-wide blast radius.Fix (architectural — likely deferred): minimize residency (unseal only for an active cloning handshake, zeroize after; drop entirely when cloning is disabled by policy), or move to per-enclave key shares / threshold signing. Needs a cloning-handshake redesign — file as tracked-but-deferred, not a quick fix.
☐ I-14 — Enclave cannot operate with tokens using the standard
decimals = 18Layer: cross-layer · present
extract_uint256_as_u64rejects any amount whose high 24 bytes are non-zero (>u64::MAX≈1.844e19, i.e. > ~18.44 whole tokens at 18 decimals). The whole pipeline isu64:TransitionSummary.total_output_amount, RGBMS_BURNED_ASSET(rgbstdAmountisu64-backed), and protocalldata_amount/rgb_amount(uint64).Fix / decision: widen the enclave amount path to
u128/U256and the proto amount fields — but note rgbstd'sAmountisu64-backed upstream, so genuinely-large 18-decimal amounts are also bounded at the RGB layer. If real amounts stay ≤u64base units, prefer documenting the supported max + fail-closed parsing instead of a full widening. Needs a proto/listener decision.