<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tharindu Karunanayaka</title>
    <description>The latest articles on DEV Community by Tharindu Karunanayaka (@tharindukn).</description>
    <link>https://dev.to/tharindukn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3992923%2Fa315e3cb-3c11-4f81-af80-0ce08688a040.jpeg</url>
      <title>DEV Community: Tharindu Karunanayaka</title>
      <link>https://dev.to/tharindukn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tharindukn"/>
    <language>en</language>
    <item>
      <title>How Do You Log Someone Out of a Stateless System? JWT Invalidation on Logout</title>
      <dc:creator>Tharindu Karunanayaka</dc:creator>
      <pubDate>Sat, 04 Jul 2026 09:37:35 +0000</pubDate>
      <link>https://dev.to/tharindukn/how-do-you-log-someone-out-of-a-stateless-system-jwt-invalidation-on-logout-bbp</link>
      <guid>https://dev.to/tharindukn/how-do-you-log-someone-out-of-a-stateless-system-jwt-invalidation-on-logout-bbp</guid>
      <description>&lt;p&gt;JWTs are one of those technologies that feel wonderful right up until you hit your first "log me out" requirement. Then you discover the awkward truth: the very property that makes JWTs attractive — statelessness — is also what makes logout hard.&lt;/p&gt;

&lt;p&gt;This post walks through what JWTs actually are, why "invalidating" one is a design problem rather than a one-liner, and the practical methods available to revoke an access token on logout, along with the bottleneck each one introduces.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick refresher on JWTs
&lt;/h2&gt;

&lt;p&gt;A JSON Web Token (JWT) is a signed, self-contained token. It carries a JSON payload of claims — who the user is, when the token was issued, when it expires, and often a unique token id (&lt;code&gt;jti&lt;/code&gt;) — and a cryptographic signature over that payload. Because the token is signed with a secret (or a private key), any server holding the corresponding key can verify it is authentic and untampered &lt;em&gt;without calling a database&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That last part is the entire point. When a request arrives with a JWT, the server checks the signature and the expiry, reads the claims, and proceeds. No lookup, no shared session store, no round trip. This is what people mean when they call JWT auth &lt;strong&gt;stateless&lt;/strong&gt;: the server keeps no per-user session record. The token itself &lt;em&gt;is&lt;/em&gt; the session, and it's valid until it expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access tokens and refresh tokens
&lt;/h2&gt;

&lt;p&gt;In practice you rarely use a single token. The common pattern splits responsibility across two:&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;access token&lt;/strong&gt; is the short-lived workhorse. It's sent on every API request and typically expires in minutes (5–15 is common). Because it's checked statelessly on every call, you want its lifetime short — if it leaks, the damage window is small.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;refresh token&lt;/strong&gt; is long-lived (days or weeks) and does one job: obtain new access tokens when the current one expires. It is not sent on every request — only to a dedicated token endpoint. This lets the access token stay short and stateless while the user avoids logging in every ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refresh token is easy — the access token is the problem
&lt;/h2&gt;

&lt;p&gt;Here's the key insight that shapes everything below: &lt;strong&gt;the refresh token is not the hard part of logout.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Refresh tokens are few, long-lived, and hit a single endpoint. Whoever issues them — your auth provider, or your own token endpoint — already stores them server-side. On logout you simply tell the provider to invalidate that refresh token, and it's done. The user can no longer mint new access tokens. One database write, one endpoint, no impact on your request hot path.&lt;/p&gt;

&lt;p&gt;The access token is where the difficulty actually lives. It's stateless by design: no server stored a copy, and its signature stays valid until the &lt;code&gt;exp&lt;/code&gt; claim passes. So even after you've killed the refresh token, the user's &lt;em&gt;current&lt;/em&gt; access token keeps working until it naturally expires. If you do nothing, a "logged out" user still has a working access token for its remaining lifetime.&lt;/p&gt;

&lt;p&gt;So the real question this post answers is narrow: &lt;strong&gt;how do we stop an already-issued access token from being accepted, without giving up too much of the statelessness we adopted JWTs for?&lt;/strong&gt; Every method below is a point on that tradeoff curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Short expiry and just wait it out
&lt;/h2&gt;

&lt;p&gt;The simplest approach is to not revoke the access token at all. Keep it very short-lived and accept that logout means "invalidate the refresh token, delete the tokens client-side, and let the access token expire on its own."&lt;/p&gt;

&lt;p&gt;Because the refresh token is already dead, the user can't get a new access token — so the only exposure is the current one finishing out its few remaining minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottleneck:&lt;/strong&gt; it doesn't actually revoke anything. A leaked or stolen access token works until it expires, and there is no way to kill it early. This is perfectly fine for low-risk apps with genuinely short tokens, but unacceptable when you need immediate, guaranteed revocation of a specific token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Token blocklist (denylist)
&lt;/h2&gt;

&lt;p&gt;Keep a list of access tokens that have been explicitly revoked, identified by their &lt;code&gt;jti&lt;/code&gt;. On logout, add the token's &lt;code&gt;jti&lt;/code&gt; to the blocklist. On every request, look up the incoming token's &lt;code&gt;jti&lt;/code&gt; before honoring it — if it's on the list, reject it even though the signature is valid.&lt;/p&gt;

&lt;p&gt;The natural home for this is a &lt;strong&gt;cache with a TTL, such as Redis&lt;/strong&gt;. The crucial detail: you set each blocklist entry's TTL to match the token's own remaining validity. Once the token would have expired anyway, the entry evaporates on its own, so the blocklist never grows unbounded — it only ever holds tokens that are both revoked &lt;em&gt;and&lt;/em&gt; not yet naturally expired. Every request does a quick &lt;code&gt;jti&lt;/code&gt; lookup against that cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottleneck:&lt;/strong&gt; you've reintroduced a lookup on every request, which is exactly the statelessness you were trying to avoid. In practice it's a cheap in-memory hit, but it's now a hard dependency on the auth path — if the cache is down or slow, your authentication is down or slow. At scale it becomes shared infrastructure you must replicate, secure, and keep highly available. You've traded some statelessness for the ability to revoke instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Token versioning / "valid-after" timestamp
&lt;/h2&gt;

&lt;p&gt;Store a single small value per user — a token version number or a &lt;code&gt;tokensValidAfter&lt;/code&gt; timestamp. Embed the version (or the token's issue time) in the JWT. On each request, compare the token against the user's current value; if the token predates it, reject it. Logging out is just bumping the version, which instantly invalidates every access token issued before the bump.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottleneck:&lt;/strong&gt; you still need a lookup for the user's current version, so it isn't free — but it's a tiny per-user value rather than a per-token list, which makes it far cheaper to store and trivial to cache. The tradeoff is coarser granularity: bumping the version invalidates &lt;em&gt;all&lt;/em&gt; of that user's access tokens at once. That's exactly what you want for "log out everywhere," but if you need to revoke a single device while leaving others alive, you'd need per-device versioning, which grows the state again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing between them: combine Method 1 and Method 2
&lt;/h2&gt;

&lt;p&gt;For most real systems, the best answer is not to pick one method but to &lt;strong&gt;combine short expiry (Method 1) with a &lt;code&gt;jti&lt;/code&gt; blocklist (Method 2)&lt;/strong&gt; — on top of invalidating the refresh token at the provider.&lt;/p&gt;

&lt;p&gt;Here's why the combination works so well. Method 1 alone can't revoke a specific token early; Method 2 alone would force you to keep a huge, long-lived blocklist if tokens lived for hours. Together they cancel out each other's weaknesses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short expiry keeps the blocklist tiny and self-cleaning.&lt;/strong&gt; Because access tokens only live a few minutes, any blocklist entry's TTL is also only a few minutes. The cache stays small, cheap, and fast no matter how many logouts happen — entries are added on logout and disappear moments later when the token would have expired anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The blocklist closes the gap that short expiry leaves open.&lt;/strong&gt; Method 1 alone means a logged-out or stolen token keeps working for its full remaining lifetime. Adding the &lt;code&gt;jti&lt;/code&gt; blocklist lets you kill that specific token &lt;em&gt;immediately&lt;/em&gt;, so you get instant, guaranteed revocation when you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost stays bounded.&lt;/strong&gt;  be clear about what you're paying. Every single authenticated request now makes a network call to the cache to check the jti. That adds latency to each request — typically a small amount (often sub-millisecond to a few milliseconds on a Cache), but it's real, it's on the critical path, and it scales with your request volume. You're also now depending on the cache being available: if Cache is slow, every request slows down; if it's unreachable, you have to decide whether to fail open (accept tokens you can't verify against the blocklist) or fail closed (reject everyone), and neither is comfortable. And it's another piece of stateful infrastructure to run, replicate, monitor, and secure. The blocklist staying small thanks to short TTLs is genuinely helpful, but it doesn't remove these costs — it only keeps the memory footprint modest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put together, a typical logout flow looks like this: invalidate the refresh token at the provider (so no new access tokens can be minted), add the current access token's &lt;code&gt;jti&lt;/code&gt; to the Cache blocklist with a TTL equal to its remaining lifetime (so the existing token dies immediately), and clear the tokens on the client. Every incoming request then verifies the signature and expiry statelessly, plus one fast &lt;code&gt;jti&lt;/code&gt; lookup — and short token lifetimes guarantee that lookup table stays small.&lt;/p&gt;

