Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why not use the PRF extension for WebAuthN signer #246

Closed
ennioVisco opened this issue Jan 31, 2024 · 11 comments
Closed

Why not use the PRF extension for WebAuthN signer #246

ennioVisco opened this issue Jan 31, 2024 · 11 comments

Comments

@ennioVisco
Copy link

We are working on a passkey implementation of signers, so we familiarize a lot with the new example that combines it with EIP-4337 (we were working at this part right now).

However, we are curious about a choice:

const key = await crypto.subtle.importKey(
'spki',
passkeyCredential.response.getPublicKey(),
{
name: 'ECDSA',
namedCurve: 'P-256',
hash: { name: 'SHA-256' },
},
true, // boolean that marks the key as an exportable one
['verify'],
)

In this lines you export the private key, potentially making it vulnerable to in-memory attacks.
Why not using instead the prf extension of WebAuthN (currently supported only on Chrome), to avoid exposing the key altogether?

@mmv08
Copy link
Member

mmv08 commented Feb 1, 2024

We are working on a passkey implementation of signers, so we familiarize a lot with the new example that combines it with EIP-4337 (we were working at this part right now).

However, we are curious about a choice:

const key = await crypto.subtle.importKey(
'spki',
passkeyCredential.response.getPublicKey(),
{
name: 'ECDSA',
namedCurve: 'P-256',
hash: { name: 'SHA-256' },
},
true, // boolean that marks the key as an exportable one
['verify'],
)

In this lines you export the private key, potentially making it vulnerable to in-memory attacks. Why not using instead the prf extension of WebAuthN (currently supported only on Chrome), to avoid exposing the key altogether?

Hmm, in my understanding, the snippet you referenced retrieves the public key from the WebAuthN response and imports it into the crypto package. If I remember correctly, the import was needed to convert the public key to get the X and Y coordinates for the signer deployment. I think the majority of authenticators do not support exporting private keys (depends on the implementation, of course)

@ennioVisco
Copy link
Author

ennioVisco commented Feb 1, 2024

We are working on a passkey implementation of signers, so we familiarize a lot with the new example that combines it with EIP-4337 (we were working at this part right now).
However, we are curious about a choice:

const key = await crypto.subtle.importKey(
'spki',
passkeyCredential.response.getPublicKey(),
{
name: 'ECDSA',
namedCurve: 'P-256',
hash: { name: 'SHA-256' },
},
true, // boolean that marks the key as an exportable one
['verify'],
)

In this lines you export the private key, potentially making it vulnerable to in-memory attacks. Why not using instead the prf extension of WebAuthN (currently supported only on Chrome), to avoid exposing the key altogether?

Hmm, in my understanding, the snippet you referenced retrieves the public key from the WebAuthN response and imports it into the crypto package. If I remember correctly, the import was needed to convert the public key to get the X and Y coordinates for the signer deployment. I think the majority of authenticators do not support exporting private keys (depends on the implementation, of course)

Oh. I see, you're right. So, to sign you simply use the following lines:

const safeInitOpHash = ethers.TypedDataEncoder.hash(
{ verifyingContract: SAFE_SIGNER_LAUNCHPAD_ADDRESS, chainId },
{
SafeInitOp: [
{ type: 'bytes32', name: 'userOpHash' },
{ type: 'uint48', name: 'validAfter' },
{ type: 'uint48', name: 'validUntil' },
{ type: 'address', name: 'entryPoint' },
],
},
safeInitOp,
)
const assertion = (await navigator.credentials.get({
publicKey: {
challenge: ethers.getBytes(safeInitOpHash),
allowCredentials: [{ type: 'public-key', id: hexStringToUint8Array(passkey.rawId) }],
},
})) as Assertion | null

where essentially the message to sign is safeInitOpHash and is provided as a challenge to the authenticator, am I right?
This can make sense, but it puzzles me a bit... e.g. are you sure the challenge message can contain as much data as a transaction normally has?

@mmv08
Copy link
Member

mmv08 commented Feb 1, 2024

This can make sense, but it puzzles me a bit... e.g. are you sure the challenge message can contain as much data as a transaction normally has?

The standard says that the challenge is an arbitrary bytes array: https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-challenge

