-
Notifications
You must be signed in to change notification settings - Fork 221
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
Passkey support #808
Passkey support #808
Conversation
const safeProvider = new SafeProvider({ provider: this.#provider, signer: this.#signer }) | ||
const safeProvider = new SafeProvider({ | ||
provider: this.#provider, | ||
signer: this.#signer as string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid the cast ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done! I created a createSafeProvider
util function to create a proper SafeProvider
object based on the signer
provided by the user.
I added this in the other PR (passkey-4337-support) to be able to use it also in the relay-kit. With this change the protocol-kit, the relay-kit and the account-abstraction-kit is compatible with passkeys.
@@ -29,6 +29,7 @@ import ModuleManager from './managers/moduleManager' | |||
import OwnerManager from './managers/ownerManager' | |||
import { | |||
AddOwnerTxParams, | |||
AddPasskeyOwnerTxParams, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why two separate types? Could we use an Union type instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added two separate types to create guards:
export function isAddOwnerTxParams(
params: AddOwnerTxParams | AddPasskeyOwnerTxParams
): params is AddOwnerTxParams {
return (params as AddOwnerTxParams).ownerAddress !== undefined
}
export function isAddPasskeyOwnerTxParams(
params: AddOwnerTxParams | AddPasskeyOwnerTxParams
): params is AddPasskeyOwnerTxParams {
return (params as AddPasskeyOwnerTxParams).passkey !== undefined
}
packages/protocol-kit/src/Safe.ts
Outdated
const isPasskeySigner = await this.#safeProvider.isPasskeySigner() | ||
if (isPasskeySigner) { | ||
const txHash = await this.getTransactionHash(transaction) | ||
const signedHash = await this.#safeProvider.signMessage(txHash) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually do this.signHash(txHash)
for signing tx hashes and will adjust the V and return the EthSafeSignature object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true! I updated this in the 4337 support PR:
const isPasskeySigner = await this.#safeProvider.isPasskeySigner()
if (isPasskeySigner) {
const txHash = await this.getTransactionHash(transaction)
signature = await this.signHash(txHash)
packages/protocol-kit/src/Safe.ts
Outdated
const chainId = await safeProvider.getChainId() | ||
const customContracts = contractNetworks?.[chainId.toString()] | ||
|
||
const safeWebAuthnSignerFactoryContract = await getSafeWebAuthnSignerFactoryContract({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be retrieved from the safeProvider right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, but I updated this code to use the createSafeProvider
util function to wrap all the logic about to create the safeProvider
object to reuse it in all packages.
@@ -40,7 +41,7 @@ type SafeConfigWithPredictedSafeProps = { | |||
|
|||
export type SafeConfigProps = { | |||
provider: SafeProviderConfig['provider'] | |||
signer?: SafeProviderConfig['signer'] | |||
signer?: string | passkeyArgType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change? SafeProviderConfig signer should be the correct one right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true! I restored it in the other PR: https://github.com/safe-global/safe-core-sdk/pull/841/files#diff-d7c347ab5fed07174eb38d1a9731c90191e250ca1afabe36c35924940823a8cbR44
constructor( | ||
passkeyRawId: ArrayBuffer, | ||
coordinates: PasskeyCoordinates, | ||
safeWebAuthnSignerFactoryContract: SafeWebAuthnSignerFactoryContractImplementationType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about using the safeProvider as a param and retrieve the contract and external provider from there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the best option is to avoid this, because the user can use the custom contracts and we have all this logic in the Safe.init()
function.
So to avoid to include this customContracts
logic inside of the SafeProvider, I think that we should keep this:
const safeWebAuthnSignerFactoryContract = await getSafeWebAuthnSignerFactoryContract({
safeProvider,
safeVersion: '1.4.1',
customContracts
})
const passkeySigner = await PasskeySigner.init(
signer,
safeWebAuthnSignerFactoryContract,
safeProvider.getExternalProvider()
)
new SafeProvider({
provider,
signer: passkeySigner
})
...t/src/contracts/SafeWebAuthnSignerFactory/v1.4.1/SafeWebAuthnSignerFactoryContract_v1_4_1.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need an utility (buildPasskey) to build the object used with navigator.credentials.create
{
pubKeyCredParams: [
{
// ECDSA w/ SHA-256: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1
alg: -7,
type: 'public-key',
},
],
challenge: crypto.getRandomValues(new Uint8Array(32)),
rp: {
name: 'Safe Wallet',
},
user: {
displayName: 'Safe Owner',
id: crypto.getRandomValues(new Uint8Array(32)),
name: 'safe-owner',
},
timeout: 60000,
attestation: 'none',
}
packages/protocol-kit/src/Safe.ts
Outdated
* @returns TRUE if the account is an owner | ||
*/ | ||
async isOwner(owner: string | passkeyArgType): Promise<boolean> { | ||
const isOwnerAddress = typeof owner === 'string' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The this.#safeProvider
has an util to detect if this is an address
. Consider to just query it. I dont think it will match eip3770 though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case we need to know if the owner
param is a string
or a passkeyArgType
not an address
.
packages/protocol-kit/src/Safe.ts
Outdated
|
||
const passkeySigner = await PasskeySigner.init(owner, webAuthnSignerFactoryContract, provider) | ||
|
||
const ownerAddress = await passkeySigner.getAddress() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be wrong but isnt all this chunk just the this.#safeProvider.getSignerAddress()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the this.#safeProvider
might have been initialized with an EOA instead of a passkey. We need to ensure that the owner's address is created from the passkey provided by the user in the isOwner
function parameter, not from the one in the this.#safeProvider
.
packages/protocol-kit/src/Safe.ts
Outdated
provider, | ||
signer | ||
}) | ||
const isPasskeySigner = signer && typeof signer !== 'string' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disclaimer: The comment has nothing to do with this line but wasnt the protocol kit supposed to have a getPasskeyCreationOptions
or something like that for the people to have a schema on how to create a "safe-compliant" passkey or something?
@@ -62,7 +63,38 @@ class SafeFactory { | |||
}: SafeFactoryInitConfig) { | |||
this.#provider = provider | |||
this.#signer = signer | |||
this.#safeProvider = new SafeProvider({ provider, signer }) | |||
const isPasskeySigner = signer && typeof signer !== 'string' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider on extracting this to a local util since its reused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done! I addressed this comment in this other PR. I created a util in called createSafeProvider
to reuse all the logic behind this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After your suggestion in this comment, I create an static async init
method in the SafeProvider
instead of the createSafeProvider
|
||
export type SafeProviderConfig = { | ||
/** signerOrProvider - Ethers signer or provider */ | ||
provider: Eip1193Provider | HttpTransport | SocketTransport | ||
signer?: HexAddress | PrivateKey | ||
signer?: HexAddress | PrivateKey | passkeyArgType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isnt this the SafeSigner
above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope, in the SafeSigner
the PasskeySigner
is a different type
return this.#externalProvider.getSigner(this.signer) | ||
if (typeof this.signer === 'string') { | ||
// If the signer is not an Ethereum address, it should be a private key | ||
if (this.signer && !ethers.isAddress(this.signer)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if it reached this point, we can assume that this.signer
is defined. Please, remove the left-hand side conditionhere and below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
|
||
super(chainId, safeProvider, defaultAbi, safeVersion, customContractAddress, customContractAbi) | ||
|
||
this.safeVersion = safeVersion |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason why its not just this.safeVersion = '1.4.1'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is because super(...)
call must be the first statement in the constructor. after super(...)
call we can set the this.safeVersion
value
} | ||
})) as PublicKeyCredential & { response: AuthenticatorAssertionResponse } | ||
|
||
if (!assertion || !assertion?.response?.authenticatorData) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think you need the first verification since you are using safe navigation/elvis operator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
publicKey: { | ||
challenge: data, | ||
allowCredentials: [{ type: 'public-key', id: this.passkeyRawId }], | ||
userVerification: 'required' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine this would throw that weird prompt in the user's face and that is not something that the Dapp necessarily opted-in (since its not an option here). It should probably be added to the js-docs.
} | ||
|
||
signTypedData(): Promise<string> { | ||
throw new Error('Passkey Signers cannot sign signTypedData, they can only sign data.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider on changing the isTypedDataSigner
as well (although that will probably change the error).
* Fix existing unit tests * Test case for initializing Safe4337Pack with a passkey signer * Tests for signing a SafeOperation with passkey + sending to bundler * Extend webauthnShim to initialize with a static private key * Passkey private key must be specified via env variable * Move logic to create a mocked passkey object to utils folder in protocol-kit * Fix passkey tests in protocol-kit and use `createMockPasskey` function from utils * Remove `.only` from test * Add comment * Add env variable `PASSKEY_PRIVATE_KEY` * Fix `createMockPasskey` function JSDoc * Allow to create mocked passkeys with a static private key * Fix test for latest changes Passkey signer is not being added to the owners of a 4337 Safe, but only the SafeWebAuthnSharedSigner address. * Fix tests * Remove duplicated file for passkey test utils * Remove unnecessary `await` * Fix account-abstraction-kit test --------- Co-authored-by: Daniel <[email protected]>
Pull Request Test Coverage Report for Build 9545149597Details
💛 - Coveralls |
* Added SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS as parameter in the addDummySignature
Pull Request Test Coverage Report for Build 9582107333Details
💛 - Coveralls |
* Add createSwapOwnerTx passkey tests * Add remove owner tests
* add SafeWebAuthnSharedSigner Contract * added passkey shared signer support to signHash * Added unit tests
Pull Request Test Coverage Report for Build 10524084006Details
💛 - Coveralls |
|
||
export type SafeProviderConfig = { | ||
/** signerOrProvider - Ethers signer or provider */ | ||
provider: Eip1193Provider | HttpTransport | SocketTransport | ||
signer?: HexAddress | PrivateKey | ||
signer?: HexAddress | PrivateKey | PasskeySigner | PasskeyArgType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confused here with the added PasskeySigner
. This is to instantiate the SafeProvider with some signer from outside as an address, a privateKey or a passkey args object. Can this be instantiate with the PasskeySigner class?
In any case this should be added as well to the SafeSigner type above
|
||
constructor({ provider, signer }: SafeProviderConfig) { | ||
provider: SafeProviderConfig['provider'] | ||
signer?: SafeSigner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use the SafeProviderConfig['signer'] in all places and remove the SafeSigner type or add a new SafeEthereumProvider (for example) instead, so we would use:
provider: SafeProviderConfig['provider'],
signer: SafeProviderConfig['signer']
or
provider: SafeEthereumProvider,
signer: SafeSigner
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only to make it cleaner as this two props are used in multiple places
* refactor in Safe.ts to remove passkey shared signer logic * Add getPasskeyOwnerAddress as util fn * add createPasskeyDeploymentTransaction as a util function * fix: use safeProvider instance * fix isOwner test for v1.0.0 * add getPasskeyOwnerAddress tests * Add dinamic addresses to the getPasskeyOwnerAddress tests
…safe-deployments type
@@ -0,0 +1,50 @@ | |||
import Safe from '../../Safe' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are missing many global routes @safe-global/protocol-kit/Safe
* use contract addresses from safe-modules-deployments * upated safe passkey address in fixtures
What it solves
Resolves #789 & #793 by adding support for passkeys in the
protocol-kit
.How this PR fixes it
protocol-kit
instantiation using passkeysYou can create an instance of the
protocol-kit
using a passkey as a Signer. The passkey Signer is a contract compliant with EIP-1271 standards.the passkey argument is an object with 2 keys:
rawId
: The raw identifier of the passkey. SeerawId
docspublicKey
: The passkey’s public key, generated via the getPublicKey() method, necessary for generating X & Y coordinates. SeegetPublicKey()
method docsAdd a passkey owner
Now you can use the
createAddOwnerTx
method present in the protocol-kit using a passkey:This function returns the Safe transaction to add a passkey as owner and optionally change the threshold. If the passkey Signer contract has not been deployed, this function creates a batch that includes both the Signer contract deployment and the addOwner transaction.
isOwner
functionThe isOwner function now checks if a specific address or passkey is an owner of the current Safe:
Sign a transaction using passkeys
Added the WebAuthn Signer Factory Contract
This
SafeWebAuthnSignerFactory
contract is used to create a Signer and to get the address of a Signer.