&lt;p&gt;The recurring theme is unavoidable: &lt;strong&gt;every method that gives you real revocation power reintroduces some state.&lt;/strong&gt; JWTs let you choose &lt;em&gt;how much&lt;/em&gt; state and &lt;em&gt;where&lt;/em&gt; it sits. Combining short expiry with a small, TTL-bounded &lt;code&gt;jti&lt;/code&gt; blocklist is the sweet spot for most applications — it keeps the request path almost fully stateless while still letting you revoke an access token the instant a user logs out.&lt;/p&gt;

</description>
      <category>security</category>
      <category>jwt</category>
      <category>logout</category>
      <category>invalidation</category>
    </item>
    <item>
      <title>Social signing: log in, sign, never touch the key with AWS Nitro Enclaves</title>
      <dc:creator>Tharindu Karunanayaka</dc:creator>
      <pubDate>Sat, 04 Jul 2026 08:59:19 +0000</pubDate>
      <link>https://dev.to/tharindukn/social-signing-log-in-sign-never-touch-the-key-with-aws-nitro-enclaves-2if6</link>
      <guid>https://dev.to/tharindukn/social-signing-log-in-sign-never-touch-the-key-with-aws-nitro-enclaves-2if6</guid>
      <description>&lt;p&gt;Picture a product where users sign up with a normal social login, get a blockchain wallet without knowing it, and send transactions by clicking a button. No seed phrase, no browser extension, no scary "write down these 12 words or your money is gone forever" screen. To them it feels like any other app. Behind it sits a real on-chain account with a real private key.&lt;/p&gt;

&lt;p&gt;The whole thing lives or dies on one question: if a backend is creating and holding private keys for every user, what happens when someone breaks in? A database full of user private keys is about the worst thing you can leak. So the bar is simple to state and annoying to actually meet: &lt;em&gt;even the people running the service shouldn't be able to read the keys.&lt;/em&gt; Not the developers, not the on-call engineer at 2am, not an attacker who's popped a shell on the box, not whoever ends up with a copy of the database.&lt;/p&gt;

&lt;p&gt;The usual way teams get this experience is to buy it. You integrate a Wallet-as-a-Service provider — Privy, Magic, Web3Auth, Turnkey, etc abd let them handle key creation, custody, and signing behind their SDK. That's a perfectly reasonable choice, and for a lot of teams it's the right one. But it comes with trade-offs: a third party now holds (or co-controls) your users' keys, you're tied to their pricing and uptime, your security story is only as good as their black box, and there's usually a per-wallet or per-signature bill that grows with you. This post is about the other path — building the whole thing yourself, in your own AWS account, so no outside vendor ever touches a private key and the trust boundary is entirely yours.&lt;/p&gt;

&lt;p&gt;Doing that used to mean standing up and babysitting your own HSMs, which is exactly why most people reach for a WaaS in the first place. The shortcut is AWS Nitro Enclaves doing the sensitive work, with KMS acting as a bouncer that will only unlock a key for one specific, provable piece of code. You get HSM-grade key protection without running an HSM, on infrastructure you already have. I'll follow the user's journey — sign up, key gets made, log in, send funds — and bring in the security machinery at the points where it actually matters, rather than dumping it all up front.&lt;/p&gt;

&lt;p&gt;If you're comfortable with OAuth, EC2, IAM and a bit of Python you'll be fine. The examples are deliberately chain-agnostic: wherever you'd otherwise load a raw private key (ed25519, secp256k1, whatever your chain uses) into memory to sign a transaction, the same pattern applies. Slot in your chain's SDK where the placeholders are.&lt;/p&gt;

&lt;p&gt;Here's the shape of it before get into the weeds:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Femc6s7esufrwzshttvra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Femc6s7esufrwzshttvra.png" alt=" " width="800" height="1215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one thing to hold onto: a plaintext private key only ever exists for a few milliseconds, and only ever in two places — inside a sealed enclave and inside a KMS hardware module. It never lands on the app servers, the database, the logs, or the wire. Not even when it's first created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing up, and letting the enclave make the key
&lt;/h2&gt;

&lt;p&gt;The front door is boring on purpose. User taps "sign up," an OpenID Connect flow runs through the identity provider (Cognito in front of the social providers works well here, but Auth0 or Google directly are the same idea), and back comes a JWT with the usual claims — &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;sub&lt;/code&gt; claim is the important one. It's a stable unique ID for the user, and it becomes the handle everything else hangs off of.&lt;/p&gt;

&lt;p&gt;Now, where do you generate the key? The obvious answer is a Lambda, and it works. But it has a nagging problem: for a few milliseconds the raw key sits in ordinary Lambda memory. Nobody's &lt;em&gt;supposed&lt;/em&gt; to be able to read it, but "supposed to" is doing a lot of work in that sentence. The stronger option is to generate the key inside the enclave too. Then the raw key is born inside the sealed box and gets encrypted before anything ever leaves. There's no window, however tiny, where it exists somewhere it could theoretically be read.&lt;/p&gt;

&lt;p&gt;The parent server just asks the enclave (over the local vsock channel — more on that soon) to make a wallet, and passes along some temporary AWS credentials so the enclave can reach KMS. This code runs &lt;em&gt;inside&lt;/em&gt; the enclave:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;

&lt;span class="n"&gt;KMS_KEY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:kms:us-east-1:123456789012:key/abcd-…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kms_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# The enclave has no network of its own; it reaches KMS through the
&lt;/span&gt;    &lt;span class="c1"&gt;# local vsock proxy the parent runs (same path used for decryption).
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_key_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;aws_session_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Runs INSIDE the enclave. The plaintext private key never leaves this function.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_keypair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# your chain's keygen (ed25519, secp256k1, …)
&lt;/span&gt;    &lt;span class="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kms_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;KeyId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KMS_KEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Plaintext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# the raw private key material
&lt;/span&gt;    &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CiphertextBlob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;# public address, fine to hand back
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encrypted_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;          &lt;span class="c1"&gt;# only ciphertext leaves the enclave
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the enclave hands back the public address and the ciphertext, the parent's job is trivial: drop that ciphertext into Secrets Manager under the user's &lt;code&gt;sub&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secretsmanager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aws_creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                  &lt;span class="c1"&gt;# who is this user?
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_enclave&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credential&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aws_creds&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wallet/user/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SecretString&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# { address, encrypted_key }
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="c1"&gt;# show the user their shiny new address
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnywyxio5f8ffrefjeoir.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnywyxio5f8ffrefjeoir.webp" alt=" " width="799" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the key actually lives
&lt;/h2&gt;

&lt;p&gt;The encrypted key goes into AWS Secrets Manager, one secret per user, named &lt;code&gt;wallet/user/&amp;lt;sub&amp;gt;&lt;/code&gt;. What's stored there is already useless on its own — it's envelope ciphertext that can't be unwrapped without both KMS and the enclave, which is the whole point of the next section. So even if someone dumped every secret in the account, they'd have a pile of garbage.&lt;/p&gt;

&lt;p&gt;Secrets Manager is a good fit here for a few reasons beyond just holding bytes. It re-encrypts everything at rest with its own KMS key, so the key ends up wrapped twice: once by the master key inside the enclave, then again by Secrets Manager. It gives you per-secret IAM, CloudTrail on every read, and versioning, all of which are nice to have when the thing you're storing is somebody's wallet. Scope the parent's role so it can only touch &lt;code&gt;wallet/user/*&lt;/code&gt; and nothing else in the account.&lt;/p&gt;

&lt;p&gt;One detail on cost and key management, since it comes up: use a &lt;em&gt;single&lt;/em&gt; KMS master key to encrypt every user's private key. A private key is nowhere near KMS's 4 KB encryption limit, so there's no reason to mint a KMS key per user, and KMS bills per master key, so per-user keys would be financially silly. The per-user isolation that matters doesn't come from having millions of master keys anyway — it comes from each ciphertext being distinct and from the decrypt being gated. Which, finally, brings us to the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftsih5bxqf0bvjzw1iz7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftsih5bxqf0bvjzw1iz7d.png" alt=" " width="798" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending a transaction by logging in
&lt;/h2&gt;

&lt;p&gt;This is the payoff and also where all the security machinery earns its keep. A returning user logs in, gets a fresh JWT, asks to send some funds, and the system signs the transaction with their key — without that key ever being visible to the servers. To believe that last clause you need to understand three things, so here they are roughly in the order you'd hit them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sealed box
&lt;/h3&gt;

&lt;p&gt;A Nitro Enclave is a stripped-down virtual machine carved out of a normal EC2 instance. No disk, no network card, no SSH, and — this is the part that matters — no way for anyone to read its memory, including root on the parent instance. The only channel in or out is a local socket called vsock, addressed by a context ID (think of it like an IP) and a port. Only the key-handling code runs in there: creating wallets and signing. Everything else — the web server, JWT checks, Secrets Manager calls — runs on the parent, outside the box, which you should assume could get compromised.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwotetsfpumdsd5jg4jn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwotetsfpumdsd5jg4jn.png" alt=" " width="800" height="1253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  PCR0, or how KMS knows it's really your code
&lt;/h3&gt;

&lt;p&gt;For KMS to hand a key only to your signing code, it needs some way to recognize that code. That mechanism is PCR0.&lt;/p&gt;

&lt;p&gt;PCR is short for Platform Configuration Register. When an enclave boots, the Nitro hypervisor measures the image as it loads it — hashing the contents into a set of registers that the enclave itself can't write to. If you've ever poked at a TPM and secure boot, it's the same idea. Only the hypervisor sets these values, at boot, which is exactly why you can trust them. There are a few of them:&lt;/p&gt;

&lt;p&gt;PCR0 is a SHA-384 hash of the entire enclave image, kernel and app and all. That's the one you'll usually use. PCR1 covers just the kernel and bootstrap, PCR2 just the app without the kernel, and PCR8 is the certificate you signed the image with, if you signed it (handy if you'd rather pin "anything the build pipeline signed" than one exact hash).&lt;/p&gt;

&lt;p&gt;The thing to internalize about PCR0 is that it's a content hash, so it's completely deterministic and completely unforgiving. Rebuild the exact same image and you get the exact same PCR0. Change one line, bump a dependency, nudge the base image, and it's a totally different value. You see it printed when you build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nitro-cli build-enclave &lt;span class="nt"&gt;--docker-uri&lt;/span&gt; signing-server:latest &lt;span class="nt"&gt;--output-file&lt;/span&gt; signing_server.eif
&lt;span class="c"&gt;# Measurements:&lt;/span&gt;
&lt;span class="c"&gt;#   "PCR0": "abacc679…cacd9fc0373ecd78c34c3e4cbf78ea9b5b0452a18"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That string is the fingerprint of your exact code, and it's what KMS checks before it will decrypt anything. Two things trip people up here, so consider yourself warned. First, because PCR0 changes on every code change, shipping a new enclave build means updating the KMS policy — allow both the old and new value during a rollout so you don't lock yourself out mid-deploy (signing images and pinning PCR8 is the cleaner long-term answer). Second, if you run with &lt;code&gt;--debug-mode&lt;/code&gt; to see console output, attestation is disabled and all the PCRs go to zero. Great for local testing, a disaster if you point it at real keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attestation, and the part everyone gets wrong
&lt;/h3&gt;

&lt;p&gt;When the enclave asks KMS to decrypt, it doesn't send a plain API call. The Nitro SDK wraps the request in a signed attestation document that carries the PCR values and a one-time public key the enclave just generated. KMS checks that the document genuinely came from Nitro hardware and that PCR0 matches the condition in the key policy.&lt;/p&gt;

&lt;p&gt;(Worth noting the asymmetry with sign-up: encrypting the key back during account creation needed no attestation at all — anyone with the right IAM can encrypt. Decrypting is the dangerous direction, so that's the one that's locked down.)&lt;/p&gt;

&lt;p&gt;Here's the bit that's easy to get wrong, and almost everyone does at first: the decryption happens in &lt;em&gt;two&lt;/em&gt; places, not one.&lt;/p&gt;

&lt;p&gt;The real decryption happens inside KMS, not in the enclave. The master key never leaves the hardware module; KMS unwraps the ciphertext into the raw key on its side. But it obviously can't just send that plaintext back over the wire, since the reply travels through the untrusted parent to get to the enclave. So instead KMS re-encrypts the plaintext using that one-time public key from the attestation document and returns &lt;em&gt;that&lt;/em&gt; (the &lt;code&gt;CiphertextForRecipient&lt;/code&gt; field). Then a second decryption happens locally, inside the enclave, using the matching one-time private key that never left it.&lt;/p&gt;

&lt;p&gt;You don't hand-roll any of this. The &lt;code&gt;kmstool_enclave_cli&lt;/code&gt; binary that AWS ships does both halves — it builds the attested request (that's decryption #1, in KMS) and does the local unseal (decryption #2, in the enclave), and just hands you the plaintext.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0ro147b5jklyw78ez04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0ro147b5jklyw78ez04.png" alt=" " width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The policy condition doing the enforcing is one line, &lt;code&gt;kms:RecipientAttestation:ImageSha384&lt;/code&gt;, which reads as "only an enclave whose PCR0 equals this may decrypt":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow decrypt only from our enclave"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;ACCOUNT_ID&amp;gt;:role/ec2-instance-role"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringEqualsIgnoreCase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kms:RecipientAttestation:ImageSha384"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR_PCR0_VALUE&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So stealing the instance credentials gets you nowhere. Dumping every secret gets you nowhere. To decrypt a key you'd have to &lt;em&gt;be the exact code, running inside a genuine enclave&lt;/em&gt; — and even that wouldn't help you, for reasons I'll come back to at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wiring it together
&lt;/h3&gt;

&lt;p&gt;The full send path: verify the login JWT, pull the user's ciphertext out of Secrets Manager by &lt;code&gt;sub&lt;/code&gt;, fill in the public chain data the transaction needs (nonce, fee, gas, chain ID — whatever your chain uses), then push the ciphertext and the unsigned transaction into the enclave to sign.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fomlh57f30bg610e5dv3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fomlh57f30bg610e5dv3j.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Verifying the login token on the parent is a few lines of PyJWT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PyJWKClient&lt;/span&gt;

&lt;span class="n"&gt;jwks_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PyJWKClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://YOUR_IDP_DOMAIN/.well-known/jwks.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;signing_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwks_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_signing_key_from_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-api-identifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://YOUR_IDP_DOMAIN/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;                                  &lt;span class="c1"&gt;# ["sub"] == the user's Secrets Manager key
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on stepping up to MFA for bigger transfers.&lt;/strong&gt; The login JWT tells you who the user is, but for a large payment you want fresh proof that the actual human is behind &lt;em&gt;this particular transaction&lt;/em&gt;, not just that they logged in an hour ago. The pattern for that is step-up authentication. The parent runs some business logic first — over some value threshold, say, or past a daily cumulative limit, or a destination the account has never sent to — and if the transaction trips that rule it refuses to sign and sends back a challenge instead. The app then prompts for MFA (a TOTP code, a passkey, a push), and on success the IdP mints a &lt;em&gt;second&lt;/em&gt; JWT that's scoped to this one transaction: bound to the exact amount and destination, with an expiry measured in a minute or two so it can't be replayed on a different or later transfer. The parent checks both tokens — the session one for identity, the step-up one for this specific transaction — before anything reaches the enclave.&lt;/p&gt;


&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;require_step_up_if_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step_up_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                           &lt;span class="c1"&gt;# small tx: session JWT is enough
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;step_up_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;StepUpRequired&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                    &lt;span class="c1"&gt;# tell the app to prompt for MFA
&lt;/span&gt;    &lt;span class="n"&gt;mfa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step_up_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# same signature/claims checks
&lt;/span&gt;    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mfa&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;session_claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="c1"&gt;# bind it to THIS user + THIS transfer
&lt;/span&gt;            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mfa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mfa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;destination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mfa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;mfa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])):&lt;/span&gt;   &lt;span class="c1"&gt;# amr = auth methods actually used
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The nice thing is that it's a completely separate lock that slots in &lt;em&gt;before&lt;/em&gt; the key machinery, and it doesn't touch any of it. The enclave, the attestation, the KMS policy — all unchanged. You've just made the "should this transfer happen at all" decision stricter for the transactions that deserve the extra friction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The parent side, then, authenticates, loads the ciphertext, fills in the public chain data, and relays. It never touches a plaintext key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secretsmanager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChainClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-node.example:PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# your chain's RPC client
&lt;/span&gt;&lt;span class="n"&gt;ENCLAVE_CID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ENCLAVE_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_enclave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_VSOCK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOCK_STREAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ENCLAVE_CID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ENCLAVE_PORT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aws_creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                   &lt;span class="c1"&gt;# 401 if invalid
&lt;/span&gt;    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_secret_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wallet/user/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SecretString&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Build the unsigned tx and fill in public chain data (nonce, fee, gas, chain id…).
&lt;/span&gt;    &lt;span class="n"&gt;unsigned_tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_tx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_addr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;unsigned_tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fill_chain_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unsigned_tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# public data, no key needed
&lt;/span&gt;
    &lt;span class="n"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_enclave&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sign&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unsigned_tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encrypted_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encrypted_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credential&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aws_creds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                &lt;span class="c1"&gt;# temp instance creds
