Skip to content

Commit

Permalink
feat!: better account linking (#605)
Browse files Browse the repository at this point in the history
  • Loading branch information
arty-name authored Sep 5, 2022
1 parent 40616c1 commit c396dc3
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 76 deletions.
40 changes: 17 additions & 23 deletions packages/core/src/__integrationtests__/AccountLinking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ import type {
KiltKeyringPair,
} from '@kiltprotocol/types'
import { Keyring } from '@polkadot/keyring'
import { BN, u8aToHex } from '@polkadot/util'
import { BN } from '@polkadot/util'
import { mnemonicGenerate } from '@polkadot/util-crypto'
import type { KeypairType } from '@polkadot/util-crypto/types'
import { Balance } from '../balance'
import { convertToTxUnit } from '../balance/Balance.utils'
import {
Expand All @@ -38,15 +37,12 @@ import { disconnect } from '../kilt'

let paymentAccount: KiltKeyringPair
let linkDeposit: BN
let keyring: Keyring
let signingCallback: AccountLinks.LinkingSignerCallback
let sign: AccountLinks.LinkingSignCallback

beforeAll(async () => {
await initializeApi()
paymentAccount = await createEndowedTestAccount()
linkDeposit = await AccountLinks.queryDepositAmount()
keyring = new Keyring({ ss58Format })
signingCallback = AccountLinks.defaultSignerCallback(keyring)
}, 40_000)

describe('When there is an on-chain DID', () => {
Expand Down Expand Up @@ -169,11 +165,14 @@ describe('When there is an on-chain DID', () => {

let keypair: KeyringPair
beforeAll(async () => {
keypair = keyring.addFromMnemonic(
mnemonicGenerate(),
undefined,
keyType as KeypairType
// TODO: remove this line to test against ethereum linking enabled chains
if (keyType === 'ethereum') return

const keyTool = makeSigningKeyTool(
Did.Utils.signatureAlgForKeyType[keyType]
)
keypair = keyTool.keypair
sign = AccountLinks.makeLinkingSignCallback(keypair)
didKey = makeSigningKeyTool()
newDidKey = makeSigningKeyTool()
did = await createFullDidFromSeed(paymentAccount, didKey.keypair)
Expand All @@ -185,7 +184,7 @@ describe('When there is an on-chain DID', () => {
await AccountLinks.getAuthorizeLinkWithAccountExtrinsic(
keypair.address,
did.uri,
signingCallback
sign
)
const signedTx = await Did.authorizeExtrinsic(
did,
Expand Down Expand Up @@ -226,7 +225,7 @@ describe('When there is an on-chain DID', () => {
await AccountLinks.getAuthorizeLinkWithAccountExtrinsic(
keypair.address,
newDid.uri,
signingCallback
sign
)
const signedTx = await Did.authorizeExtrinsic(
newDid,
Expand Down Expand Up @@ -315,17 +314,12 @@ describe('When there is an on-chain DID', () => {
describe('and a generic Ecdsa Substrate account different than the sender to link', () => {
let genericAccount: KeyringPair
beforeAll(async () => {
const genericKeyring = new Keyring()
// also testing that signing with type bitflag works, like the polkadot extension does it
signingCallback = async (payload, address) =>
u8aToHex(
genericKeyring.getPair(address).sign(payload, { withType: true })
)
genericAccount = genericKeyring.addFromMnemonic(
mnemonicGenerate(),
undefined,
'ecdsa'
genericAccount = new Keyring({ type: 'ecdsa' }).addFromMnemonic(
mnemonicGenerate()
)
// also testing that signing with type bitflag works, like the polkadot extension does it
sign = async (payload) => genericAccount.sign(payload, { withType: true })

await fundAccount(genericAccount.address, convertToTxUnit(new BN(10), 1))
didKey = makeSigningKeyTool()
newDidKey = makeSigningKeyTool()
Expand All @@ -338,7 +332,7 @@ describe('When there is an on-chain DID', () => {
await AccountLinks.getAuthorizeLinkWithAccountExtrinsic(
genericAccount.address,
did.uri,
signingCallback
sign
)
const signedTx = await Did.authorizeExtrinsic(
did,
Expand Down
118 changes: 65 additions & 53 deletions packages/did/src/DidLinks/AccountLinks.chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,15 @@ import type { AccountId32, Extrinsic } from '@polkadot/types/interfaces'
import type { AnyNumber, Codec, TypeDef } from '@polkadot/types/types'
import type { HexString } from '@polkadot/util/types'
import type { KeyringPair } from '@polkadot/keyring/types'
import type { KeypairType, VerifyResult } from '@polkadot/util-crypto/types'
import type { KeypairType } from '@polkadot/util-crypto/types'
import {
assert,
BN,
stringToU8a,
u8aConcatStrict,
u8aToHex,
u8aToU8a,
u8aWrapBytes,
U8A_WRAP_ETHEREUM,
} from '@polkadot/util'
import type Keyring from '@polkadot/keyring'

import type {
AugmentedQuery,
Expand All @@ -59,12 +56,11 @@ export type Address = KiltAddress | SubstrateAddress | EthereumAddress
/**
* Type of a linking payload signing function.
*
* It takes the HEX-encoded tuple (DidAddress, BlockNumber) and expects the HEX-encoded signature generated by the provided address in return.
* It takes the HEX-encoded tuple (DidAddress, BlockNumber) and returns the Uint8Array signature generated by the provided address.
*/
export type LinkingSignerCallback = (
encodedLinkingDetails: HexString,
address: SubstrateAddress
) => Promise<HexString>
export type LinkingSignCallback = (
encodedLinkingDetails: HexString
) => Promise<Uint8Array>

type EncodedMultiAddress =
| { AccountId20: Uint8Array }
Expand Down Expand Up @@ -406,14 +402,45 @@ export async function getLinkRemovalByDidExtrinsic(
/**
* Return the default sign callback, which uses the address argument to crete a signing closure for the given payload.
*
* @param keyring The keyring to retrieve the signing key.
* @param keypair The keypair to sign the data with.
* @returns The signature generating callback that uses the keyring to sign the input payload using the input address.
*/
export function defaultSignerCallback(keyring: Keyring): LinkingSignerCallback {
return (payload: HexString, address: Address): Promise<HexString> =>
Promise.resolve(
u8aToHex(keyring.getPair(address).sign(payload, { withType: false }))
)
export function makeLinkingSignCallback(
keypair: KeyringPair
): LinkingSignCallback {
return async function sign(payload: HexString): Promise<Uint8Array> {
return keypair.sign(payload, { withType: false })
}
}

function getUnprefixedSignature(
message: HexString,
signature: Uint8Array,
address: Address
): { signature: Uint8Array; type: KeypairType } {
try {
// try to verify the signature without the prefix first
const unprefixed = signature.subarray(1)
const { crypto, isValid } = signatureVerify(message, unprefixed, address)
if (isValid) {
return {
signature: unprefixed,
type: crypto as KeypairType,
}
}
} catch {
// if it fails, maybe the signature prefix caused that, so we try to verify the whole signature
}

const { crypto, isValid } = signatureVerify(message, signature, address)
if (isValid) {
return {
signature,
type: crypto as KeypairType,
}
}

throw new SDKErrors.SignatureUnverifiableError()
}

/**
Expand All @@ -423,70 +450,55 @@ export function defaultSignerCallback(keyring: Keyring): LinkingSignerCallback {
*
* @param accountAddress Address of the account to be linked.
* @param did Full DID to be linked.
* @param signingCallback The signature generation callback that generates the account signature over the encoded (DidAddress, BlockNumber) tuple.
* @param nBlocksValid How many blocks into the future should the account-signed proof be considered valid?
* @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 Extrinsic that must be DID-authorized by the full DID used.
*/
export async function getAuthorizeLinkWithAccountExtrinsic(
accountAddress: Address,
did: DidUri,
signingCallback: LinkingSignerCallback,
sign: LinkingSignCallback,
nBlocksValid = 10
): Promise<Extrinsic> {
const api = await BlockchainApiConnection.getConnectionOrConnect()

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 blockNumberType =
const BlockNumber =
api.tx.didLookup.associateAccount.meta.args[1].type.toString()
// This is some magic on the polkadot types internals to get the DidAddress definition from the metadata.
// We get it from the connectedAccounts storage, which is a double map from (DidAddress, Account) -> Null.
const didAddressType = (
const DidAddress = (
api.registry.lookup.getTypeDef(
// gets the type id of the keys on the connectedAccounts storage (which is a double map).
api.query.didLookup.connectedAccounts.creator.meta.type.asMap.key
).sub as TypeDef[]
)[0].type // get the type of the first key, which is the DidAddress
const encodedDetails = api
.createType(`(${didAddressType}, ${blockNumberType})`, [
encodeDid(did),
validTill,
])

const encoded = api
.createType(`(${DidAddress}, ${BlockNumber})`, [encodeDid(did), validTill])
.toU8a()

const isAccountId32 = decodeAddress(accountAddress).length > 20
const length = stringToU8a(String(encoded.length))
const paddedDetails = u8aToHex(
decodeAddress(accountAddress).length > 20
? u8aWrapBytes(encodedDetails)
: u8aConcatStrict([
U8A_WRAP_ETHEREUM,
stringToU8a(`${encodedDetails.length}`),
encodedDetails,
])
isAccountId32
? u8aWrapBytes(encoded)
: u8aConcatStrict([U8A_WRAP_ETHEREUM, length, encoded])
)

const { signature, type } = getUnprefixedSignature(
paddedDetails,
await sign(paddedDetails),
accountAddress
)
// The signature may be prefixed; so we try to verify the signature without the prefix first.
// If it fails, we try the same with the prefix and return the result of the second operation.
let signature = u8aToU8a(await signingCallback(paddedDetails, accountAddress))
let result: VerifyResult
try {
result = signatureVerify(
paddedDetails,
signature.subarray(1),
accountAddress
)
// We discard this error message, as the error is caught in the catch block
assert(result.isValid, '')
// Remove type from signature if DID not fail to verify
signature = signature.subarray(1)
} catch {
// Otherwise, try to verify the whole signature
result = signatureVerify(paddedDetails, signature, accountAddress)
assert(result.isValid, 'signature not valid')
}

const sigType = result.crypto as KeypairType
return getAccountSignedAssociationExtrinsic(
accountAddress,
validTill,
signature,
sigType
type
)
}

0 comments on commit c396dc3

Please sign in to comment.