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

Handle grantSession keys as available parameter #11

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,10 @@ Grants a session on the account.
expiry?: number

// The keys to grant on the session.
// Defaults to a Web Crypto P256 key.
keys?: {
algorithm: 'p256' | 'secp256k1',
publicKey: `0x${string}`,
type: 'p256' | 'secp256k1' | 'webauthn',
}[]
}]
}
Expand Down
6 changes: 5 additions & 1 deletion contracts/src/account/ExperimentalDelegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ contract ExperimentalDelegation is Receiver, MultiSendCallOnly {
/// @notice The type of key.
enum KeyType {
P256,
WebAuthnP256
WebAuthnP256,
Secp256k1
}

/// @notice A Key that can be used to authorize calls.
Expand Down Expand Up @@ -206,6 +207,9 @@ contract ExperimentalDelegation is Receiver, MultiSendCallOnly {
WebAuthnP256.Metadata memory metadata = abi.decode(wrappedSignature.metadata, (WebAuthnP256.Metadata));
if (WebAuthnP256.verify(digest, metadata, wrappedSignature.signature, key.publicKey)) return success;
}
if (key.keyType == KeyType.Secp256k1 && ECDSA.verify(digest, wrappedSignature.signature, key.publicKey)) {
return success;
}
}

// If the signature is not valid, return the failure magic value.
Expand Down
25 changes: 25 additions & 0 deletions contracts/src/utils/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,29 @@ library ECDSA {
uint256 s;
uint8 yParity;
}

/// @notice Verifies an ECDSA signature.
/// @param digest 32 bytes of the signed data hash
/// @param signature Signature of the signer
/// @param publicKey Public key of the signer
/// @return success Represents if the operation was successful
function verify(bytes32 digest, Signature memory signature, PublicKey memory publicKey)
internal
pure
returns (bool)
{
uint8 v = signature.yParity + 27;
bytes32 r = bytes32(signature.r);
bytes32 s = bytes32(signature.s);

address signer = ecrecover(digest, v, r, s);

if (signer == address(0)) return false;

bytes memory publicKeyBytes = abi.encodePacked(publicKey.x, publicKey.y);

bytes32 publicKeyHash = keccak256(publicKeyBytes);

return signer == address(uint160(uint256(publicKeyHash)));
}
}
48 changes: 48 additions & 0 deletions contracts/test/account/ExperimentalDelegation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ contract ExperimentalDelegationTest is Test {
assertEq(expiry, 0);
}

function test_authorize_withSecp256k1Key() public {
vm.pauseGasMetering();

VmSafe.Wallet memory wallet = vm.createWallet("wallet");

ExperimentalDelegation.Key[] memory keys = new ExperimentalDelegation.Key[](1);
keys[0] = ExperimentalDelegation.Key(
0, ExperimentalDelegation.KeyType.Secp256k1, ECDSA.PublicKey(wallet.publicKeyX, wallet.publicKeyY)
);

vm.expectRevert();
delegation.keys(0);

vm.prank(address(delegation));
vm.resumeGasMetering();
delegation.authorize(keys);
vm.pauseGasMetering();

(uint256 expiry, ExperimentalDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) =
delegation.keys(0);
assertEq(authorizedPublicKey.x, wallet.publicKeyX);
assertEq(authorizedPublicKey.y, wallet.publicKeyY);
assertEq(expiry, 0);
}

function test_authorize_withAuthorizedKey() public {
vm.pauseGasMetering();

Expand Down Expand Up @@ -322,4 +347,27 @@ contract ExperimentalDelegationTest is Test {
bytes4(keccak256("isValidSignature(bytes32,bytes)"))
);
}

function test_isValidSignature_forAuthorizingSecp256k1Key() public {
bytes32 hash = keccak256(abi.encodePacked(delegation.nonce(), keccak256("0xdeadbeef")));
VmSafe.Wallet memory wallet = vm.createWallet("wallet");

ExperimentalDelegation.Key[] memory keys = new ExperimentalDelegation.Key[](1);
keys[0] = ExperimentalDelegation.Key(
0, ExperimentalDelegation.KeyType.Secp256k1, ECDSA.PublicKey(wallet.publicKeyX, wallet.publicKeyY)
);

vm.prank(address(delegation));
delegation.authorize(keys);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, hash);
ExperimentalDelegation.WrappedSignature memory wrappedSignature = ExperimentalDelegation.WrappedSignature(
0, ECDSA.Signature(uint256(r), uint256(s), v == 27 ? 0 : 1), false, "0x"
);

