Skip to content

Commit

Permalink
Feature Passkey Proxy v2, Simple Param Shift (#2242)
Browse files Browse the repository at this point in the history
# Goal
The goal of this PR is to shift the `accountOwnershipProof` out of the
signature payload. For reasons why, see #2241

with @aramikm 


Closes #2241 

# Discussion

- Added `proxy_v2`
- Deprecated `proxy`
- Duplicated tests, as eventually we'll remove v1

---------

Co-authored-by: Aramik <[email protected]>
  • Loading branch information
wilwade and aramikm authored Dec 16, 2024
1 parent 407b520 commit 2f40169
Show file tree
Hide file tree
Showing 11 changed files with 917 additions and 31 deletions.
87 changes: 87 additions & 0 deletions e2e/passkey/passkeyProxyV2.ethereum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import '@frequency-chain/api-augment';
import assert from 'assert';
import {
createAndFundKeypair,
EcdsaSignature,
getBlockNumber,
getNonce,
Sr25519Signature,
} from '../scaffolding/helpers';
import { KeyringPair } from '@polkadot/keyring/types';
import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
import { getFundingSource } from '../scaffolding/funding';
import { getUnifiedPublicKey, getUnifiedAddress } from '../scaffolding/ethereum';
import { createPassKeyAndSignAccount, createPassKeyCallV2, createPasskeyPayloadV2 } from '../scaffolding/P256';
import { u8aToHex, u8aWrapBytes } from '@polkadot/util';
const fundingSource = getFundingSource(import.meta.url);

describe('Passkey Pallet Proxy V2 Ethereum Tests', function () {
describe('passkey ethereum tests', function () {
let fundedSr25519Keys: KeyringPair;
let fundedEthereumKeys: KeyringPair;
let receiverKeys: KeyringPair;

before(async function () {
fundedSr25519Keys = await createAndFundKeypair(fundingSource, 300_000_000n);
fundedEthereumKeys = await createAndFundKeypair(fundingSource, 300_000_000n, undefined, undefined, 'ethereum');
receiverKeys = await createAndFundKeypair(fundingSource);
});

it('should transfer via passkeys with root sr25519 key into an ethereum style account', async function () {
const accountPKey = getUnifiedPublicKey(fundedSr25519Keys);
const nonce = await getNonce(fundedSr25519Keys);
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(
getUnifiedAddress(receiverKeys),
55_000_000n
);
const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey);
const accountSignature = fundedSr25519Keys.sign(u8aWrapBytes(passKeyPublicKey));
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
const passkeyPayload = await createPasskeyPayloadV2(
multiSignature,
passKeyPrivateKey,
passKeyPublicKey,
passkeyCall,
false
);
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedSr25519Keys, passkeyPayload);
await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource));
await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2);
// adding some delay before fetching the nonce to ensure it is updated
await new Promise((resolve) => setTimeout(resolve, 1000));
const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedSr25519Keys)).nonce.toNumber();
assert.equal(nonce + 1, nonceAfter);
});

it('should transfer via passkeys with root ethereum style key into another one', async function () {
const accountPKey = getUnifiedPublicKey(fundedEthereumKeys);
console.log(`accountPKey ${u8aToHex(accountPKey)}`);
const nonce = await getNonce(fundedEthereumKeys);
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(
getUnifiedAddress(receiverKeys),
66_000_000n
);
const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey);
// ethereum keys should not have wrapping
const accountSignature = fundedEthereumKeys.sign(passKeyPublicKey);
console.log(`accountSignature ${u8aToHex(accountSignature)}`);
const multiSignature: EcdsaSignature = { Ecdsa: u8aToHex(accountSignature) };
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
const passkeyPayload = await createPasskeyPayloadV2(
multiSignature,
passKeyPrivateKey,
passKeyPublicKey,
passkeyCall,
false
);
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundingSource, passkeyPayload);
await assert.doesNotReject(passkeyProxy.sendUnsigned());
await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2);
// adding some delay before fetching the nonce to ensure it is updated
await new Promise((resolve) => setTimeout(resolve, 1000));
const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedEthereumKeys)).nonce.toNumber();
assert.equal(nonce + 1, nonceAfter);
});
});
});
112 changes: 112 additions & 0 deletions e2e/passkey/passkeyProxyV2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import '@frequency-chain/api-augment';
import assert from 'assert';
import { createAndFundKeypair, getBlockNumber, getNonce, Sr25519Signature } from '../scaffolding/helpers';
import { KeyringPair } from '@polkadot/keyring/types';
import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
import { getFundingSource } from '../scaffolding/funding';
import { u8aToHex, u8aWrapBytes } from '@polkadot/util';
import { createPassKeyAndSignAccount, createPassKeyCallV2, createPasskeyPayloadV2 } from '../scaffolding/P256';
import { getUnifiedPublicKey } from '../scaffolding/ethereum';
const fundingSource = getFundingSource(import.meta.url);

