Republic Republic Republic
  • Log in
Open account
Oops! We couldn’t find any results...
Can’t find a deal? Try advanced search.
Is something missing? Add your suggestion here.
Primary market Live deals Trading Buy and sell Republic Note Own a piece of Republic's upside
Republic Ventures Opportunities for accredited investors
Republic Capital Multi-stage venture firm
Wallet Manage your digital assets Mobile app Available on iOS or Android Learning center Explore investor resources FAQ Get your questions answered
Capital fundraising Raise on Republic Tokenized assets Design, launch, manage tokenized assets Sharedrops Gift equity as a reward Founder Academy A complete guide to raising funds
Advisory Access veteran web3 advisors Infrastructure Stake your digital assets Tokenization Deploy your assets on-chain
Republic Capital In-house Venture Capital fund Broker dealer Regulated capital services
Republic Republic Republic
Oops! We couldn’t find any results...
Can’t find a deal? Try advanced search.
Is something missing? Add your suggestion here.
  • US

  • Log in
  • Open account
All investors
Primary market Live deals Trading Buy and sell
Republic Note Own a piece of Republic's upside
Accredited only
Republic Ventures Opportunities for accredited investors
Institutional
Republic Capital Multi-stage venture firm
More
Wallet Manage your digital assets Mobile app Available on iOS or Android Learning center Explore investor resources FAQ Get your questions answered
Growth capital solutions
Capital fundraising Raise on Republic Tokenized assets Design, launch, manage tokenized assets Sharedrops Gift equity as a reward Founder Academy A complete guide to raising funds
Web3 services
Advisory Access veteran web3 advisors Infrastructure Stake your digital assets
Tokenization Deploy your assets on-chain
Institutional services
Republic Capital In-house Venture Capital fund
Broker dealer Regulated capital services
Insights

Digital assets

· July 13, 2026

From trust to proof: How we made Republic Wallet's security verifiable on any machine

Same source, same hash, any laptop on Earth. The unglamorous engineering that lets anyone recompute exactly what's running inside our wallet enclave — no trust required.


Profile picture of Ryan Lutz
By Ryan Lutz
  • Liked
    Like
    0

For most of the industry, wallet security is a set of promises -- an access policy, an audit report, a list of who can touch what. We think it should be something you can check for yourself.

We recently completed a major structural reimplementation of the most sensitive operations in our wallet -- generating a seed, splitting it into key shares, and every signing operation that follows -- into an AWS Nitro Enclave, in the interest of maximizing security. These are self-custody wallets: the client controls a threshold of the key shares, we never persist enough to reconstruct one ourselves, and signing material never exists in the clear anywhere outside the enclave. The security model went from enforced by policy to enforced by silicon. Here is what that's worth, and it's worth saying plainly: it means we can hand you a number, and you can confirm -- with no access to our infrastructure, no NDA, no taking our word for anything -- that the exact code we published is the exact code holding the keys, down to the byte. Not "Republic says so." Not "an auditor signed off in Q3." A fact you can recompute yourself, on your own laptop, in a few minutes. For a system that touches customer funds, that is the difference between a promise and a proof. The catch is that the proof rests entirely on one unglamorous requirement: the enclave has to build to the exact same cryptographic measurement on every machine -- including the Apple Silicon laptops our engineers actually develop on -- and that turned out to be a surprisingly stubborn thing to guarantee.

This is how we got there, and why a few kilobytes of a Rosetta cache nearly stood in the way.


What a Nitro Enclave actually proves

A quick primer for readers who don't live in attestation documents. A Nitro Enclave is an isolated virtual machine carved out of a parent EC2 instance -- with no persistent storage, no operator shell, and no network path except a single local channel to its host. The point worth slowing down on is what that isolation means in plain terms: once the enclave is running, no one can look inside it -- not an attacker who compromises the host, not an AWS operator, not even the Republic engineers who deployed it. There is no door. The machine it runs on can hand it a message and receive an answer, but it cannot open the box up and watch it work, read its memory, or pause it to poke around. You can talk to an enclave; you cannot examine one. So if you can't see inside, the natural question is how you could ever know what's in there -- and the answer is the one thing the enclave will tell you about itself. When it boots, the Nitro hypervisor measures the enclave image and signs an attestation document containing a set of Platform Configuration Registers, or PCRs -- each one a SHA-384 hash.