&lt;/span&gt;    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed_tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;                 &lt;span class="c1"&gt;# submit; needs no key
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does the enclave need AWS credentials handed to it? Because it has no network of its own. It borrows the parent's temporary instance credentials, tunneled through the same vsock proxy, to reach KMS. On their own those credentials are useless for this — without a valid attestation document, KMS just refuses to decrypt.&lt;/p&gt;

&lt;p&gt;And the enclave itself, which is a single small server handling both the &lt;code&gt;create&lt;/code&gt; from sign-up and the &lt;code&gt;sign&lt;/code&gt; here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="n"&gt;VSOCK_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kms_decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ciphertext_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;kmstool_enclave_cli does decryption #1 (KMS) and #2 (local unseal).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/app/kmstool_enclave_cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--proxy-port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--aws-access-key-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_key_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--aws-secret-access-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--aws-session-token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--ciphertext&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext_hex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;# "PLAINTEXT: &amp;lt;base64&amp;gt;"
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# the raw private key
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;create_wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credential&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;          &lt;span class="c1"&gt;# from sign-up
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sign&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;private_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kms_decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encrypted_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credential&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;signed_tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# your chain's signer
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed_tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;signed_tx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;private_key&lt;/span&gt;                              &lt;span class="c1"&gt;# don't let plaintext outlive the request
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_VSOCK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOCK_STREAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VMADDR_CID_ANY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VSOCK_PORT&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire trust boundary, and it's small enough to read in one sitting. The same sealed box both mints keys and signs with them, and in either case the plaintext key only exists inside the enclave — created in &lt;code&gt;create_wallet&lt;/code&gt; or briefly revived in &lt;code&gt;kms_decrypt&lt;/code&gt;, used once, and dropped before any reply crosses back over vsock. (&lt;code&gt;generate_keypair&lt;/code&gt; and &lt;code&gt;sign_transaction&lt;/code&gt; are the only two chain-specific pieces; everything else is identical no matter which blockchain you're on.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Building and running it
&lt;/h2&gt;

&lt;p&gt;The enclave app is a Docker image that you convert into an enclave image file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; amazonlinux:2&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3 gcc python3-devel &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip3 &lt;span class="nb"&gt;install &lt;/span&gt;boto3 &amp;lt;your-chain-sdk&amp;gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; kmstool_enclave_cli /app/kmstool_enclave_cli&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; libnsm.so /app/libnsm.so&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; server.py /app/server.py&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python3", "/app/server.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nitro-cli build-enclave &lt;span class="nt"&gt;--docker-uri&lt;/span&gt; signing-server:latest &lt;span class="nt"&gt;--output-file&lt;/span&gt; signing_server.eif
&lt;span class="c"&gt;# ^ prints PCR0 → paste it into the KMS key policy&lt;/span&gt;
nitro-cli run-enclave &lt;span class="nt"&gt;--cpu-count&lt;/span&gt; 2 &lt;span class="nt"&gt;--memory&lt;/span&gt; 3806 &lt;span class="se"&gt;\&lt;/span&gt;
                      &lt;span class="nt"&gt;--eif-path&lt;/span&gt; signing_server.eif &lt;span class="nt"&gt;--enclave-cid&lt;/span&gt; 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha: enclaves aren't managed by Docker or systemd, so nothing restarts them for you if they die. A tiny watchdog under systemd that polls &lt;code&gt;nitro-cli describe-enclaves&lt;/code&gt; and relaunches the enclave when it's gone covers this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Back at the start, the choice looked like "buy a Wallet-as-a-Service and hand off custody" or "run your own HSMs and hate your life." This is the third door. You get HSM-grade key protection out of infrastructure you already run, you own the entire trust boundary end to end, and the only two pieces that are chain-specific are &lt;code&gt;generate_keypair&lt;/code&gt; and &lt;code&gt;sign_transaction&lt;/code&gt; — everything else is the same whether you're on Ethereum, Solana, Bitcoin, or something that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;It's not a small amount of moving parts, and the attestation flow takes a beat to click. But once it does, the mental model is clean: social login decides &lt;em&gt;who&lt;/em&gt; can act, the JWT and step-up MFA decide &lt;em&gt;what&lt;/em&gt; they can do, PCR0 decides &lt;em&gt;which code&lt;/em&gt; is allowed to touch a key, and the enclave plus KMS make sure the plaintext only ever exists somewhere nobody — including you — can reach into. That's a wallet you can put in front of ordinary users and still describe honestly: we made your key, we help you spend it, and we can't read it.&lt;/p&gt;

&lt;p&gt;If you take one thing away, let it be that "non-custodial-ish, self-hosted, no vendor holding the keys" is not a moonshot anymore. It's a weekend spike on top of a reference implementation. So go build it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More info&lt;/strong&gt; — the full working reference implementation (VPC, KMS policy, enclave build, CDK) is open source: &lt;a href="https://github.com/aws-samples/aws-nitro-enclave-blockchain-wallet" rel="noopener noreferrer"&gt;aws-samples/aws-nitro-enclave-blockchain-wallet&lt;/a&gt;. Clone it, deploy it, and read the parts this post glossed over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next steps&lt;/strong&gt; — two directions worth exploring once the single-enclave version is working:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remove the single point of trust with MPC.&lt;/strong&gt; Right now one enclave can reconstruct a whole key. Split it so no single enclave (or single AWS account) ever holds the complete key, and a signature needs several parties to cooperate: &lt;a href="https://aws.amazon.com/blogs/web3/build-secure-multi-party-computation-mpc-wallets-using-aws-nitro-enclaves/" rel="noopener noreferrer"&gt;Build secure multi-party computation (MPC) wallets using AWS Nitro Enclaves&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it production-grade.&lt;/strong&gt; Add key backup and recovery (what happens if a user loses access?), multi-AZ high availability for the enclave and monitoring/alerting on failed attestations.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>blockchain</category>
      <category>wallet</category>
    </item>
  </channel>
</rss>
