From e8b8e6fba1ff27dd5b11b9196ccc6f85c3723ed4 Mon Sep 17 00:00:00 2001 From: weichweich Date: Mon, 27 Feb 2023 15:46:22 +0100 Subject: [PATCH 1/7] feat: split `associateAccountToChainArgs` --- .../did/src/DidLinks/AccountLinks.chain.ts | 127 +++++++++++++----- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 1b0373e63..22d6c711c 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -16,6 +16,7 @@ import { u8aConcatStrict, u8aToHex, u8aWrapBytes, + BN, } from '@polkadot/util' import { ApiPromise } from '@polkadot/api' @@ -125,27 +126,20 @@ function getUnprefixedSignature( } /** - * Builds the parameters for an extrinsic to link the `account` to the `did` where the fees and deposit are covered by some third account. - * This extrinsic must be authorized using the same full DID. - * Note that in addition to the signing account and DID used here, the submitting account will also be able to dissolve the link via reclaiming its deposit! + * Generates the challenge that links a DID to an account. + * The account has to sign the raw challenge, while the DID will sign the extrinsic that contains the challenge and will + * link the account to the DID. * - * @param accountAddress Address of the account to be linked. - * @param did Full DID to be linked. - * @param sign The sign callback that generates the account signature over the encoded (DidAddress, BlockNumber) tuple. - * @param nBlocksValid The link request will be rejected if submitted later than (current block number + nBlocksValid)? - * @returns An array of parameters for `api.tx.didLookup.associateAccount` that must be DID-authorized by the full DID used. + * @param did The URI of the DID that that should be linked to an account. + * @param validUntil Last blocknumber that this challenge is valid for. + * @returns The encoded challenge. */ -export async function associateAccountToChainArgs( - accountAddress: Address, +export async function getLinkingChallenge( did: DidUri, - sign: (encodedLinkingDetails: HexString) => Promise, - nBlocksValid = 10 -): Promise { + validUntil: BN +): Promise { const api = ConfigService.get('api') - const blockNo = await api.query.system.number() - const validTill = blockNo.addn(nBlocksValid) - // Gets the current definition of BlockNumber (second tx argument) from the metadata. const BlockNumber = api.tx.didLookup.associateAccount.meta.args[1].type.toString() @@ -158,33 +152,37 @@ export async function associateAccountToChainArgs( ).sub as TypeDef[] )[0].type // get the type of the first key, which is the DidAddress - const encoded = api - .createType(`(${DidAddress}, ${BlockNumber})`, [toChain(did), validTill]) + return api + .createType(`(${DidAddress}, ${BlockNumber})`, [toChain(did), validUntil]) .toU8a() +} - const isAccountId32 = decodeAddress(accountAddress).length > 20 - const length = stringToU8a(String(encoded.length)) - const paddedDetails = u8aToHex( - isAccountId32 - ? u8aWrapBytes(encoded) - : u8aConcatStrict([U8A_WRAP_ETHEREUM, length, encoded]) - ) - - const { signature, type } = getUnprefixedSignature( - paddedDetails, - await sign(paddedDetails), - accountAddress - ) +/** + * Generates the arguments for the extrinsic that links an account to a DID. + * + * @param accountAddress Address of the account to be linked. + * @param validUntil Last blocknumber that this challenge is valid for. + * @param signature The signature for the linking challenge. + * @param type The key type used to sign the challenge. + * @returns The arguments for the call that links account and DID. + */ +export async function getLinkingArguments( + accountAddress: Address, + validUntil: BN, + signature: Uint8Array, + type: KeypairType +): Promise { + const api = ConfigService.get('api') const proof = { [type]: signature } as EncodedSignature if (isEthereumEnabled(api)) { if (type === 'ethereum') { - const result = [{ Ethereum: [accountAddress, signature] }, validTill] + const result = [{ Ethereum: [accountAddress, signature] }, validUntil] // Force type cast to enable the old blockchain types to accept the future format return result as unknown as AssociateAccountToChainResult } - const result = [{ Dotsama: [accountAddress, proof] }, validTill] + const result = [{ Dotsama: [accountAddress, proof] }, validUntil] // Force type cast to enable the old blockchain types to accept the future format return result as unknown as AssociateAccountToChainResult } @@ -194,5 +192,66 @@ export async function associateAccountToChainArgs( 'Ethereum linking is not yet supported by this chain' ) - return [accountAddress, validTill, proof] + return [accountAddress, validUntil, proof] +} + +/** + * Wraps the provided challenge according to the key type. + * + * Ethereum addresses will cause the challenge to be prefixed with + * `\x19Ethereum Signed Message:\n` and the length of the message. + * + * For all other key types the message will be wrapped in `..`. + * + * @param type The key type that will sign the challenge. + * @param challenge The challenge to proof ownership of both account and DID. + * @returns The wrapped challenge. + */ +export function getWrappedChallenge( + type: KeypairType, + challenge: Uint8Array +): Uint8Array { + if (type === 'ethereum') { + const length = stringToU8a(String(challenge.length)) + return u8aConcatStrict([U8A_WRAP_ETHEREUM, length, challenge]) + } + return u8aWrapBytes(challenge) +} + +/** + * Builds the parameters for an extrinsic to link the `account` to the `did` where the fees and deposit are covered by some third account. + * This extrinsic must be authorized using the same full DID. + * Note that in addition to the signing account and DID used here, the submitting account will also be able to dissolve the link via reclaiming its deposit! + * + * @param accountAddress Address of the account to be linked. + * @param did Full DID to be linked. + * @param sign The sign callback that generates the account signature over the encoded (DidAddress, BlockNumber) tuple. + * @param nBlocksValid The link request will be rejected if submitted later than (current block number + nBlocksValid)? + * @returns An array of parameters for `api.tx.didLookup.associateAccount` that must be DID-authorized by the full DID used. + */ +export async function associateAccountToChainArgs( + accountAddress: Address, + did: DidUri, + sign: (encodedLinkingDetails: HexString) => Promise, + nBlocksValid = 10 +): Promise { + const api = ConfigService.get('api') + + const blockNo = await api.query.system.number() + const validTill = blockNo.addn(nBlocksValid) + + const challenge = await getLinkingChallenge(did, validTill) + + const predictedType = accountAddress.length === 20 ? 'ethereum' : 'sr25519' + const wrappedChallenge = u8aToHex( + getWrappedChallenge(predictedType, challenge) + ) + + const { signature, type } = getUnprefixedSignature( + wrappedChallenge, + await sign(wrappedChallenge), + accountAddress + ) + + return getLinkingArguments(accountAddress, validTill, signature, type) } From 065949a9cdb11b8825a6467e57fd31d4f3367a6c Mon Sep 17 00:00:00 2001 From: weichweich Date: Tue, 28 Feb 2023 15:22:58 +0100 Subject: [PATCH 2/7] feat: introduce wrapping type --- packages/did/src/DidLinks/AccountLinks.chain.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 22d6c711c..a6f601a41 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -195,6 +195,11 @@ export async function getLinkingArguments( return [accountAddress, validUntil, proof] } +/** + * Identifies the strategy to wrap raw bytes for signing. + */ +export type WrappingStrategy = 'ethereum' | 'polkadot' + /** * Wraps the provided challenge according to the key type. * @@ -208,7 +213,7 @@ export async function getLinkingArguments( * @returns The wrapped challenge. */ export function getWrappedChallenge( - type: KeypairType, + type: WrappingStrategy, challenge: Uint8Array ): Uint8Array { if (type === 'ethereum') { @@ -242,7 +247,7 @@ export async function associateAccountToChainArgs( const challenge = await getLinkingChallenge(did, validTill) - const predictedType = accountAddress.length === 20 ? 'ethereum' : 'sr25519' + const predictedType = accountAddress.length === 20 ? 'ethereum' : 'polkadot' const wrappedChallenge = u8aToHex( getWrappedChallenge(predictedType, challenge) ) From 5745396b2cc47c535ba636b314d117fb53c4595c Mon Sep 17 00:00:00 2001 From: weichweich Date: Wed, 1 Mar 2023 07:40:20 +0100 Subject: [PATCH 3/7] fix: no rust style comments? --- packages/did/src/DidLinks/AccountLinks.chain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index a6f601a41..44b87ddc2 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -27,7 +27,9 @@ import { ConfigService } from '@kiltprotocol/config' import { EncodedSignature } from '../Did.utils.js' import { toChain } from '../Did.chain.js' -/// A chain-agnostic address, which can be encoded using any network prefix. +/** + * A chain-agnostic address, which can be encoded using any network prefix. + */ export type SubstrateAddress = KeyringPair['address'] export type EthereumAddress = HexString From 636cfb7c71350d3010bd361da2649ee7b905e2d8 Mon Sep 17 00:00:00 2001 From: Albrecht Date: Wed, 1 Mar 2023 08:56:18 +0100 Subject: [PATCH 4/7] Update packages/did/src/DidLinks/AccountLinks.chain.ts Co-authored-by: Antonio --- packages/did/src/DidLinks/AccountLinks.chain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 44b87ddc2..6e389b7b0 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -129,7 +129,7 @@ function getUnprefixedSignature( /** * Generates the challenge that links a DID to an account. - * The account has to sign the raw challenge, while the DID will sign the extrinsic that contains the challenge and will + * The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will * link the account to the DID. * * @param did The URI of the DID that that should be linked to an account. From ccfdcb431b2add414bc917270b712e98aca37b98 Mon Sep 17 00:00:00 2001 From: weichweich Date: Thu, 2 Mar 2023 10:03:59 +0100 Subject: [PATCH 5/7] fix: test for correct eth address length --- packages/did/src/DidLinks/AccountLinks.chain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 6e389b7b0..d643042fe 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -249,7 +249,9 @@ export async function associateAccountToChainArgs( const challenge = await getLinkingChallenge(did, validTill) - const predictedType = accountAddress.length === 20 ? 'ethereum' : 'polkadot' + // ethereum addresses are 42 characters long since they are 20 bytes hex encoded strings + // (they start with 0x, 2 characters per byte) + const predictedType = accountAddress.length === 42 ? 'ethereum' : 'polkadot' const wrappedChallenge = u8aToHex( getWrappedChallenge(predictedType, challenge) ) From 38464d87c9d05fdf98f0b3c8b0ea17ff94e34318 Mon Sep 17 00:00:00 2001 From: weichweich Date: Thu, 2 Mar 2023 11:45:41 +0100 Subject: [PATCH 6/7] feat: support v2 challenge --- .../did/src/DidLinks/AccountLinks.chain.ts | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index d643042fe..379b065f3 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -127,16 +127,7 @@ function getUnprefixedSignature( throw new SDKErrors.SignatureUnverifiableError() } -/** - * Generates the challenge that links a DID to an account. - * The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will - * link the account to the DID. - * - * @param did The URI of the DID that that should be linked to an account. - * @param validUntil Last blocknumber that this challenge is valid for. - * @returns The encoded challenge. - */ -export async function getLinkingChallenge( +async function getLinkingChallengeV1( did: DidUri, validUntil: BN ): Promise { @@ -159,6 +150,32 @@ export async function getLinkingChallenge( .toU8a() } +function getLinkingChallengeV2(did: DidUri, validUntil: BN): Uint8Array { + return stringToU8a( + `Publicly link the signing address to ${did} before block number ${validUntil}` + ) +} + +/** + * Generates the challenge that links a DID to an account. + * The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will + * link the account to the DID. + * + * @param did The URI of the DID that that should be linked to an account. + * @param validUntil Last blocknumber that this challenge is valid for. + * @returns The encoded challenge. + */ +export async function getLinkingChallenge( + did: DidUri, + validUntil: BN +): Promise { + const api = ConfigService.get('api') + if (isEthereumEnabled(api)) { + return getLinkingChallengeV2(did, validUntil) + } + return getLinkingChallengeV1(did, validUntil) +} + /** * Generates the arguments for the extrinsic that links an account to a DID. * @@ -249,7 +266,7 @@ export async function associateAccountToChainArgs( const challenge = await getLinkingChallenge(did, validTill) - // ethereum addresses are 42 characters long since they are 20 bytes hex encoded strings + // ethereum addresses are 42 characters long since they are 20 bytes hex encoded strings // (they start with 0x, 2 characters per byte) const predictedType = accountAddress.length === 42 ? 'ethereum' : 'polkadot' const wrappedChallenge = u8aToHex( From d1842675302396732d0496d957a00c6ab06793e9 Mon Sep 17 00:00:00 2001 From: weichweich Date: Tue, 7 Mar 2023 08:38:27 +0100 Subject: [PATCH 7/7] refactor: rename dotsama to polkadot --- packages/did/src/DidLinks/AccountLinks.chain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 379b065f3..be3a372c6 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -201,7 +201,7 @@ export async function getLinkingArguments( // Force type cast to enable the old blockchain types to accept the future format return result as unknown as AssociateAccountToChainResult } - const result = [{ Dotsama: [accountAddress, proof] }, validUntil] + const result = [{ Polkadot: [accountAddress, proof] }, validUntil] // Force type cast to enable the old blockchain types to accept the future format return result as unknown as AssociateAccountToChainResult }