diff --git a/README.md b/README.md index 43c2e8e..57cb357 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,34 @@ If a call requires a DID assertion method key, either `ATT_MNEMONIC` or `DID_MNE To run this script, execute `yarn call-authorize` and then copy the HEX-encoded operation to be submitted via [PolkadotJS Apps][polkadot-apps] in `Developer > Extrinsics > Decode`, using the account specified in `SUBMITTER_ADDRESS`. +## Generate a DIP signature with a DID key + +This script signs any valid HEX-encoded call of any other parachain with the right key re-generated from the provided seedling information, i.e., either with the provided mnemonic, or with the provided combination of base mnemonic and derivation path. + +Valid HEX-encoded calls can be generated by interacting with [PolkadotJS Apps][polkadot-apps] under the `Developer > Extrinsics` menu. +Once the right call (i.e., the right pallet and right method) with the right parameters has been specified, the HEX-encoded value under `encoded call data` can be copied and passed as parameter to this script. + +The following env variables are required: + +- `CONSUMER_WS_ADDRESS`: The endpoint address of the consumer chain on which DIP is to be used. +- `SUBMITTER_ADDRESS`: The address (encoded with the target chain network prefix `38`) that is authorized to submit the transaction on the target chain. +- `ENCODED_CALL`: The HEX-encoded call to DID-sign. +- `DID_URI`: The URI of the DID authorizing the operation +- `VERIFICATION_METHOD`: The verification method of the DID key to use. Because this script is not able to automatically derive the DID key required to sign the call on the target chain, it has to be explicitely set with this variable. Example values are `authentication`, `assertionMethod`, and `capabilityDelegation`. + +The following optional env variables can be passed: + +- `IDENTITY_DETAILS`: The runtime type definition of the identity details stored on the consumer chain, according to the DIP protocol. It defaults to `u128`, which represents a simple nonce value. +- `ACCOUNT_ID`: The runtime type definition of account address on the consumer chain. It defaults to `AccountId32`, which is the default of most Substrate-based chains. Some chains might use `AccountId20`. + +As with DID creation, there is no strong requirement on what other variables must be set. +Depending on the expected key to be used to sign the call, the right mnemonic or the right base mnemonic + derivation path must be provided. + +For instance, if a call requires a DID authentication key, either `AUTH_MNEMONIC` or `DID_MNEMONIC` and `AUTH_DERIVATION_PATH` must be specified. +If a call requires a DID assertion method key, either `ATT_MNEMONIC` or `DID_MNEMONIC` and `ATT_DERIVATION_PATH` must be specified. + +To run this script, execute `yarn dip-sign` and then copy the generated signature and block number to be submitted via [PolkadotJS Apps][polkadot-apps] as part of the DIP tx submission process, using the account specified in `SUBMITTER_ADDRESS`. + ## Change a DID key The following env variables are required: diff --git a/package.json b/package.json index a8a889a..dfa6bf1 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "att-key-set": "ts-node src/att-key-set.ts", "del-key-set": "ts-node src/del-key-set.ts", "did-create": "ts-node src/did-create.ts", + "dip-sign": "ts-node src/dip-sign.ts", "call-authorize": "ts-node src/call-sign", "check-ts": "tsc src/** --skipLibCheck --noEmit", "lint": "eslint . --ext .ts --format=codeframe", diff --git a/src/dip-sign.ts b/src/dip-sign.ts new file mode 100644 index 0000000..c1eae4e --- /dev/null +++ b/src/dip-sign.ts @@ -0,0 +1,78 @@ +import 'dotenv/config' + +import * as Kilt from '@kiltprotocol/sdk-js' +import { ApiPromise, WsProvider } from '@polkadot/api' + +import * as utils from './utils' + +async function main() { + const consumerWsAddress = process.env[utils.envNames.consumerWsAddress] + if (consumerWsAddress === undefined) { + throw new Error( + `No ${utils.envNames.consumerWsAddress} env variable specified.` + ) + } + const api = await ApiPromise.create({ provider: new WsProvider(consumerWsAddress) }) + + const submitterAddress = process.env[ + utils.envNames.submitterAddress + ] as Kilt.KiltAddress + if (submitterAddress === undefined) { + throw new Error( + `No "${utils.envNames.submitterAddress}" env variable specified.` + ) + } + + // eslint-disable-next-line max-len + const authKey = + utils.generateAuthenticationKey() ?? + Kilt.Utils.Crypto.makeKeypairFromUri('//Alice') + const assertionKey = utils.generateAttestationKey() + const delegationKey = utils.generateDelegationKey() + + const didUri = process.env[utils.envNames.didUri] as Kilt.DidUri + if (didUri === undefined) { + throw new Error(`"${utils.envNames.didUri}" not specified.`) + } + + const encodedCall = process.env[utils.envNames.encodedCall] + const decodedCall = api.createType('Call', encodedCall) + + const [requiredKey, verificationMethod] = (() => { + const providedMethod = utils.parseVerificationMethod() + switch (providedMethod) { + case 'authentication': + return [authKey, providedMethod] + case 'assertionMethod': + return [assertionKey, providedMethod] + case 'capabilityDelegation': + return [delegationKey, providedMethod] + } + })() + if (requiredKey === undefined) { + throw new Error( + 'The DID key to authorize the operation is not part of the DID Document. Please add such a key before re-trying.' + ) + } + const [dipSignature, blockNumber] = + await utils.generateDipTxSignature( + api, + didUri, + decodedCall, + submitterAddress, + verificationMethod, + utils.getKeypairTxSigningCallback(requiredKey), + ) + + console.log( + ` + DID signature for submission via DIP: ${JSON.stringify(utils.hexifyDipSignature(dipSignature), null, 2)}. + Block number used for signature generation: ${blockNumber.toString()}. + Please add these details to the "dipConsumer.dispatchAs" function in PolkadotJS. + ` + ) +} + +main() + .catch((e) => console.error(e)) + .then(() => process.exit(0)) diff --git a/src/utils.ts b/src/utils.ts index 5eda544..0089858 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,9 @@ -import { Keyring } from '@polkadot/api' +import type { BN } from '@polkadot/util' +import type { Call } from '@polkadot/types/interfaces' + +import { ApiPromise, Keyring } from '@polkadot/api' +import { KeyringPair } from '@polkadot/keyring/types' +import { u8aToHex } from '@polkadot/util' import * as Kilt from '@kiltprotocol/sdk-js' @@ -20,6 +25,10 @@ export const envNames = { delDerivationPath: 'DEL_DERIVATION_PATH', delKeyType: 'DEL_KEY_TYPE', encodedCall: 'ENCODED_CALL', + consumerWsAddress: 'CONSUMER_WS_ADDRESS', + verificationMethod: 'VERIFICATION_METHOD', + identityDetailsType: 'IDENTITY_DETAILS', + accountIdType: 'ACCOUNT_ID' } type Defaults = { @@ -27,6 +36,8 @@ type Defaults = { authKeyType: Kilt.KeyringPair['type'] attKeyType: Kilt.KeyringPair['type'] delKeyType: Kilt.KeyringPair['type'] + identityDetailsType: string, + accountIdType: string } export const defaults: Defaults = { @@ -34,6 +45,8 @@ export const defaults: Defaults = { authKeyType: 'sr25519', attKeyType: 'sr25519', delKeyType: 'sr25519', + identityDetailsType: 'u128', + accountIdType: 'AccountId32' } export function getKeypairSigningCallback( @@ -90,7 +103,7 @@ export function generateAuthenticationKey(): Kilt.KiltKeyringPair | undefined { authKeyMnemonic === undefined ? undefined : (process.env[envNames.authKeyType] as Kilt.KeyringPair['type']) || - defaults.authKeyType + defaults.authKeyType if (authKeyMnemonic !== undefined) { return new Keyring().addFromMnemonic( authKeyMnemonic, @@ -125,7 +138,7 @@ export function generateAttestationKey(): Kilt.KiltKeyringPair | undefined { attKeyMnemonic === undefined ? undefined : (process.env[envNames.attKeyType] as Kilt.KeyringPair['type']) || - defaults.attKeyType + defaults.attKeyType if (attKeyMnemonic !== undefined) { return new Keyring().addFromMnemonic( attKeyMnemonic, @@ -160,7 +173,7 @@ export function generateDelegationKey(): Kilt.KiltKeyringPair | undefined { delKeyMnemonic === undefined ? undefined : (process.env[envNames.delKeyType] as Kilt.KeyringPair['type']) || - defaults.delKeyType + defaults.delKeyType if (delKeyMnemonic !== undefined) { return new Keyring().addFromMnemonic( delKeyMnemonic, @@ -197,7 +210,7 @@ export function generateNewAuthenticationKey(): authKeyMnemonic === undefined ? undefined : (process.env[envNames.newAuthKeyType] as Kilt.KeyringPair['type']) || - defaults.authKeyType + defaults.authKeyType if (authKeyMnemonic !== undefined) { return new Keyring().addFromMnemonic( authKeyMnemonic, @@ -208,3 +221,63 @@ export function generateNewAuthenticationKey(): return undefined } } + +const validValues: Set = new Set(['authentication', 'assertionMethod', 'capabilityDelegation']) +export function parseVerificationMethod(): Kilt.VerificationKeyRelationship { + const verificationMethod = process.env[envNames.verificationMethod] + if (verificationMethod === undefined) { + throw new Error(`No ${envNames.verificationMethod} env variable specified.`) + } + const castedVerificationMethod = verificationMethod as Kilt.VerificationKeyRelationship + if (validValues.has(castedVerificationMethod)) { + return castedVerificationMethod + } else { + throw new Error(`Provided value for ${envNames.verificationMethod} does not match any of the expected values: ${validValues}.`) + } +} + +export async function generateDipTxSignature( + api: ApiPromise, + did: Kilt.DidUri, + call: Call, + submitterAccount: KeyringPair['address'], + didKeyRelationship: Kilt.VerificationKeyRelationship, + sign: Kilt.SignExtrinsicCallback, +): Promise<[Kilt.Did.EncodedSignature, BN]> { + const isDipCapable = api.tx.dipConsumer.dispatchAs !== undefined + if (!isDipCapable) { + throw new Error(`The target chain at does not seem to support DIP.`) + } + const blockNumber = await api.query.system.number() + console.log(`DIP signature targeting block number: ${blockNumber.toHuman()}`) + const genesisHash = await api.query.system.blockHash(0) + console.log(`DIP consumer genesis hash: ${genesisHash.toHuman()}`) + const identityDetails = await api.query.dipConsumer.identityProofs(Kilt.Did.toChain(did)) + const identityDetailsType = process.env[envNames.identityDetailsType] ?? defaults.identityDetailsType + console.log( + `DIP subject identity details on consumer chain: ${JSON.stringify(identityDetails, null, 2)} with runtime type "${identityDetailsType}"` + ) + const accountIdType = process.env[envNames.accountIdType] ?? defaults.accountIdType + console.log( + `DIP AccountId runtime type: "${accountIdType}"` + ) + const signaturePayload = + api.createType( + `(Call, ${identityDetailsType}, ${accountIdType}, BlockNumber, Hash)`, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [call, (identityDetails as any).details, submitterAccount, blockNumber, genesisHash] + ).toU8a() + console.log(`Encoded payload for signing: ${u8aToHex(signaturePayload)}`) + const signature = await sign({ data: signaturePayload, keyRelationship: didKeyRelationship, did }) + return [{ + [signature.keyType]: signature.signature + } as Kilt.Did.EncodedSignature, blockNumber.toBn()] +} + +export function hexifyDipSignature(signature: Kilt.Did.EncodedSignature) { + const [signatureType, byteSignature] = Object.entries(signature)[0] + const hexifiedSignature = { + [signatureType]: u8aToHex(byteSignature) + } + return hexifiedSignature +}