Register What it measures
PCR0 A hash of the entire enclave image (the EIF file).
PCR1 The kernel and bootstrap used to boot it.
PCR2 The application -- our user-space filesystem, the part we write.

The point is this: anyone can demand that attestation before trusting the enclave, and compare its PCRs against values we publish. That comparison only means anything if the published value is reproducible from public source. If Republic is the only party who can produce the hash, "verify" quietly collapses back into "trust."


The problem: identical source, different hash

Here is the catch. The obvious recipe -- docker build your application image, then nitro-cli build-enclave to turn it into an Enclave Image Format (EIF) file -- does not give you a reproducible measurement. Run it on two machines, or even twice on one, and PCR0/PCR2 can differ. One culprit is well understood, and we dispatch it the standard way: compilers and toolchains love to stamp the moment of the build into their output -- gcc's __DATE__, ar archive headers, rustc, cmake -- so we set SOURCE_DATE_EPOCH=0 in the Dockerfile, the reproducible-builds convention these tools honor as a fixed stand-in for "now." That pins the timestamps baked inside the compiled binaries. What remains are three more stubborn sources, none of them in what the compiler emits -- they're in how the image is packed and handed to nitro-cli -- each fixable in a small host-side step.

[FIGURE 1 -- "One source. One measurement. Any host."]
A before/after diagram. Raw build: three build hosts (macOS · Apple Silicon, Linux · x86-64, Linux · aarch64) each produce a different PCR0. Canonicalized: the same three hosts run through the canonicalize step and produce one identical PCR0/PCR2. See the designed HTML version for the rendered figure.

1 · Entry order -- the filesystem comes out in a different order every time

docker export walks the image filesystem in whatever order the storage driver hands back. On Docker Desktop that's one order; on a Linux host running overlay2 it's another -- and on overlay2 it isn't even stable run-to-run on the same machine. nitro-cli hashes entries in the order it receives them, so two byte-identical filesystems enumerated differently produce two different PCRs. The fix is the most boring line in the script, which is precisely the point: sort every entry by path before writing it back.


src = tarfile.open(sys.argv[1], mode="r")
members = src.getmembers()
members.sort(key=lambda m: m.name)   # deterministic, path-sorted


Beause the image has no hardlinks, a plain lexicographic sort is safe -- a parent directory always sorts ahead of its children, so nothing is ever referenced before it exists.

02 · The Rosetta cache -- the file you can't delete from inside the thing that creates it

This is the one that cost us the most time, and the reason the technique is worth writing down. Build a linux/amd64 image on an Apple Silicon Mac and Docker Desktop runs the build under Rosetta 2 -- which writes its translation cache into the image at /root/.cache/rosetta. That directory never exists when the same source is built on a native x86 host, so its mere presence changes the measurement.

The obvious fix -- RUN rm -rf /root/.cache/rosetta in the Dockerfile -- doesn't work, and the reason is wonderfully circular: every command in an emulated build runs through Rosetta, so the rm itself re-creates the cache as it executes. You cannot delete it from inside the thing that creates it. So we don't try -- we drop it host-side, while rewriting the tar, where no emulator is in the loop.


DROP = "root/.cache/rosetta"
members = [m for m in src.getmembers()
           if not (m.name.rstrip("/") == DROP
                   or m.name.rstrip("/").startswith(DROP + "/"))]


We match the directory and everything beneath it, not just the literal path.

03 · Runtime mtimes -- timestamps stamped after the build is already over

