When TEEs become insecure for private key operations

A look at how a JWT authentication vulnerability led to $400k+ in drained wallets—and what it reveals about the tradeoffs between frictionless UX and wallet security.

Some thoughts on the Kontigo app wallet vulnerability.

What happened

The attacker exploited a vulnerability in Supabase's GoTrue auth server. By using an OpenID Connect compliant ID token issuer under their control, they could authenticate as any legitimate user.

This allowed the attacker to mint JWT tokens and impersonate 1,005 users, draining 340,905.28 USDT from their wallets.

Three days later, the attacker used pre-issued TEE wallet JWTs to drain an additional 56,913 USDC. The TEE trusted these tokens, which had a default validity of 30 days.

Why JWTs matter for wallet security

How can a JWT lead to draining a wallet?

Most TEE-based embedded wallets require a valid user token to reconstruct the user's private key. A TEE wallet JWT is issued after verifying an incoming OIDC-compatible JWT against the configured JWKS.

The root of trust is in that token. If it's exposed or forged, you're cooked.

How this could have been avoided

Require user interaction whenever they interact with the TEE to reconstruct the private key. This way, JWTs would not be the root of the trust.

Options include:

  • OTP verification
  • Passkeys
  • Authenticator apps

This attack is a reminder that frictionless always comes with trade-offs.

A note on custody claims

Is the Kontigo app a self-custody wallet? No, but now at least it's being called non-custodial in the post-mortem.

That claim is also questionable. Kontigo could mint access tokens via Supabase Admin endpoints on behalf of their users—tokens that would be valid to create new TEE JWTs.

What this means for OpenSigner

Let me take these learnings home and apply them to OpenSigner: wallet security is a spectrum. Developers should be able to tune the friction of the wallets they provide based on the assets they secure.

OpenSigner is an open-source on-device signing solution based on SSS with 2/3 quorum. It supports 3 encryption methods for securing its cold share:

shamir-secret-sharing

For its relation to the Kontigo attack, I'll focus on automatic recovery. The other two methods would not be affected by a similar attack because user input is required by default.

How automatic recovery works

Whenever a user creates or recovers their wallet on a new device, they must decrypt the cold share.

In automatic recovery, the developer is given an encryption share they must safeguard at all times. This encryption share is part of a 2/2 quorum encryption key required to encrypt the cold share.

encryption-key

By default, the system's root of trust is with the access token—it must be secured at all times.

The developer must expose a backend endpoint to create a one-time encryption session based on the encryption share they own.

This introduces a first barrier to a similar attack, because the developer can check that the request to create an encryption session comes from their own front-end.

To avoid the same pitfall where an access token could be forged, we developed an OTP-based solution that requires users to provide input to create the encryption session.

Key takeaways

  • JWTs as root of trust are risky — If your TEE wallet relies solely on JWT verification, a compromised auth layer means compromised wallets
  • Frictionless UX has security costs — Every step you remove is a layer of protection gone
  • User interaction matters — Requiring OTP, passkeys, or authenticator input during key reconstruction adds meaningful security

Building secure wallets means finding the right balance between convenience and protection. OpenSigner gives developers the tools to make that choice.