describe('Passkey Pallet Proxy V2 Tests', function () {
describe('proxy basic tests', function () {
let fundedKeys: KeyringPair;
let receiverKeys: KeyringPair;

before(async function () {
fundedKeys = await createAndFundKeypair(fundingSource, 300_000_000n);
receiverKeys = await createAndFundKeypair(fundingSource);
});

it('should fail due to unsupported call', async function () {
const accountPKey = getUnifiedPublicKey(fundedKeys);
const nonce = await getNonce(fundedKeys);

const remarksCalls = ExtrinsicHelper.api.tx.system.remark('passkey-test');
const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey);
const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey));
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, remarksCalls);
const passkeyPayload = await createPasskeyPayloadV2(
multiSignature,
passKeyPrivateKey,
passKeyPublicKey,
passkeyCall,
false
);

const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource));
});

it('should fail to transfer balance due to bad account ownership proof', async function () {
const accountPKey = getUnifiedPublicKey(fundedKeys);
const nonce = await getNonce(fundedKeys);
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n);
const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey);
const accountSignature = fundedKeys.sign('badPasskeyPublicKey');
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
const passkeyPayload = await createPasskeyPayloadV2(
multiSignature,
passKeyPrivateKey,
passKeyPublicKey,
passkeyCall,
false
);

const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource));
});

it('should fail to transfer balance due to bad passkey signature', async function () {
const accountPKey = getUnifiedPublicKey(fundedKeys);
const nonce = await getNonce(fundedKeys);
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n);
const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey);
const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey));
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
const passkeyPayload = await createPasskeyPayloadV2(
multiSignature,
passKeyPrivateKey,
passKeyPublicKey,
passkeyCall,
true
);

const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource));
});

it('should transfer small balance from fundedKeys to receiverKeys', async function () {
const accountPKey = getUnifiedPublicKey(fundedKeys);
const nonce = await getNonce(fundedKeys);
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(
getUnifiedPublicKey(receiverKeys),
100_000_000n
);
const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey);
const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey));
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
const passkeyPayload = await createPasskeyPayloadV2(
multiSignature,
passKeyPrivateKey,
passKeyPublicKey,
passkeyCall,
false
);
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource));
await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2);
const receiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys);
// adding some delay before fetching the nonce to ensure it is updated
await new Promise((resolve) => setTimeout(resolve, 2000));
const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedKeys)).nonce.toNumber();
assert.equal(nonce + 1, nonceAfter);
assert(receiverBalance.data.free.toBigInt() > 0n);
});
});
});
60 changes: 60 additions & 0 deletions e2e/scaffolding/P256.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ export async function createPassKeyCall(
return passkeyCall;
}

export async function createPassKeyCallV2(
accountPKey: Uint8Array,
nonce: number,
call: SubmittableExtrinsic<'rxjs', ISubmittableResult>
) {
const ext_call_type = ExtrinsicHelper.api.registry.createType('Call', call);
const passkeyCall = {
accountId: accountPKey,
accountNonce: nonce,
call: ext_call_type,
};

return passkeyCall;
}

export async function createPasskeyPayload(
passKeyPrivateKey: Uint8Array,
passKeyPublicKey: Uint8Array,
Expand Down Expand Up @@ -72,3 +87,48 @@ export async function createPasskeyPayload(

return payload;
}

export async function createPasskeyPayloadV2(
accountSignature: MultiSignatureType,
passKeyPrivateKey: Uint8Array,
passKeyPublicKey: Uint8Array,
passkeyCallPayload: any = {},
set_invalid_passkey_data: boolean = false
) {
const authenticatorDataRaw = 'WJ8JTNbivTWn-433ubs148A7EgWowi4SAcYBjLWfo1EdAAAAAA';
const replacedClientDataRaw =
'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiI3JwbGMjIiwib3JpZ2luIjoiaHR0cHM6Ly9wYXNza2V5LmFtcGxpY2EuaW86ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZSwiYWxnIjoiSFMyNTYifQ';
const challengeReplacer = '#rplc#';
let clientData = base64UrlToUint8Array(replacedClientDataRaw);
let authenticatorData = base64UrlToUint8Array(authenticatorDataRaw);

if (set_invalid_passkey_data) {
authenticatorData = new Uint8Array(0);
clientData = new Uint8Array(0);
}
const passkeyCallType = ExtrinsicHelper.api.createType('PalletPasskeyPasskeyCallV2', passkeyCallPayload);

// Challenge is sha256(passkeyCallType)
const calculatedChallenge = sha256(passkeyCallType.toU8a());
const calculatedChallengeBase64url = Buffer.from(calculatedChallenge).toString('base64url');
// inject challenge inside clientData
const clientDataJSON = Buffer.from(clientData)
.toString('utf-8')
.replace(challengeReplacer, calculatedChallengeBase64url);
// prepare signing payload which is [authenticator || sha256(client_data_json)]
const passkeySha256 = sha256(new Uint8Array([...authenticatorData, ...sha256(Buffer.from(clientDataJSON))]));
const passKeySignature = secp256r1.sign(passkeySha256, passKeyPrivateKey).toDERRawBytes();
const passkeyPayload = {
passkeyPublicKey: Array.from(passKeyPublicKey),
verifiablePasskeySignature: {
signature: Array.from(passKeySignature),
authenticatorData: Array.from(authenticatorData),
clientDataJson: Array.from(Buffer.from(clientDataJSON)),
},
accountOwnershipProof: accountSignature,
passkeyCall: passkeyCallType,
};
const payload = ExtrinsicHelper.api.createType('PalletPasskeyPasskeyPayloadV2', passkeyPayload);

return payload;
}
8 changes: 8 additions & 0 deletions e2e/scaffolding/extrinsicHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -984,4 +984,12 @@ export class ExtrinsicHelper {
ExtrinsicHelper.api.events.passkey.TransactionExecutionSuccess
);
}