The last source of drift isn't in the build at all -- it's injected after it. Docker stamps a handful of paths (/etc, /etc/mtab, the /dev nodes, /.dockerenv) at container-create time, and the value depends on the flavor: Docker Desktop leaves them at epoch 0, Docker Engine writes the current wall-clock time. An in-image touch can't reach them, because they don't exist yet when the image is built. So as we rewrite each entry, we zero every timestamp -- the modification time and any mtime/atime/ctime carried in PAX extended headers.


for m in members:
    m.mtime = 0
    for k in ("mtime", "atime", "ctime"):
        m.pax_headers.pop(k, None)
    dst.addfile(m, src.extractfile(m) if m.isreg() else None)



Export, rewrite, re-import

The mechanism is simple to state: export the image's filesystem on the host, rewrite the tar (drop Rosetta, sort by path, zero every mtime), and re-import it as a single layer. Doing the surgery host-side is the whole trick -- there's no emulator in the loop to undo our work.

There's one wrinkle. docker import discards the image config -- ENV, WORKDIR, USER, ENTRYPOINT, CMD -- so before we tear the image apart, we read that config out and replay it as --change flags on the way back in. (The array is assembled the portable way, because macOS still ships bash 3.2, which has no mapfile.)


# docker import drops the config, so capture it first
CANON_CHANGES=()
while IFS= read -r -d '' arg; do CANON_CHANGES+=("$arg"); done < <(
  docker inspect "$IMAGE_TAG" --format '{{json .Config}}' \
    | python3 -c '...replay ENV/WORKDIR/USER/ENTRYPOINT/CMD...'
)

# export → rewrite the tar → re-import as one layer
docker export "$canon_cid" > "$canon_tar"
python3 - "$canon_tar" <<'PYEOF' \
  | docker import "${CANON_CHANGES[@]}" - "$IMAGE_TAG"
    # drop rosetta · sort by path · zero mtimes
PYEOF


Re-importing to a single layer doesn't move the measurement -- that's exactly what the identity check below confirms. (The full canonicalize-image.sh is included at the end of this document and as a collapsible reveal in the designed version.)


Proving it actually works

Two checks gave us confidence. The first is an identity property: run the canonicalizer with its three edits disabled and the output tar is byte-for-byte identical to the input. The repack itself moves nothing -- so every change we see in a PCR is attributable to a step we deliberately chose, not an artifact of taking the image apart and putting it back together.

The second is the reproducibility property, and it's the one that matters to you. The exact same canonicalize-image.sh sits on both paths: the production build (scripts/build-eif.sh) and a no-AWS verification path (scripts/measure-pcrs.sh) that computes the EIF measurement locally, without ever launching a Nitro instance. Build on an M3 MacBook, build on a Linux x86 CI runner, build on an ARM box -- same source, same PCR0, same PCR2.


Why a handful of hashes matter

Why go to this trouble for a few hashes? Because it changes what a security claim is. A reproducible PCR turns an attestation from a number only Republic can produce into one you can recompute from source. The distance between "trust our policy" and "verify the silicon" is exactly the distance between a measurement we assert and one you can check. For a self-custody wallet, that distance is the whole product.


Coming next in this series -- Inside the wallet: a 2-of-3 architecture where no single party holds the key.
The enclave is one piece of a larger design -- how we generate a seed, split it into three shares with Shamir's Secret Sharing, and wrap each share to a specific device key so that no single party, Republic included, can ever reconstruct a key alone. It's a 2-of-3 scheme, and it deserves its own post. That one's coming.

Until then -- and it's exactly why we're working to open-source the enclave build -- the invitation is the whole point of publishing measurements in the first place: clone the source, build it on whatever laptop is in front of you, and check that the PCRs match the ones we publish. Don't trust us. Verify the silicon.


Appendix -- canonicalize-image.sh (full source)


