Skip to content

Commit 2f40169

Browse files
wilwadearamikm
andauthored
Feature Passkey Proxy v2, Simple Param Shift (#2242)
# 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 <aramikm@gmail.com>
1 parent 407b520 commit 2f40169

File tree

11 files changed

+917
-31
lines changed

11 files changed

+917
-31
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import '@frequency-chain/api-augment';
2+
import assert from 'assert';
3+
import {
4+
createAndFundKeypair,
5+
EcdsaSignature,
6+
getBlockNumber,
7+
getNonce,
8+
Sr25519Signature,
9+
} from '../scaffolding/helpers';
10+
import { KeyringPair } from '@polkadot/keyring/types';
11+
import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
12+
import { getFundingSource } from '../scaffolding/funding';
13+
import { getUnifiedPublicKey, getUnifiedAddress } from '../scaffolding/ethereum';
14+
import { createPassKeyAndSignAccount, createPassKeyCallV2, createPasskeyPayloadV2 } from '../scaffolding/P256';
15+
import { u8aToHex, u8aWrapBytes } from '@polkadot/util';
16+
const fundingSource = getFundingSource(import.meta.url);
17+
18+
describe('Passkey Pallet Proxy V2 Ethereum Tests', function () {
19+
describe('passkey ethereum tests', function () {
20+
let fundedSr25519Keys: KeyringPair;
21+
let fundedEthereumKeys: KeyringPair;
22+
let receiverKeys: KeyringPair;
23+
24+
before(async function () {
25+
fundedSr25519Keys = await createAndFundKeypair(fundingSource, 300_000_000n);
26+
fundedEthereumKeys = await createAndFundKeypair(fundingSource, 300_000_000n, undefined, undefined, 'ethereum');
27+
receiverKeys = await createAndFundKeypair(fundingSource);
28+
});
29+
30+
it('should transfer via passkeys with root sr25519 key into an ethereum style account', async function () {
31+
const accountPKey = getUnifiedPublicKey(fundedSr25519Keys);
32+
const nonce = await getNonce(fundedSr25519Keys);
33+
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(
34+
getUnifiedAddress(receiverKeys),
35+
55_000_000n
36+
);
37+
const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey);
38+
const accountSignature = fundedSr25519Keys.sign(u8aWrapBytes(passKeyPublicKey));
39+
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
40+
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
41+
const passkeyPayload = await createPasskeyPayloadV2(
42+
multiSignature,
43+
passKeyPrivateKey,
44+
passKeyPublicKey,
45+
passkeyCall,
46+
false
47+
);
48+
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedSr25519Keys, passkeyPayload);
49+
await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource));
50+
await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2);
51+
// adding some delay before fetching the nonce to ensure it is updated
52+
await new Promise((resolve) => setTimeout(resolve, 1000));
53+
const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedSr25519Keys)).nonce.toNumber();
54+
assert.equal(nonce + 1, nonceAfter);
55+
});
56+
57+
it('should transfer via passkeys with root ethereum style key into another one', async function () {
58+
const accountPKey = getUnifiedPublicKey(fundedEthereumKeys);
59+
console.log(`accountPKey ${u8aToHex(accountPKey)}`);
60+
const nonce = await getNonce(fundedEthereumKeys);
61+
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(
62+
getUnifiedAddress(receiverKeys),
63+
66_000_000n
64+
);
65+
const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey);
66+
// ethereum keys should not have wrapping
67+
const accountSignature = fundedEthereumKeys.sign(passKeyPublicKey);
68+
console.log(`accountSignature ${u8aToHex(accountSignature)}`);
69+
const multiSignature: EcdsaSignature = { Ecdsa: u8aToHex(accountSignature) };
70+
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
71+
const passkeyPayload = await createPasskeyPayloadV2(
72+
multiSignature,
73+
passKeyPrivateKey,
74+
passKeyPublicKey,
75+
passkeyCall,
76+
false
77+
);
78+
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundingSource, passkeyPayload);
79+
await assert.doesNotReject(passkeyProxy.sendUnsigned());
80+
await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2);
81+
// adding some delay before fetching the nonce to ensure it is updated
82+
await new Promise((resolve) => setTimeout(resolve, 1000));
83+
const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedEthereumKeys)).nonce.toNumber();
84+
assert.equal(nonce + 1, nonceAfter);
85+
});
86+
});
87+
});

