Install
$ agentstack add skill-himanshuj16-algo-trading-skills-air-gapped-signing-workflow-for-cold-storage ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
When to Use
Use this skill when a treasury process, withdrawal pipeline, or trading-bot funding job must move crypto out of cold storage, and the signing key must never exist on an internet-connected machine. It models the full round trip — coordinator issues an intent, the intent crosses an air gap on QR or SD media, an offline vault displays and signs it, the envelope comes back, the coordinator verifies the binding and submits it exactly once.
The controls it encodes are the ones that survive a compromised online host: the vault derives what the human sees from the exact bytes it is about to sign, enforces its own policy and its own replay ledger, and the intent is bound to an EIP-155 chain id rather than to a network label.
It exists because the expensive failures in this workflow are not broken cryptography. They are a signer approving a screen that described a different transaction, and an operator retrying a broadcast whose response was merely lost.
When NOT to Use
- As a transaction builder.
UnsignedPayloadis a transfer intent, not a
signable Ethereum transaction. It carries no gas parameters, no data field, and is never RLP-encoded. Real signing must happen over the actual transaction body, or the approver has reviewed something the chain will never see.
- As cryptography. The signature primitive here is a keyed HMAC, which is
symmetric: the verification_key the online coordinator holds is enough to forge any signature. That is the inverse of what custody requires, and is tolerable only because this module never touches funds. Production signs with audited secp256k1 inside a hardware wallet or HSM, and the online side holds only a public key.
- For contract calls or token transfers. The display renders a native-value
transfer. An ERC-20 transfer's recipient and amount live inside data, where this module would show neither — that is precisely the blind-signing surface ERC-7730 exists to address.
- As the durable record. The issued, consumed, and unresolved sets live in
process memory. A restart forgets which payloads were already submitted, which is the one piece of state that must survive a crash.
- As a substitute for out-of-band verification. CCSS v9 asks that fund
destinations and amounts be verified over an Approved Communication Channel before key material is used. A display rendered by the vault defeats a compromised coordinator; it does not defeat an attacker who controls the address book the approver checks against.
- Multi-party approval. One vault, one approver. For M-of-N quorum, distinct
roles, and timelocks see multi-signature-approval-for-large-transfers.
Prerequisites
- Python 3.10+, standard library only.
- An approval callback wired into
OfflineAirGappedSigner. Without one the
vault refuses to sign anything — a vault with no approver is a blind-signing oracle, so the default is denial, not convenience.
- The EIP-155 chain id the vault is authorised for (
expected_chain_id) and
the chain the coordinator issues for (chain_id). Ethereum mainnet is 1.
- A broadcast adapter: a callable taking the verified
UnsignedPayloadand
returning a transaction reference string. Without one the coordinator reports REJECTED rather than pretending a submission occurred.
- Optional vault policy:
max_amount,allowed_destinations,
enforce_monotonic_nonce. These are your firm's numbers; no standard prescribes them.
- Physically isolated signing hardware with no Wi-Fi, Bluetooth, or cellular
modem, and chain-of-custody controls over the QR/SD media.
Workflow
- Issue a chain-bound intent.
create_unsigned_transfervalidates the
address and amount before consuming a nonce, so a rejected input does not burn one — a gap in a chain nonce sequence stalls every later transaction from that account. The intent carries the EIP-155 chain_id, because "ETH" alone does not distinguish mainnet from any other EVM chain sharing its address format.
- Export canonically.
to_qr_code_data()is sorted-key, separator-tight
JSON, and payload_hash() is SHA-256 over exactly those bytes. Move it on QR or inspected SD media only. USB, Bluetooth, Wi-Fi, and cellular bridges defeat the air gap.
- Decode strictly offline. The vault rejects malformed JSON, a missing or
unexpected field, a v1 payload with no chain id, an amount over 18 decimals or outside the uint256 wei range, and a nonce that is a bool. Unknown fields are rejected rather than ignored, so media cannot carry data past the display.
- Apply vault policy before asking a human. Wrong chain, off-allowlist
destination, or over-ceiling amount are denied without ever prompting — an approver trained to click through machine-refusable cases is a weakened control.
- Refuse to re-sign. The vault keeps its own signed-hash ledger and, by
default, requires a strictly increasing nonce. The coordinator is the assumed adversary; its replay protection is worthless if it is the thing that is compromised.
- Clear-sign.
clear_signing_display()renders destination, amount, chain
id, nonce, and payload hash from the payload about to be signed, and the approver must return exactly True. A truthy stub, a None, or a callback that raises is a denial.
- Return and verify the envelope. The envelope crosses back as text via
to_transport_data() / from_transport_data(), with its fields type- and shape-validated at construction — hostile media must fail closed, not raise a TypeError out of a comparison. The coordinator recomputes the hash, confirms it issued that intent, that the intent is not invalidated, that the chain matches, that the signer key id is known, that the payload is unconsumed, and only then checks the signature with hmac.compare_digest.
- Submit once, and treat ambiguity as ambiguity. The payload is marked
consumed before the adapter is called, so a crash mid-dispatch cannot be retried into a second submission. Any adapter failure returns UNRESOLVED, never REJECTED: the node may have accepted the transaction before the response was lost. Settle it with resolve_unresolved() against chain evidence, then issue a new intent with a new nonce — never resubmit the old envelope.
> Full procedure: see references/workflows.md. > Standards reference: see references/standards.md. > Printable pre-flight checklist: see assets/checklist.md.
Common Pitfalls
- Blind signing. Signing a hash without rendering the destination and amount
from the bytes being signed lets a compromised coordinator redirect funds. In the February 2025 Bybit theft the signers' keys were never stolen; the interface they read described a transfer that was not the one they authorised, and ~$1.46bn left a cold wallet with a valid quorum on it.
- Binding to a network label instead of a chain id. "ETH" is a string. An
intent approved for one EVM chain is meaningful on every other chain that shares the address format unless the chain id is inside the signed data — which is the whole point of EIP-155.
- Retrying a broadcast because the response timed out. A lost RPC response is
unknown, not failed. Resubmitting is how one timeout becomes one double spend. Mark the payload consumed before dispatch, and reconcile against the chain rather than against your own optimism.
- A boolean broadcast result. Two states cannot express three.
BroadcastResult
deliberately raises on bool() so if result: cannot silently file UNRESOLVED under failure at every call site.
- Trusting coordinator-side replay protection. If the coordinator is the
compromised component, its consumed-payload set is whatever the attacker says it is. The ledger that matters is the vault's.
- Ignoring unknown payload fields. Accepting a superset of the schema lets
media smuggle fields past the approver's display and into whatever consumes the payload downstream.
- Incrementing the nonce before validating the intent. Every rejected address
then burns a nonce, and the resulting gap stalls the account's queue.
- Treating an approval callback's truthy return as approval. A stub, a
MagicMock, or a partially-initialised UI object is truthy. Require is True.
- Reading the reference signature primitive as custody cryptography. It is a
symmetric HMAC. The online coordinator holds a key that can forge any signature. It is a test seam and nothing else.
Verification
- Run the full round trip with an approver that records its argument, and confirm
the display contains the destination, the amount, Chain ID: 1, the nonce, and a payload hash equal to signed.original_payload_hash.
- Construct
OfflineAirGappedSigner("k")with noapproval_callbackand confirm
sign_qr_payload returns None for a perfectly well-formed payload.
- Pass approvers returning
False,"yes",object(),None, and one that
raises; confirm all five refuse to sign.
- Submit a payload with
chain_id=137to a vault configured for1and confirm
it is denied without the approval callback being invoked.
- Sign the same intent twice against one vault and confirm the second returns
None; sign nonce 9 then nonce 4 and confirm the older one is refused unless enforce_monotonic_nonce=False.
- Feed a v1 payload (no
chain_id) and confirmAirGapSigningError. - Point a broadcast adapter at a
TimeoutErrorand confirm the result is
UNRESOLVED, the hash appears in unresolved_payload_hashes, a second broadcast_to_network returns REJECTED with "payload already submitted", and the adapter was called exactly once.
- Confirm an adapter returning
"",None, or a non-string isUNRESOLVED, not
ACCEPTED.
- Confirm
bool(BroadcastResult(...))raisesTypeError. - Construct envelopes with a non-string signature, a 3-character signature, an
uppercase hash, and a blank signer id, and confirm each raises AirGapSigningError at construction rather than failing later.
- Invalidate an issued intent and confirm its envelope is rejected with
"intent was invalidated" and never reaches the adapter.
- Submit
"MALICIOUS"as a destination and confirm the next valid intent still
receives the next consecutive nonce.
- Submit amounts of
0,-1,NaN,Infinity,1.0000000000000000001,
str(2**256), and 1E+1000 and confirm each raises.
- Run
python -m unittest discover -s skills/air-gapped-signing-workflow-for-cold-storage/scripts
and confirm a 100% pass rate.
Related Skills
hot-cold-wallet-split-for-trading-botsmulti-signature-approval-for-large-transfershardware-security-module-hsm-for-signing-keyscrypto-wallet-key-custody-securitytest-transaction-verification-before-large-transfersexchange-withdrawal-whitelist-enforcementsegregation-of-duties-for-custody-operationsrecovery-plan-for-lost-or-compromised-keyspost-incident-forensics-for-suspected-key-compromise
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: HimanshuJ16
- Source: HimanshuJ16/Algo-Trading-Skills
- License: Apache-2.0
- Homepage: https://skills.himanshujangir.com
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.