#!/usr/bin/env bash
# Canonicalize a built enclave Docker image into a deterministic, single-layer
# image so that `nitro-cli build-enclave` produces the SAME PCR0/PCR2 on every
# machine and CPU architecture, given identical source.
#
# Both the production build (scripts/build-eif.sh) and the no-AWS verification
# path (scripts/measure-pcrs.sh) call this so the published measurements are
# actually reproducible by a third party. Without it, raw `docker build` ->
# `nitro-cli` yields a value that is not stable across -- or even within -- hosts.
#
# Three independent sources of non-determinism, each neutralized here:
#
#   1. ENTRY ORDER. `docker build` + `docker export` emit filesystem entries in
#      a storage-driver-dependent order; on Linux/overlay2 it is not even
#      stable run-to-run on one machine. nitro-cli's measurement is
#      order-sensitive, so we SORT every entry by path.
#
#   2. ROSETTA CACHE. On Apple Silicon, Docker Desktop runs the linux/amd64
#      build under Rosetta 2, which writes /root/.cache/rosetta into the image.
#      It never exists on native x86 and cannot be removed with a Dockerfile
#      RUN (every emulated command re-creates it as it executes). We DROP it.
#
#   3. RUNTIME MTIMES. Docker stamps a few paths (/etc, /etc/mtab, /dev/*,
#      /.dockerenv) at container-create time; the value differs by Docker
#      flavor and host (Docker Desktop leaves them at epoch 0, Docker Engine
#      uses the current time). The in-image `touch` cannot reach them because
#      they are injected after build. We ZERO every mtime.
#
# Mechanism: export the image filesystem host-side, rewrite the tar (drop
# rosetta, sort by path, zero mtimes), and re-import as a single layer,
# re-applying the original image config (ENV/WORKDIR/USER/ENTRYPOINT/CMD) that
# `docker import` would otherwise discard. Re-importing to a single layer does
# NOT change the PCR (verified: an identity re-pack is byte-identical). The
# image has no hardlinks, so a lexicographic sort is safe -- a parent path
# always sorts before its children.
#
# Usage: canonicalize-image.sh 
# Env:   CANON_PLATFORM (default linux/amd64)
set -euo pipefail

IMAGE_TAG="${1:?usage: canonicalize-image.sh }"
PLATFORM="${CANON_PLATFORM:-linux/amd64}"

echo ">> Canonicalizing ${IMAGE_TAG} (sorted, rosetta-stripped, mtimes zeroed)..."