e2e/passkey/passkeyProxyV2.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import '@frequency-chain/api-augment';
2+
import assert from 'assert';
3+
import { createAndFundKeypair, getBlockNumber, getNonce, Sr25519Signature } from '../scaffolding/helpers';
4+
import { KeyringPair } from '@polkadot/keyring/types';
5+
import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
6+
import { getFundingSource } from '../scaffolding/funding';
7+
import { u8aToHex, u8aWrapBytes } from '@polkadot/util';
8+
import { createPassKeyAndSignAccount, createPassKeyCallV2, createPasskeyPayloadV2 } from '../scaffolding/P256';
9+
import { getUnifiedPublicKey } from '../scaffolding/ethereum';
10+
const fundingSource = getFundingSource(import.meta.url);
11+
12+
describe('Passkey Pallet Proxy V2 Tests', function () {
13+
describe('proxy basic tests', function () {
14+
let fundedKeys: KeyringPair;
15+
let receiverKeys: KeyringPair;
16+
17+
before(async function () {
18+
fundedKeys = await createAndFundKeypair(fundingSource, 300_000_000n);
19+
receiverKeys = await createAndFundKeypair(fundingSource);
20+
});
21+
22+
it('should fail due to unsupported call', async function () {
23+
const accountPKey = getUnifiedPublicKey(fundedKeys);
24+
const nonce = await getNonce(fundedKeys);
25+
26+
const remarksCalls = ExtrinsicHelper.api.tx.system.remark('passkey-test');
27+
const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey);
28+
const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey));
29+
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
30+
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, remarksCalls);
31+
const passkeyPayload = await createPasskeyPayloadV2(
32+
multiSignature,
33+
passKeyPrivateKey,
34+
passKeyPublicKey,
35+
passkeyCall,
36+
false
37+
);
38+
39+
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
40+
await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource));
41+
});
42+
43+
it('should fail to transfer balance due to bad account ownership proof', async function () {
44+
const accountPKey = getUnifiedPublicKey(fundedKeys);
45+
const nonce = await getNonce(fundedKeys);
46+
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n);
47+
const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey);
48+
const accountSignature = fundedKeys.sign('badPasskeyPublicKey');
49+
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
50+
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
51+
const passkeyPayload = await createPasskeyPayloadV2(
52+
multiSignature,
53+
passKeyPrivateKey,
54+
passKeyPublicKey,
55+
passkeyCall,
56+
false
57+
);
58+
59+
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
60+
await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource));
61+
});
62+
63+
it('should fail to transfer balance due to bad passkey signature', async function () {
64+
const accountPKey = getUnifiedPublicKey(fundedKeys);
65+
const nonce = await getNonce(fundedKeys);
66+
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n);
67+
const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey);
68+
const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey));
69+
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
70+
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
71+
const passkeyPayload = await createPasskeyPayloadV2(
72+
multiSignature,
73+
passKeyPrivateKey,
74+
passKeyPublicKey,
75+
passkeyCall,
76+
true
77+
);
78+
79+
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
80+
await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource));
81+
});
82+
83+
it('should transfer small balance from fundedKeys to receiverKeys', async function () {
84+
const accountPKey = getUnifiedPublicKey(fundedKeys);
85+
const nonce = await getNonce(fundedKeys);
86+
const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(
87+
getUnifiedPublicKey(receiverKeys),
88+
100_000_000n
89+
);
90+
const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey);
91+
const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey));
92+
const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) };
93+
const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls);
94+
const passkeyPayload = await createPasskeyPayloadV2(
95+
multiSignature,
96+
passKeyPrivateKey,
97+
passKeyPublicKey,
98+
passkeyCall,
99+
false
100+
);
101+
const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload);
102+
await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource));
103+
await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2);
104+
const receiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys);
105+
// adding some delay before fetching the nonce to ensure it is updated
106+
await new Promise((resolve) => setTimeout(resolve, 2000));
107+
const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedKeys)).nonce.toNumber();
108+
assert.equal(nonce + 1, nonceAfter);
109+
assert(receiverBalance.data.free.toBigInt() > 0n);
110+
});
111+
});
112+
});

e2e/scaffolding/P256.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ export async function createPassKeyCall(
3030
return passkeyCall;
3131
}
3232

33+
export async function createPassKeyCallV2(
34+
accountPKey: Uint8Array,
35+
nonce: number,
36+
call: SubmittableExtrinsic<'rxjs', ISubmittableResult>
37+
) {
38+
const ext_call_type = ExtrinsicHelper.api.registry.createType('Call', call);
39+
const passkeyCall = {
40+
accountId: accountPKey,
41+
accountNonce: nonce,
42+
call: ext_call_type,
43+
};
44+
45+
return passkeyCall;
46+
}
47+
3348
export async function createPasskeyPayload(
3449
passKeyPrivateKey: Uint8Array,
3550
passKeyPublicKey: Uint8Array,
@@ -72,3 +87,48 @@ export async function createPasskeyPayload(
7287

7388
return payload;
7489
}
90+
91+
export async function createPasskeyPayloadV2(
92+
accountSignature: MultiSignatureType,
93+
passKeyPrivateKey: Uint8Array,
94+
passKeyPublicKey: Uint8Array,
95+
passkeyCallPayload: any = {},
96+
set_invalid_passkey_data: boolean = false
97+
) {
98+
const authenticatorDataRaw = 'WJ8JTNbivTWn-433ubs148A7EgWowi4SAcYBjLWfo1EdAAAAAA';
99+
const replacedClientDataRaw =
100+
'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiI3JwbGMjIiwib3JpZ2luIjoiaHR0cHM6Ly9wYXNza2V5LmFtcGxpY2EuaW86ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZSwiYWxnIjoiSFMyNTYifQ';
101+
const challengeReplacer = '#rplc#';
102+
let clientData = base64UrlToUint8Array(replacedClientDataRaw);
103+
let authenticatorData = base64UrlToUint8Array(authenticatorDataRaw);
104+
105+
if (set_invalid_passkey_data) {
106+
authenticatorData = new Uint8Array(0);
107+
clientData = new Uint8Array(0);
108+
}
109+
const passkeyCallType = ExtrinsicHelper.api.createType('PalletPasskeyPasskeyCallV2', passkeyCallPayload);
110+
111+
// Challenge is sha256(passkeyCallType)
112+
const calculatedChallenge = sha256(passkeyCallType.toU8a());
113+
const calculatedChallengeBase64url = Buffer.from(calculatedChallenge).toString('base64url');
114+
// inject challenge inside clientData
115+
const clientDataJSON = Buffer.from(clientData)
116+
.toString('utf-8')
117+
.replace(challengeReplacer, calculatedChallengeBase64url);
118+
// prepare signing payload which is [authenticator || sha256(client_data_json)]
119+
const passkeySha256 = sha256(new Uint8Array([...authenticatorData, ...sha256(Buffer.from(clientDataJSON))]));
120+
const passKeySignature = secp256r1.sign(passkeySha256, passKeyPrivateKey).toDERRawBytes();
121+
const passkeyPayload = {
122+
passkeyPublicKey: Array.from(passKeyPublicKey),
123+
verifiablePasskeySignature: {
124+
signature: Array.from(passKeySignature),
125+
authenticatorData: Array.from(authenticatorData),
126+
clientDataJson: Array.from(Buffer.from(clientDataJSON)),
127+
},
128+
accountOwnershipProof: accountSignature,
129+
passkeyCall: passkeyCallType,
130+
};
131+
const payload = ExtrinsicHelper.api.createType('PalletPasskeyPasskeyPayloadV2', passkeyPayload);
132+
133+
return payload;
134+
}

e2e/scaffolding/extrinsicHelpers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,4 +984,12 @@ export class ExtrinsicHelper {
984984
ExtrinsicHelper.api.events.passkey.TransactionExecutionSuccess
985985
);
986986
}
987+
988+
public static executePassKeyProxyV2(keys: KeyringPair, payload: any) {
989+
return new Extrinsic(
990+
() => ExtrinsicHelper.api.tx.passkey.proxyV2(payload),
991+
keys,
992+
ExtrinsicHelper.api.events.passkey.TransactionExecutionSuccess
993+
);
994+
}
987995
}

pallets/passkey/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The Passkey pallet provides for:
1818
## Interactions
1919

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

2829
### Extrinsics
2930

30-
| Name/Description | Caller | Payment | Key Events | Runtime Added |
31-
|----------------------------------------|--------| ------------------ |-------------------------------------------------------------------------------------------------------------------------------------------|---------------|
32-
| `proxy`<br />Proxies an extrinsic call | Anyone | Tokens | [`TransactionExecutionSuccess`](https://frequency-chain.github.io/frequency/pallet_passkey/module/enum.Event.html#variant.TransactionExecutionSuccess) | 92 |
31+
| Name/Description | Caller | Payment | Key Events | Runtime Added |
32+
| ------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
33+
| `proxy`<br />Proxies an extrinsic call | Anyone | Tokens | [`TransactionExecutionSuccess`](https://frequency-chain.github.io/frequency/pallet_passkey/module/enum.Event.html#variant.TransactionExecutionSuccess) | 92 |
34+
| ------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
35+
| `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 |
3336

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

pallets/passkey/src/benchmarking.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ mod app_sr25519 {
2828

2929
type SignerId = app_sr25519::Public;
3030

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

50-
let call: PasskeyCall<T> = PasskeyCall {
50+
let call: PasskeyCallV2<T> = PasskeyCallV2 {
5151
account_id: test_account_1_account_id,
5252
account_nonce: T::Nonce::zero(),
53-
account_ownership_proof: signature,
5453
call: Box::new(inner_call),
5554
};
5655

5756
let passkey_signature =
5857
passkey_sign(&secret, &call.encode(), &client_data, &authenticator).unwrap();
59-
let payload = PasskeyPayload {
58+
let payload = PasskeyPayloadV2 {
6059
passkey_public_key,
6160
verifiable_passkey_signature: VerifiablePasskeySignature {
6261
signature: passkey_signature,
6362
client_data_json: client_data.try_into().unwrap(),
6463
authenticator_data: authenticator.try_into().unwrap(),
6564
},
65+
account_ownership_proof: signature,
6666
passkey_call: call,
6767
};
6868
payload
@@ -77,13 +77,13 @@ benchmarks! {
7777
validate {
7878
let payload = generate_payload::<T>();
7979
}: {
80-
assert_ok!(Passkey::validate_unsigned(TransactionSource::InBlock, &Call::proxy { payload }));
80+
assert_ok!(Passkey::validate_unsigned(TransactionSource::InBlock, &Call::proxy_v2 { payload }));
8181
}
8282

8383
pre_dispatch {
8484
let payload = generate_payload::<T>();
8585
}: {
86-
assert_ok!(Passkey::pre_dispatch(&Call::proxy { payload }));
86+
assert_ok!(Passkey::pre_dispatch(&Call::proxy_v2 { payload }));
8787
}
8888

8989
impl_benchmark_test_suite!(

0 commit comments

Comments
 (0)