assertEq(
delegation.isValidSignature(hash, abi.encode(wrappedSignature)),
bytes4(keccak256("isValidSignature(bytes32,bytes)"))
);
}
}
12 changes: 11 additions & 1 deletion src/internal/accountDelegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ export type Calls = readonly {
data?: Hex.Hex | undefined
}[]

export type Key = WebAuthnKey | WebCryptoKey
export type Key =
| WebAuthnKey
| WebCryptoKey
| Secp256k1Key
| BaseKey<'unknown', unknown>

export type SerializedKey = {
expiry: bigint
Expand All @@ -66,18 +70,24 @@ export type WebCryptoKey = BaseKey<
}
>

export type Secp256k1Key = BaseKey<'secp256k1', {}>

////////////////////////////////////////////////////////////////
// Constants
////////////////////////////////////////////////////////////////

const keyType = {
unknown: -1,
p256: 0,
webauthn: 1,
secp256k1: 2,
} as const
export type KeyType = keyof typeof keyType

const keyTypeSerialized = {
0: 'p256',
1: 'webauthn',
2: 'secp256k1',
} as const

////////////////////////////////////////////////////////////
Expand Down
35 changes: 27 additions & 8 deletions src/internal/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ export function from<
const [
{
address,
expiry = Math.floor(Date.now() / 1_000) + 60 * 60, // 1 hour
expiry = Math.floor(Date.now() / 1_000) + 60 * 60, // 1 hour,
keys,
},
] = (params as RpcSchema.ExtractParams<
Schema.Schema,
Expand All @@ -250,14 +251,28 @@ export function from<
: state.accounts[0]
if (!account) throw new ox_Provider.UnauthorizedError()

const key = await AccountDelegation.createWebCryptoKey({
expiry: BigInt(expiry),
})
let keysToAuthorize: AccountDelegation.Key[] = []
if (keys) {
keysToAuthorize = keys.map(
(key) =>
({
expiry: BigInt(expiry),
publicKey: PublicKey.fromHex(key.publicKey),
status: 'unlocked',
type: key.type as 'unknown',
}) as const,
)
} else {
const key = await AccountDelegation.createWebCryptoKey({
expiry: BigInt(expiry),
})
keysToAuthorize.push(key)
}

// TODO: wait for tx to be included?
await AccountDelegation.authorize(state.client, {
account,
keys: [key],
keys: keysToAuthorize,
rpId: keystoreHost,
})

Expand All @@ -270,20 +285,24 @@ export function from<
...x,
accounts: x.accounts.map((account, i) =>
i === index
? { ...account, keys: [...account.keys, key] }
? { ...account, keys: [...account.keys, ...keysToAuthorize] }
: account,
),
}
})

emitter.emit('message', {
data: getActiveSessionKeys([...account.keys, key]),
data: getActiveSessionKeys([...account.keys, ...keysToAuthorize]),
type: 'sessionsChanged',
})

const id = Hex.concat(
...keysToAuthorize.map((key) => PublicKey.toHex(key.publicKey)),
)

return {
expiry,
id: PublicKey.toHex(key.publicKey),
id,
} satisfies RpcSchema.ExtractReturnType<
Schema.Schema,
'experimental_grantSession'
Expand Down
2 changes: 1 addition & 1 deletion src/internal/rpcSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ export type GrantSessionParameters = {
expiry?: number | undefined
keys?:
| readonly {
algorithm: 'p256' | 'secp256k1'
publicKey: Hex.Hex
type: AccountDelegation.KeyType
}[]
| undefined
}
Expand Down
11 changes: 9 additions & 2 deletions src/internal/wagmi/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
disconnect as wagmi_disconnect,
} from 'wagmi/actions'

import type * as AccountDelegation from '../accountDelegation.js'
import type {
CreateAccountParameters,
GrantSessionParameters,
Expand Down Expand Up @@ -252,7 +253,7 @@ export async function grantSession<config extends Config>(
config: config,
parameters: grantSession.Parameters<config>,
): Promise<grantSession.ReturnType> {
const { address, chainId, connector, expiry } = parameters
const { address, chainId, connector, expiry, keys } = parameters

const client = await getConnectorClient(config, {
account: address,
Expand All @@ -268,7 +269,7 @@ export async function grantSession<config extends Config>(
ReturnType: RpcSchema.ExtractReturnType<Schema, method>
}>({
method,
params: [{ address, expiry }],
params: [{ address, expiry, keys }],
})
}

Expand All @@ -277,6 +278,12 @@ export declare namespace grantSession {
ConnectorParameter & {
address?: Address | undefined
expiry?: number | undefined
keys?:
| {
publicKey: Hex
type: AccountDelegation.KeyType
}[]
| undefined
}

type ReturnType = {
Expand Down