# Preserve image config across the export -> import round trip; `docker import`
# drops ENV/WORKDIR/USER/ENTRYPOINT/CMD otherwise. Portable (bash 3.2+) array
# build -- macOS ships bash 3.2, which has no `mapfile`.
CANON_CHANGES=()
while IFS= read -r -d '' arg; do CANON_CHANGES+=("$arg"); done < <(
  docker inspect "${IMAGE_TAG}" --format '{{json .Config}}' | python3 -c '
import json, sys
c = json.load(sys.stdin)
out = []
for e in c.get("Env") or []: out += ["--change", "ENV " + e]
if c.get("WorkingDir"): out += ["--change", "WORKDIR " + c["WorkingDir"]]
if c.get("User"): out += ["--change", "USER " + c["User"]]
if c.get("Entrypoint"): out += ["--change", "ENTRYPOINT " + json.dumps(c["Entrypoint"])]
if c.get("Cmd"): out += ["--change", "CMD " + json.dumps(c["Cmd"])]
sys.stdout.write("".join(a + "\0" for a in out))
')

canon_cid="$(docker create --platform="${PLATFORM}" "${IMAGE_TAG}")"
canon_tar="$(mktemp "${TMPDIR:-/tmp}/cc-canon.XXXXXX")"
trap 'rm -f "${canon_tar}"; docker rm "${canon_cid}" >/dev/null 2>&1 || true' EXIT

docker export "${canon_cid}" > "${canon_tar}"

# Seekable tar so members can be sorted before writing. BSD tar on macOS has no
# --delete, so we rewrite with python tarfile (portable across macOS/Linux).
python3 - "${canon_tar}" <<'PYEOF' | docker import --platform="${PLATFORM}" 
  "${CANON_CHANGES[@]}" - "${IMAGE_TAG}" >/dev/null
import sys, tarfile
DROP = "root/.cache/rosetta"
src = tarfile.open(sys.argv[1], mode="r")
members = [m for m in src.getmembers()
          if not (m.name.rstrip("/") == DROP or m.name.rstrip("/").startswith(DROP + "/"))]
members.sort(key=lambda m: m.name)
dst = tarfile.open(fileobj=sys.stdout.buffer, mode="w|")
for m in members:
    m.mtime = 0
    for k in ("mtime", "atime", "ctime"):
        m.pax_headers.pop(k, None)
    dst.addfile(m, src.extractfile(m) if m.isreg() else None)
dst.close()
PYEOF

docker rm "${canon_cid}" >/dev/null
rm -f "${canon_tar}"
trap - EXIT
echo "   Done -- ${IMAGE_TAG} is now a canonical single-layer image."



Get notified about new posts

Not a valid email address

Share this story

Read next

June Portfolio Update

Republic

June Portfolio Update

Polygon marks its six-year anniversary, stating its chain has moved "trillions" on-chain &amp; used by global enterprises. Polygon announced a six-year anniversary edition highlighting that its chain has moved "trillions" on-chain and that it supports g...

Why Republic tokenizes RWA on Sui

Digital assets

Why Republic tokenizes RWA on Sui

A complete guide to storing and staking SUI: set up a non-custodial wallet, understand Sui's proof-of-stake rewards, and delegate to Republic in a few steps.

From Uranium to Treasuries: How Etherlink is Unlocking Real-World Assets

Digital assets

From Uranium to Treasuries: How Etherlink is Unlocking Real-World Assets

The Real World Assets (RWA) sector has become one of the fastest-growing narratives in blockchain. In 2026, tokenized real-world assets are bridging traditional finance and decentralized technology, offering investors new levels of liquidity, transparency,...

Republic

Giving everyone access to early-stage startup investing

For investors
  • Why invest
  • How it works
  • FAQ
  • Risks
  • Privacy policy
  • Accessibility
  • Cookie Preferences
  • Form CRS
For startups
  • Why raise
  • Learn
  • FAQ
  • Tokenized assets
Company
  • About
  • Insights
  • Events
  • Contact
  • Security
  • We're hiring!
Dollar Refer a startup, get $2,500
Dollar Refer a startup, get $2,500

Invest in the app

Android app iOS app

Invest in the app

Android app iOS app

This site (the "Site") is owned and maintained by OpenDeal Inc., which is not a registered broker-dealer. OpenDeal Inc. does not give investment advice, endorsement, analysis or recommendations with respect to any securities. All securities listed here are being offered by, and all information included on this Site is the responsibility of, the applicable issuer of such securities. The intermediary facilitating the offering will be identified in such offering’s documentation.

By accessing the Site and any pages thereof, you agree to be bound by the Terms of Use and Privacy Policy. Please also see OpenDeal Broker’s Business Continuity Plan and Additional Risk Disclosures. All issuers offering securities under regulation crowdfunding as hosted by OpenDeal Portal LLC are listed on the All Companies Page. The inclusion or exclusion of an issuer on the Platform Page and/or Republic’s Homepage, which includes offerings conducted under regulation crowdfunding as well as other exemptions from registration, is not based upon any endorsement or recommendation by OpenDeal Inc, OpenDeal Portal LLC, or OpenDeal Broker LLC, nor any of their affiliates, officers, directors, agents, and employees. Rather, issuers of securities may, in their sole discretion, opt-out of being listed on the Platform Page and Homepage.

Neither OpenDeal Inc., OpenDeal Portal LLC nor OpenDeal Broker LLC verify information provided by companies on this Site and makes no assurance as to the completeness or accuracy of any such information. Additional information about companies fundraising on the Site can be found by searching the EDGAR database, or the offering documentation located on the Site when the offering does not require an EDGAR filing.

Invest in startups using your credit card
You can invest using your credit card

Made in SF/NYC