What ends up signed is a 32-byte transaction hash, which is not a large chunk of data. I think they accept arbitrary length data because what ends up being signed is a hash of (authenticator data + hash(client data json, which includes the challenge)). You can see the "Signing procedure" details in the spec here: https://w3c.github.io/webauthn/#signing-procedure

@ennioVisco
Copy link
Author

Thanks for sharing these insights. The link to the specification was pretty useful to understand the details.
The implementation looks coherent and safe 😄

@mmv08
Copy link
Member

mmv08 commented Feb 2, 2024

Thanks for sharing these insights. The link to the specification was pretty useful to understand the details. The implementation looks coherent and safe 😄

Thank you for sharing the PRF extension! @nlordell had an idea to use the generated key as a private key on a native secp256k1 curve, thus avoiding the expensive on-chain secp256r1 verification. Limited PRF extension support might make it tricky, though.

@ennioVisco
Copy link
Author

ennioVisco commented Feb 2, 2024

Thanks for sharing these insights. The link to the specification was pretty useful to understand the details. The implementation looks coherent and safe 😄

Thank you for sharing the PRF extension! @nlordell had an idea to use the generated key as a private key on a native secp256k1 curve, thus avoiding the expensive on-chain secp256r1 verification. Limited PRF extension support might make it tricky, though.

Yeah, that's pretty much what we are testing right now. Tbh it turns out that most of the devices that do not support prf support largeBlob (the Apple ones). Still, extensions require newest versions of the OS (September 2023+ mostly), and I'm not sure I'd recommend that strategy compared to the one you adopted, because in this case we couldn't find a way to avoid exposing the private key (to the running code).
That is also because the deriveKey method does not seem to support asymmetric keys for some reasons :/

@nlordell
Copy link
Collaborator

nlordell commented Feb 2, 2024

For reference I documented it here: #249

@nlordell
Copy link
Collaborator

nlordell commented Feb 2, 2024

in this case we couldn't find a way to avoid exposing the private key (to the running code).

I guess this depends on your threat model right? If you are concerned with leaking secrets from memory, then all signing needs to happen on a trusted authenticator, and the PRF method isn't well suited. That's a pretty hardcore threat model though 😅.

@ennioVisco
Copy link
Author

then all signing needs to happen on a trusted authenticator

But this is what's happening in your current implementation, isn't it?

That's a pretty hardcore threat model though 😅.

That's probably true, yet a compelling reason to say that a WebAuthN-based wallet is even more secure than traditional wallets...

@nlordell
Copy link
Collaborator

nlordell commented Feb 7, 2024

But this is what's happening in your current implementation, isn't it?

Correct. But if we would use PRF then this wouldn't be the case right? Since the client (i.e. browser JS runtime) have access to raw key material.

yet a compelling reason to say that a WebAuthN-based wallet is even more secure than traditional wallets...

To me, this makes them equivalent to hardware wallets. The big advantage of Passkey over existing hardware wallets is that devices you already own (your smartphone or Yubikey) become Ethereum-enabled signers. So "hardware wallet availability".

@ennioVisco
Copy link
Author

ennioVisco commented Feb 7, 2024

But this is what's happening in your current implementation, isn't it?

Correct. But if we would use PRF then this wouldn't be the case right? Since the client (i.e. browser JS runtime) have access to raw key material.

yet a compelling reason to say that a WebAuthN-based wallet is even more secure than traditional wallets...

To me, this makes them equivalent to hardware wallets. The big advantage of Passkey over existing hardware wallets is that devices you already own (your smartphone or Yubikey) become Ethereum-enabled signers. So "hardware wallet availability".

Exactly, so to sum up, also for future readers of this thread:

  • go with PRF if your goal is to have a wallet which is as secure as typical wallet applications (e.g. Metamask, Coinbase Wallet etc.), where the Javascript runtime has access to the secret key. Drawback: it requires more recent devices. Advantage: transactions are cheaper
  • go with classical WebAuthN if your goal is to have something as secure as typical hard wallets (e.g. Ledger, Trezor, etc.), where only a secure device (the phones' TPM or a plugged-in one) has access to private keys. Drawback: transactions are more expensive. Advantage: it is generally available and supported by at least 2 years older devices.

It is worth mentioning that also the largeBlob extension (for devices that do not support the PRF extension) can be equivalent, as it allows to store a secret on the secure device, therefore the Ethereum private key can be stored directly and used on-demand

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants