How Keyless Works
Aptos Keyless enables a dApp to derive and access a blockchain account for a user who successfully signed in to the dApp via an OIDC provider (e.g., Google). Importantly, this blockchain account is scoped to the dApp. This means other dApps, who can similarly sign-in the same user, via the same OIDC provider, are not able to access this account and instead get their own account.
But how does this work?
Overview
At a very high level, a successful sign-in into the dApp via the OIDC provider will result in the dApp receiving a JSON Web Token (JWT) signed by the OIDC provider. The JWT will contain, among other things, three important pieces of information:
- The user’s identity (contained in the JWT’s
sub
field) - The dApp’s identity (contained in the JWT’s
aud
field) - Application-specific data; specifically, an ephemeral public key (EPK) (contained in the JWT’s
nonce
field), whose associated ephemeral secret key (ESK) only the user knows.
Now, assume that the user’s blockchain account address is (more or less) a hash of the user’s identity in sub
and the dApp’s identity in aud
from above.
Then, the key observation is that the signed JWT effectively acts as a digital certificate, temporarily binding this blockchain address to the EPK, and allowing the EPK to sign TXNs for it. In other words, it securely delegates TXN signing rights for this blockchain account to the EPK. (Note: The EPK contains an expiration date and is thus short-lived.)
Importantly, if the user loses their ESK, the user can obtain a new signed JWT over a new EPK via the application by simply signing in again via the OIDC provider. (Or, in some cases, by requesting a new signed JWT using an OAuth refresh token.)
With this system, the challenge is maintaining privacy, since revealing the JWT on-chain would leak the user’s identity. Furthermore, revealing the EPK to the OIDC provider would allow it to track the user’s TXNs on-chain.
We explain below how Keyless accounts work and how they address these challenges.
Flow: Deriving a keyless account for a user in a dApp
First, let us look at how a dApp can sign-in a user via (say) Google, derive that user’s keyless blockchain address and, for example, send that user an asset.
Step 1: The user generates an ephemeral key pair: an EPK with an expiration date, and its associated ESK. The dApp keeps the EPK and safely stores the ESK on the user-side (e.g., in the browser’s local storage, or in a trusted enclave if the ESK is a WebAuthn passkey).
Step 2: The dApp commits to the EPK as , where is a blinding factor. When the user clicks on the “Sign in with Google” button, the dApp redirects the user to Google’s sign in page and, importantly, sets the nonce
parameter in the URL to this EPK commitment. This hides the EPK from Google, maintaining privacy of the user’s TXN activity.
Step 3: Typically, the user has an HTTP cookie from having previously-signed-in to their Google account, so Google merely checks this cookie. If the user has multiple Google accounts, Google asks the user to select which one they want to sign-in into the dApp. (The less common path is for the user to have to type in their Google username and password.)
Step 4: Once the user has signed in, Google sends the dApp a signed JWT, which includes the user's sub
identifier (e.g., uid-123
), the application’s aud
identifier (e.g., "dapp-xyz"
) and the nonce
with the EPK commitment. (This assumes that the dApp has previously registered with Google and received this "dapp-xyz"
identifier.)
Step 5: The dApp now has almost everything it needs to derive a keyless account for the user: the user’s identifier (sub
) and the dApp’s identifier (aud
). But, to preserve the privacy of the user, the dApp will use a third piece of information: a blinding factor called a pepper. The dApp will contact a so-called guardian who will deterministically derive a random from the given (sub
, aud
). Importantly, the guardian will only reveal to the dApp upon seeing a validly-signed JWT for the queried (sub
, aud
).
Step 6: The dApp derives the address of the account as , where is a cryptographic hash function.
Note that the pepper is used to hide the user and app identity inside the address since, as we described above, only an authorized user with a valid JWT will be able to obtain this pepper.
Also, note that the address is independent of the EPK. This is why the ESK need not be long-lived and can be lost.
Finally, the dApp can, for example, send an NFT to the user at their address .
But how can the dApp authorize TXN from this account at ? We discuss that next.
Flow: Obtaining a zero-knowledge proof before transacting
In the previous flow, we showed how a dApp can sign in a Google user and derive their privacy-preserving keyless address, with the help of a guardian.
Next, we show how this dApp can obtain a zero-knowledge proof (ZKP), which will allow it to authorize transactions from this address for the user. Importantly, the transaction will hide the user’s identifying information (e.g., the sub
field).
Step 1: The dApp sends all the necessary public information (i.e., , ) and private information (i.e., JWT, signature from Google, EPK blinding factor , and pepper ) to the prover service.
Step 2: The prover derives the user’s address and computes a zero-knowledge proof (ZKP) for the keyless relation (described below). This proof acts as a privacy-preserving digital certificate, and binds the user's address to the ephemeral public key . The prover then sends to the dApp.
In order to bind the with the user's address , the ZKP will be used to convince the validators that the user is in possession of (1) a JWT signed by Google, (2) which commits to the in its nonce
field, and (3) contains the same information as in the address, without leaking anything about the JWT, its signature , , or .
More formally, the ZKP convinces a verifier (i.e., the blockchain), who has public inputs , that the prover knows secret inputs such that the relation depicted below holds:
Recall from before that the signed JWT itself binds the blockchain address to , so that can sign transactions for . However, the JWT would leak the user’s identity, so the ZKP serves to hide the JWT (and other private information) while arguing that the proper checks hold (i.e., the checks in ).
Next, we show how the dApp can now authorize TXNs from .
Flow: Sending a TXN from a keyless account
The previous flow explained how a dApp can obtain a ZKP from the prover service. Next, we describe how the dApp leverages this ZKP to transact for the account.
Step 1: The dApp obtains an ephemeral signature over the TXN from the user. This could be done behind the user’s back, by the dApp itself who might manage the ESK. Or, it could be an actual signing request sent to the user, such as when the ESK is a WebAuthn passkey, which is stored on the user’s trusted hardware.
Step 2: The dApp sends the TXN, the ZKP , the ephemeral public key , and the ephemeral signature to the blockchain validators.
Step 3: To check the TXN is validly-signed, the validators perform several steps: (1) check that has not expired, (2) fetch the user’s address from the TXN, (3) verify the ZKP against , and (4) verify the ephemeral signature on the TXN against the . If all these checks pass, they can safely execute the TXN.