public static executePassKeyProxyV2(keys: KeyringPair, payload: any) {
return new Extrinsic(
() => ExtrinsicHelper.api.tx.passkey.proxyV2(payload),
keys,
ExtrinsicHelper.api.events.passkey.TransactionExecutionSuccess
);
}
}
11 changes: 6 additions & 5 deletions pallets/passkey/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The Passkey pallet provides for:
## Interactions

### Extrinsic verification

Because the Polkadot SDK currently lacks support for P256 signatures, we had to use an unsigned
extrinsic to allow this custom verification before dispatching transactions. To achieve this, we
added P256 signature verification within the `ValidateUnsigned` trait implementation for the pallet.
Expand All @@ -27,10 +28,10 @@ checks within the ValidateUnsigned trait implementation to mitigate potential vu

### Extrinsics

| Name/Description | Caller | Payment | Key Events | Runtime Added |
|----------------------------------------|--------| ------------------ |-------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `proxy`<br />Proxies an extrinsic call | Anyone | Tokens | [`TransactionExecutionSuccess`](https://frequency-chain.github.io/frequency/pallet_passkey/module/enum.Event.html#variant.TransactionExecutionSuccess) | 92 |
| Name/Description | Caller | Payment | Key Events | Runtime Added |
| ------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `proxy`<br />Proxies an extrinsic call | Anyone | Tokens | [`TransactionExecutionSuccess`](https://frequency-chain.github.io/frequency/pallet_passkey/module/enum.Event.html#variant.TransactionExecutionSuccess) | 92 |
| ------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `proxy_v2`<br />Proxies an extrinsic call | Anyone | Tokens | [`TransactionExecutionSuccess`](https://frequency-chain.github.io/frequency/pallet_passkey/module/enum.Event.html#variant.TransactionExecutionSuccess) | 92 |

See [Rust Docs](https://frequency-chain.github.io/frequency/pallet_passkey/module/struct.Pallet.html) for more details.


12 changes: 6 additions & 6 deletions pallets/passkey/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ mod app_sr25519 {

type SignerId = app_sr25519::Public;

fn generate_payload<T: Config>() -> PasskeyPayload<T> {
fn generate_payload<T: Config>() -> PasskeyPayloadV2<T> {
let test_account_1_pk = SignerId::generate_pair(None);
let test_account_1_account_id =
T::AccountId::decode(&mut &test_account_1_pk.encode()[..]).unwrap();
Expand All @@ -47,22 +47,22 @@ fn generate_payload<T: Config>() -> PasskeyPayload<T> {
let inner_call: <T as Config>::RuntimeCall =
frame_system::Call::<T>::remark { remark: vec![] }.into();

let call: PasskeyCall<T> = PasskeyCall {
let call: PasskeyCallV2<T> = PasskeyCallV2 {
account_id: test_account_1_account_id,
account_nonce: T::Nonce::zero(),
account_ownership_proof: signature,
call: Box::new(inner_call),
};

let passkey_signature =
passkey_sign(&secret, &call.encode(), &client_data, &authenticator).unwrap();
let payload = PasskeyPayload {
let payload = PasskeyPayloadV2 {
passkey_public_key,
verifiable_passkey_signature: VerifiablePasskeySignature {
signature: passkey_signature,
client_data_json: client_data.try_into().unwrap(),
authenticator_data: authenticator.try_into().unwrap(),
},
account_ownership_proof: signature,
passkey_call: call,
};
payload
Expand All @@ -77,13 +77,13 @@ benchmarks! {
validate {
let payload = generate_payload::<T>();
}: {
assert_ok!(Passkey::validate_unsigned(TransactionSource::InBlock, &Call::proxy { payload }));
assert_ok!(Passkey::validate_unsigned(TransactionSource::InBlock, &Call::proxy_v2 { payload }));
}

pre_dispatch {
let payload = generate_payload::<T>();
}: {
assert_ok!(Passkey::pre_dispatch(&Call::proxy { payload }));
assert_ok!(Passkey::pre_dispatch(&Call::proxy_v2 { payload }));
}

impl_benchmark_test_suite!(
Expand Down
Loading

0 comments on commit 2f40169

Please sign in to comment.