From b14862248c20979fd677bfca38eb8382547d1dab Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 16 Oct 2023 15:49:38 +0200 Subject: [PATCH 01/16] feat: SignerInterface type signers --- packages/utils/package.json | 3 + packages/utils/src/Signers.ts | 171 ++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 packages/utils/src/Signers.ts diff --git a/packages/utils/package.json b/packages/utils/package.json index 3759f261b..a95aeea23 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -34,6 +34,9 @@ "typescript": "^4.8.3" }, "dependencies": { + "@kiltprotocol/ecdsa-secp256k1-jcs-2023": "0.0.1-rc.2", + "@kiltprotocol/eddsa-jcs-2022": "0.0.1-rc.2", + "@kiltprotocol/sr25519-jcs-2023": "0.0.1-rc.2", "@kiltprotocol/types": "workspace:*", "@polkadot/api": "^10.4.0", "@polkadot/keyring": "^12.0.0", diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts new file mode 100644 index 000000000..339559dad --- /dev/null +++ b/packages/utils/src/Signers.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { createSigner as ed25519Signer } from '@kiltprotocol/eddsa-jcs-2022' +import { createSigner as sr25519Signer } from '@kiltprotocol/sr25519-jcs-2023' +import { createSigner as es256kSigner } from '@kiltprotocol/ecdsa-secp256k1-jcs-2023' +import type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' +import { KeyringPair } from '@kiltprotocol/types' +import { + encodeAddress, + randomAsHex, + secp256k1Sign, +} from '@polkadot/util-crypto' +import { Keypair } from '@polkadot/util-crypto/types' +import { decodePair } from '@polkadot/keyring/pair/decode' + +/** + * Signer that produces an ECDSA signature over a Blake2b-256 digest of the message using the secp256k1 curve. + * The signature has a recovery bit appended to the end, allowing public key recovery. + * + * @param root0 + * @param root0.seed + * @param root0.keyUri + */ +export async function polkadotEcdsaSigner({ + seed, + keyUri, +}: { + seed: Uint8Array + keyUri: string +}): Promise { + return { + id: keyUri, + algorithm: 'Ecrecover-Secp256k1-Blake2b', // could also be called ES256K-R-Blake2b + sign: async ({ data }) => { + return secp256k1Sign(data, { secretKey: seed }, 'blake2') + }, + } +} + +/** + * Signer that produces an ECDSA signature over a Keccak-256 digest of the message using the secp256k1 curve. + * The signature has a recovery bit appended to the end, allowing public key recovery. + * + * @param input + * @param input.seed + * @param input.keyUri + * @returns + */ +export async function ethereumEcdsaSigner({ + seed, + keyUri, +}: { + seed: Uint8Array + keyUri: string +}): Promise { + return { + id: keyUri, + algorithm: 'Ecrecover-Secp256k1-Keccak', // could also be called ES256K-R-Keccak + sign: async ({ data }) => { + return secp256k1Sign(data, { secretKey: seed }, 'keccak') + }, + } +} + +function extractPk(pair: KeyringPair): Uint8Array { + const pw = randomAsHex() + const encoded = pair.encodePkcs8(pw) + const { secretKey } = decodePair(pw, encoded) + return secretKey +} + +const signerFactory = { + Ed25519: ed25519Signer, + Sr25519: sr25519Signer, + Es256K: es256kSigner, + 'Ecrecover-Secp256k1-Blake2b': polkadotEcdsaSigner, + 'Ecrecover-Secp256k1-Keccak': ethereumEcdsaSigner, +} + +/** + * @param root0 + * @param root0.keypair + * @param root0.algorithm + * @param root0.keyUri + */ +export async function signerFromKeypair({ + keypair, + keyUri, + algorithm, +}: { + keypair: Keypair | KeyringPair + algorithm: string + keyUri?: string +}): Promise { + const makeSigner: (x: { + seed: Uint8Array + keyUri: string + }) => Promise = signerFactory[algorithm] + if (typeof makeSigner !== 'function') { + throw new Error('unknown algorithm') + } + + if (!('secretKey' in keypair) && 'encodePkcs8' in keypair) { + const id = keyUri ?? keypair.address + return { + id, + algorithm, + sign: async (signData) => { + // TODO: can probably be optimized; but care must be taken to respect keyring locking + const secretKey = extractPk(keypair) + const { sign } = await makeSigner({ seed: secretKey, keyUri: id }) + return sign(signData) + }, + } + } + + const { secretKey, publicKey } = keypair + return makeSigner({ + seed: secretKey, + keyUri: keyUri ?? encodeAddress(publicKey, 38), + }) +} + +function algsForKeyType(keyType: string): string[] { + switch (keyType.toLowerCase()) { + case 'ed25519': + return ['Ed25519'] + case 'sr25519': + return ['Sr25519'] + case 'ecdsa': + case 'secpk256k1': + return [ + 'Ecrecover-Secp256k1-Blake2b', + 'Ecrecover-Secp256k1-Keccak', + 'ES256K', + ] + default: + return [] + } +} + +/** + * @param root0 + * @param root0.keypair + * @param root0.type + * @param root0.keyUri + */ +export async function getSignersForKeypair({ + keypair, + type = (keypair as KeyringPair).type, + keyUri, +}: { + keypair: Keypair | KeyringPair + type?: string + keyUri?: string +}): Promise { + if (!type) { + throw new Error('type is required if keypair.type is not given') + } + const algorithms = algsForKeyType(type) + return Promise.all( + algorithms.map>(async (algorithm) => { + return signerFromKeypair({ keypair, keyUri, algorithm }) + }) + ) +} From 605cec8eaa0d806ace0697b1267a6890080a2280 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 18 Oct 2023 16:20:16 +0200 Subject: [PATCH 02/16] refactor!: signer interface POC --- .../credentialsV1/KiltAttestationProofV1.ts | 20 +- .../core/src/delegation/DelegationNode.ts | 49 ++-- packages/did/src/Did.chain.ts | 52 ++-- packages/did/src/Did.signature.spec.ts | 84 +++--- .../did/src/DidDetails/FullDidDetails.spec.ts | 31 ++- packages/did/src/DidDetails/FullDidDetails.ts | 96 +++++-- .../did/src/DidResolver/DidResolver.spec.ts | 2 +- .../legacy-credentials/src/Credential.spec.ts | 66 ++--- packages/legacy-credentials/src/Credential.ts | 41 ++- packages/types/src/Imported.ts | 1 + packages/utils/src/Signers.ts | 247 +++++++++++++++--- packages/utils/src/index.ts | 1 + .../src/suites/KiltAttestationProofV1.spec.ts | 23 +- .../src/suites/KiltAttestationProofV1.ts | 16 +- tests/bundle/bundle-test.ts | 62 ++--- tests/integration/AccountLinking.spec.ts | 30 ++- tests/integration/Attestation.spec.ts | 66 +++-- tests/integration/Balance.spec.ts | 4 +- tests/integration/Blockchain.spec.ts | 2 +- tests/integration/Ctypes.spec.ts | 12 +- tests/integration/Delegation.spec.ts | 66 ++--- tests/integration/Deposit.spec.ts | 40 +-- tests/integration/Did.spec.ts | 113 ++++---- tests/integration/ErrorHandler.spec.ts | 4 +- tests/integration/PublicCredentials.spec.ts | 22 +- tests/integration/Web3Names.spec.ts | 18 +- tests/integration/utils.ts | 5 +- tests/testUtils/TestUtils.ts | 59 ++--- yarn.lock | 58 +++- 29 files changed, 797 insertions(+), 493 deletions(-) diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index f4942a116..7529c95e9 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -43,11 +43,13 @@ import type { RuntimeCommonAuthorizationAuthorizationId, } from '@kiltprotocol/augment-api' import type { + DidDocument, Did, ICType, IDelegationNode, KiltAddress, SignExtrinsicCallback, + SignerInterface, } from '@kiltprotocol/types' import * as CType from '../ctype/index.js' @@ -671,7 +673,7 @@ export type TxHandler = { } export type IssueOpts = { - didSigner: DidSigner + signers: SignerInterface[] transactionHandler: TxHandler } & Parameters[4] @@ -695,8 +697,9 @@ function makeDefaultTxSubmit( * Creates a complete [[KiltAttestationProofV1]] for issuing a new credential. * * @param credential A [[KiltCredentialV1]] for which a proof shall be created. + * @param issuer The DID or DID Document of the DID acting as the issuer. * @param opts Additional parameters. - * @param opts.didSigner Object containing the attester's `did` and a `signer` callback which authorizes the on-chain anchoring of the credential with the attester's signature. + * @param opts.signers An array of signer interfaces related to the issuer's keys. The function selects the appropriate handlers for all signatures required for issuance (e.g., authorizing the on-chain anchoring of the credential). * @param opts.transactionHandler Object containing the submitter `address` that's going to cover the transaction fees as well as either a `signer` or `signAndSubmit` callback handling extrinsic signing and submission. * The signAndSubmit callback receives an unsigned extrinsic and is expected to return the `blockHash` and (optionally) `timestamp` when the extrinsic was included in a block. * This callback must thus take care of signing and submitting the extrinsic to the KILT blockchain as well as noting the inclusion block. @@ -705,18 +708,23 @@ function makeDefaultTxSubmit( */ export async function issue( credential: Omit, - { didSigner, transactionHandler, ...otherParams }: IssueOpts + issuer: Did | DidDocument, + { signers, transactionHandler, ...otherParams }: IssueOpts ): Promise { - const updatedCredential = { ...credential, issuer: didSigner.did } + const updatedCredential = { + ...credential, + issuer: typeof issuer === 'string' ? issuer : issuer.id, + } const [proof, callArgs] = initializeProof(updatedCredential) const api = ConfigService.get('api') const call = api.tx.attestation.add(...callArgs) const txSubmissionHandler = transactionHandler.signAndSubmit ?? makeDefaultTxSubmit(transactionHandler) + const didSigned = await authorizeTx( - didSigner.did, + issuer, call, - didSigner.signer, + signers, transactionHandler.account, otherParams ) diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index e63b156e4..b25a9bb8d 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -12,11 +12,10 @@ import type { IAttestation, IDelegationHierarchyDetails, IDelegationNode, - SignCallback, + SignerInterface, SubmittableExtrinsic, - DidUrl, } from '@kiltprotocol/types' -import { Crypto, SDKErrors, UUID } from '@kiltprotocol/utils' +import { Crypto, SDKErrors, Signers, UUID } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' import * as Did from '@kiltprotocol/did' @@ -259,38 +258,34 @@ export class DelegationNode implements IDelegationNode { * This is required to anchor the delegation node on chain in order to enforce the delegate's consent. * * @param delegateDid The DID of the delegate. - * @param sign The callback to sign the delegation creation details for the delegate. + * @param signers An array of signer interfaces, one of which will be selected to sign the delegation creation details for the delegate. * @returns The DID signature over the delegation **as a hex string**. */ public async delegateSign( delegateDid: DidDocument, - sign: SignCallback + signers: SignerInterface[] ): Promise { - const delegateSignature = await sign({ - data: this.generateHash(), - did: delegateDid.id, - verificationRelationship: 'authentication', - }) - const signerUrl = - `${delegateDid.id}${delegateSignature.verificationMethod.id}` as DidUrl - const { fragment } = Did.parse(signerUrl) - if (!fragment) { - throw new SDKErrors.DidError( - `DID verification method URL "${signerUrl}" couldn't be parsed` - ) - } - const verificationMethod = delegateDid.verificationMethod?.find( - ({ id }) => id === fragment + const { byDid, verifiableOnChain } = Signers.select + const signer = await Signers.selectSigner( + signers, + verifiableOnChain(), + byDid(delegateDid, { + verificationRelationship: 'authentication', + }) ) - if (!verificationMethod) { - throw new SDKErrors.DidError( - `Verification method "${signerUrl}" was not found on DID: "${delegateDid.id}"` + if (!signer) { + throw new Error( + 'no signer available for on-chain verifiable signatures by an authentication key' ) } - return Did.didSignatureToChain( - verificationMethod, - delegateSignature.signature - ) + const delegateSignature = await signer.sign({ + data: this.generateHash(), + }) + + return Did.didSignatureToChain({ + signature: delegateSignature, + algorithm: signer.algorithm, + }) } /** diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index d8ebc0f1e..ecf1b749e 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -21,8 +21,7 @@ import type { Did, KiltAddress, Service, - SignatureVerificationRelationship, - SignExtrinsicCallback, + SignerInterface, SignRequestData, SignResponseData, SubmittableExtrinsic, @@ -31,7 +30,7 @@ import type { } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' -import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import { Crypto, SDKErrors, Signers, ss58Format } from '@kiltprotocol/utils' import type { DidEncryptionMethodType, @@ -43,7 +42,6 @@ import type { import { isValidVerificationMethodType } from './DidDetails/DidDetails.js' import { - multibaseKeyToDidKey, keypairToMultibaseKey, getAddressFromVerificationMethod, getFullDid, @@ -448,8 +446,7 @@ export async function getStoreTx( } export interface SigningOptions { - sign: SignExtrinsicCallback - verificationRelationship: SignatureVerificationRelationship + signer: SignerInterface } /** @@ -458,8 +455,7 @@ export interface SigningOptions { * * @param params Object wrapping all input to the function. * @param params.did Full DID. - * @param params.verificationRelationship DID verification relationship to be used for authorization. - * @param params.sign The callback to interface with the key store managing the private key to be used. + * @param params.signer The signer interface with the key store managing the private key to be used. * @param params.call The call or extrinsic to be authorized. * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. * @param params.submitter Payment account allowed to submit this extrinsic and cover its fees, which will end up owning any deposit associated with newly created records. @@ -468,8 +464,7 @@ export interface SigningOptions { */ export async function generateDidAuthenticatedTx({ did, - verificationRelationship, - sign, + signer, call, txCounter, submitter, @@ -487,33 +482,36 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const { signature, verificationMethod } = await sign({ + const signature = await signer.sign({ data: signableCall.toU8a(), - verificationRelationship, - did, }) - const { keyType } = multibaseKeyToDidKey( - verificationMethod.publicKeyMultibase - ) - const encodedSignature = { - [keyType]: signature, - } as EncodedSignature + const encodedSignature = didSignatureToChain({ + signature, + algorithm: signer.algorithm, + }) return api.tx.did.submitDidCall(signableCall, encodedSignature) } /** - * Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics. + * Compiles an enum-type key-value pair representation of a signature created with a signer associated with a full DID verification method. Required for creating full DID signed extrinsics. * - * @param key Object describing data associated with a public key. - * @param key.publicKeyMultibase The multibase, multicodec representation of the signing public key. - * @param signature The signature generated with the full DID associated public key. + * @param input Signature and algorithm. + * @param input.algorithm Descriptor of the signature algorithm used by the signer. + * @param input.signature The signature generated by the signer. * @returns Data restructured to allow SCALE encoding by polkadot api. */ -export function didSignatureToChain( - { publicKeyMultibase }: VerificationMethod, +export function didSignatureToChain({ + algorithm, + signature, +}: { + algorithm: string signature: Uint8Array -): EncodedSignature { - const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) +}): EncodedSignature { + const lower = algorithm.toLowerCase() + const keyType = + lower === Signers.ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B.toLowerCase() + ? 'ecdsa' + : lower if (!isValidVerificationMethodType(keyType)) { throw new SDKErrors.DidError( `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index e5672ca8f..66d96bfef 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -13,6 +13,7 @@ import type { DidUrl, DidSignature, DereferenceResult, + SignerInterface, } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' @@ -24,7 +25,6 @@ import { makeSigningKeyTool } from '../../../tests/testUtils' import { isDidSignature, signatureFromJson, - signatureToJson, verifyDidSignature, } from './Did.signature' import { dereference, SupportedContentType } from './DidResolver/DidResolver' @@ -39,14 +39,17 @@ jest describe('light DID', () => { let keypair: KiltKeyringPair let did: DidDocument - let sign: SignCallback - beforeAll(() => { - const keyTool = makeSigningKeyTool() + let authenticationSigner: SignerInterface + beforeAll(async () => { + const keyTool = await makeSigningKeyTool() keypair = keyTool.keypair did = createLightDidDocument({ authentication: keyTool.authentication, }) - sign = keyTool.getSignCallback(did) + authenticationSigner = (await keyTool.getSigners(did)).find( + ({ id }) => id === did.id + did.authentication?.[0] + )! + expect(authenticationSigner).toBeDefined() }) beforeEach(() => { @@ -73,16 +76,15 @@ describe('light DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) + expect(signature).toBeInstanceOf(Uint8Array) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -90,13 +92,13 @@ describe('light DID', () => { it('deserializes old did signature (with `keyId` property) to new format', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = signatureToJson( - await sign({ + const signature = Crypto.u8aToHex( + await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) ) + const keyUri = authenticationSigner.id as DidUrl + const oldSignature = { signature, keyId: keyUri, @@ -110,16 +112,14 @@ describe('light DID', () => { it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: SIGNED_BYTES, - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -127,16 +127,14 @@ describe('light DID', () => { it('fails if relationship does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedVerificationRelationship: 'assertionMethod', }) ).rejects.toThrow() @@ -144,13 +142,9 @@ describe('light DID', () => { it('fails if verification method id does not match', async () => { const SIGNED_STRING = 'signed string' - // eslint-disable-next-line prefer-const - let { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) - const wrongVerificationMethodId = `${verificationMethod.id}1a` jest.mocked(dereference).mockResolvedValue({ contentMetadata: {}, dereferencingMetadata: { error: 'notFound' }, @@ -159,7 +153,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${wrongVerificationMethodId}` as DidUrl, + signerUrl: `${authenticationSigner.id}1a` as DidUrl, expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() @@ -167,16 +161,14 @@ describe('light DID', () => { it('fails if signature does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING.substring(1), signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() @@ -185,18 +177,16 @@ describe('light DID', () => { it('fails if verification method id malformed', async () => { jest.mocked(dereference).mockRestore() const SIGNED_STRING = 'signed string' - // eslint-disable-next-line prefer-const - let { signature, verificationMethod } = await sign({ + + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) - const malformedVerificationId = verificationMethod.id.replace('#', '?') + await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${malformedVerificationId}` as DidUrl, + signerUrl: authenticationSigner.id as DidUrl, expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() @@ -211,16 +201,14 @@ describe('light DID', () => { contentStream: { id: did.id }, }) const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() @@ -236,21 +224,19 @@ describe('light DID', () => { it('detects signer expectation mismatch if signature is by unrelated did', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) const expectedSigner = createLightDidDocument({ - authentication: makeSigningKeyTool().authentication, + authentication: (await makeSigningKeyTool()).authentication, }).id await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedSigner, expectedVerificationRelationship: 'authentication', }) @@ -259,10 +245,8 @@ describe('light DID', () => { it('allows variations of the same light did', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) const authKey = did.verificationMethod?.find( @@ -292,7 +276,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedSigner, expectedVerificationRelationship: 'authentication', }) diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 10e39ebdc..3d14af0fb 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -12,7 +12,7 @@ import { ConfigService } from '@kiltprotocol/config' import type { DidDocument, KiltKeyringPair, - SignCallback, + SignerInterface, SubmittableExtrinsic, } from '@kiltprotocol/types' @@ -31,6 +31,13 @@ const augmentedApi = ApiMocks.createAugmentedApi() const mockedApi: any = ApiMocks.getMockedApi() ConfigService.set({ api: mockedApi }) +jest.mock('../DidResolver/DidResolver', () => { + return { + ...jest.requireActual('../DidResolver/DidResolver'), + resolve: jest.fn(), + dereference: jest.fn(), + } +}) jest.mock('../Did.chain') jest .mocked(generateDidAuthenticatedTx) @@ -45,14 +52,14 @@ jest describe('When creating an instance from the chain', () => { describe('authorizeBatch', () => { let keypair: KiltKeyringPair - let sign: SignCallback + let signers: SignerInterface[] let fullDid: DidDocument beforeAll(async () => { - const keyTool = makeSigningKeyTool() + const keyTool = await makeSigningKeyTool() keypair = keyTool.keypair fullDid = await createLocalDemoFullDidFromKeypair(keyTool.keypair) - sign = keyTool.getSignCallback(fullDid) + signers = await keyTool.getSigners(fullDid) }) describe('.addSingleTx()', () => { @@ -60,10 +67,10 @@ describe('When creating an instance from the chain', () => { const extrinsic = augmentedApi.tx.indices.claim(1) await expect(async () => authorizeBatch({ - did: fullDid.id, + did: fullDid, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [extrinsic, extrinsic], - sign, + signers, submitter: keypair.address, }) ).rejects.toMatchInlineSnapshot( @@ -78,10 +85,10 @@ describe('When creating an instance from the chain', () => { const batchFunction = jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll await authorizeBatch({ - did: fullDid.id, + did: fullDid, batchFunction, extrinsics: [extrinsic, extrinsic], - sign, + signers, submitter: keypair.address, }) @@ -115,11 +122,11 @@ describe('When creating an instance from the chain', () => { ctype4Extrinsic, ] await authorizeBatch({ - did: fullDid.id, + did: fullDid, batchFunction, extrinsics, nonce: new BN(0), - sign, + signers, submitter: keypair.address, }) @@ -143,10 +150,10 @@ describe('When creating an instance from the chain', () => { it('throws if batch is empty', async () => { await expect(async () => authorizeBatch({ - did: fullDid.id, + did: fullDid, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [], - sign, + signers, submitter: keypair.address, }) ).rejects.toMatchInlineSnapshot( diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 5303e6fb0..340a7c623 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -10,14 +10,15 @@ import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' import { BN } from '@polkadot/util' import type { + DidDocument, Did, KiltAddress, SignatureVerificationRelationship, - SignExtrinsicCallback, + SignerInterface, SubmittableExtrinsic, } from '@kiltprotocol/types' -import { SDKErrors } from '@kiltprotocol/utils' +import { SDKErrors, Signers } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' import { @@ -26,6 +27,7 @@ import { toChain, } from '../Did.chain.js' import { parse } from '../Did.utils.js' +import { resolve } from '../DidResolver/DidResolver.js' // Must be in sync with what's implemented in impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for Call // in https://github.com/KILTprotocol/mashnet-node/blob/develop/runtimes/spiritnet/src/lib.rs @@ -110,21 +112,23 @@ async function getNextNonce(did: Did): Promise { return increaseNonce(currentNonce) } +const { verifiableOnChain, byDid } = Signers.select + /** * Signs and returns the provided unsigned extrinsic with the right DID verification method, if present. Otherwise, it will throw an error. * - * @param did The DID data. + * @param did The DID or DID Document of the authorizing DID. * @param extrinsic The unsigned extrinsic to sign. - * @param sign The callback to sign the operation. + * @param signers An array of signer interfaces. The function will select the appropriate signer for signing this extrinsic. * @param submitterAccount The KILT account to bind the DID operation to (to avoid MitM and replay attacks). * @param signingOptions The signing options. * @param signingOptions.txCounter The optional DID nonce to include in the operation signatures. By default, it uses the next value of the nonce stored on chain. * @returns The DID-signed submittable extrinsic. */ export async function authorizeTx( - did: Did, + did: Did | DidDocument, extrinsic: Extrinsic, - sign: SignExtrinsicCallback, + signers: SignerInterface[], submitterAccount: KiltAddress, { txCounter, @@ -132,7 +136,16 @@ export async function authorizeTx( txCounter?: BN } = {} ): Promise { - if (parse(did).type === 'light') { + let didUri: Did + let didDocument: DidDocument | undefined + if (typeof did === 'string') { + didUri = did + } else { + didUri = did.id + didDocument = did + } + + if (parse(didUri).type === 'light') { throw new SDKErrors.DidError( `An extrinsic can only be authorized with a full DID, not with "${did}"` ) @@ -145,12 +158,26 @@ export async function authorizeTx( ) } + if (!didDocument) { + didDocument = (await resolve(didUri)).didDocument as DidDocument + } + if (!didDocument?.id) { + throw new Error('failed to resolve signer DID') + } + const signer = await Signers.selectSigner( + signers, + verifiableOnChain(), + byDid(didDocument, { verificationRelationship }) + ) + if (typeof signer === 'undefined') { + throw new Error('incompatible signers') // TODO: improve error message + } + return generateDidAuthenticatedTx({ - did, - verificationRelationship, - sign, + did: didUri, + signer, call: extrinsic, - txCounter: txCounter || (await getNextNonce(did)), + txCounter: txCounter || (await getNextNonce(didUri)), submitter: submitterAccount, }) } @@ -202,9 +229,9 @@ function groupExtrinsicsByVerificationRelationship( * * @param input The object with named parameters. * @param input.batchFunction The batch function to use, for example `api.tx.utility.batchAll`. - * @param input.did The DID document. + * @param input.did The DID or DID Document of the authorizing DID. * @param input.extrinsics The array of unsigned extrinsics to sign. - * @param input.sign The callback to sign the operation. + * @param input.signers An array of signer interfaces. The function will select the appropriate signer for signing each extrinsic. * @param input.submitter The KILT account to bind the DID operation to (to avoid MitM and replay attacks). * @param input.nonce The optional nonce to use for the first batch, next batches will use incremented value. * @returns The DID-signed submittable extrinsic. @@ -214,14 +241,14 @@ export async function authorizeBatch({ did, extrinsics, nonce, - sign, + signers, submitter, }: { batchFunction: SubmittableExtrinsicFunction<'promise'> - did: Did + did: Did | DidDocument extrinsics: Extrinsic[] nonce?: BN - sign: SignExtrinsicCallback + signers: SignerInterface[] submitter: KiltAddress }): Promise { if (extrinsics.length === 0) { @@ -230,20 +257,37 @@ export async function authorizeBatch({ ) } - if (parse(did).type === 'light') { + let didUri: Did + let didDocument: DidDocument | undefined + if (typeof did === 'string') { + didUri = did + } else { + didUri = did.id + didDocument = did + } + + if (parse(didUri).type === 'light') { throw new SDKErrors.DidError( `An extrinsic can only be authorized with a full DID, not with "${did}"` ) } if (extrinsics.length === 1) { - return authorizeTx(did, extrinsics[0], sign, submitter, { + return authorizeTx(did, extrinsics[0], signers, submitter, { txCounter: nonce, }) } const groups = groupExtrinsicsByVerificationRelationship(extrinsics) - const firstNonce = nonce || (await getNextNonce(did)) + const firstNonce = nonce || (await getNextNonce(didUri)) + + // resolve DID document beforehand to avoid resolving in loop + if (!didDocument) { + didDocument = (await resolve(didUri)).didDocument + } + if (typeof didDocument?.id !== 'string') { + throw new Error('failed to resolve signer DID') + } const promises = groups.map(async (group, batchIndex) => { const list = group.extrinsics @@ -252,10 +296,18 @@ export async function authorizeBatch({ const { verificationRelationship } = group + const signer = await Signers.selectSigner( + signers, + verifiableOnChain(), + byDid(didDocument!, { verificationRelationship }) + ) + if (typeof signer === 'undefined') { + throw new Error('incompatible signers') // TODO: improve error message + } + return generateDidAuthenticatedTx({ - did, - verificationRelationship, - sign, + did: didUri, + signer, call, txCounter, submitter, diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index 622d2d272..9c7d1c96e 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -487,7 +487,7 @@ describe('When resolving a full DID', () => { .mockResolvedValueOnce( augmentedApi.createType('Option', null) ) - const randomKeypair = makeSigningKeyTool().authentication[0] + const randomKeypair = (await makeSigningKeyTool()).authentication[0] const randomDid = Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(randomKeypair), }) diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index c5bc9f12e..5b034bdce 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -23,7 +23,7 @@ import type { IClaimContents, ICredential, ICredentialPresentation, - SignCallback, + SignerInterface, VerificationMethod, } from '@kiltprotocol/types' import { @@ -471,7 +471,7 @@ describe('Presentations', () => { attesterDid: KiltDid, contents: IClaim['contents'], legitimations: ICredential[], - sign: SignCallback + signers: SignerInterface[] ): Promise<[ICredentialPresentation, IAttestation]> { // create claim const claim = Claim.fromCTypeAndClaimContents( @@ -485,7 +485,8 @@ describe('Presentations', () => { }) const presentation = await Credential.createPresentation({ credential, - signCallback: sign, + signers, + didDocument: claimer, }) // build attestation const testAttestation = Attestation.fromCredentialAndDid( @@ -496,13 +497,13 @@ describe('Presentations', () => { } beforeAll(async () => { - keyAlice = makeSigningKeyTool() + keyAlice = await makeSigningKeyTool() identityAlice = await createLocalDemoFullDidFromKeypair(keyAlice.keypair) - const keyBob = makeSigningKeyTool() + const keyBob = await makeSigningKeyTool() identityBob = await createLocalDemoFullDidFromKeypair(keyBob.keypair) - keyCharlie = makeSigningKeyTool() + keyCharlie = await makeSigningKeyTool() identityCharlie = await createLocalDemoFullDidFromKeypair( keyCharlie.keypair ) @@ -511,7 +512,7 @@ describe('Presentations', () => { identityBob.id, {}, [], - keyAlice.getSignCallback(identityAlice) + await keyAlice.getSigners(identityAlice) ) jest @@ -535,7 +536,7 @@ describe('Presentations', () => { c: 'c', }, [legitimation], - keyCharlie.getSignCallback(identityCharlie) + await keyCharlie.getSigners(identityCharlie) ) // check proof on complete data @@ -547,7 +548,7 @@ describe('Presentations', () => { ).resolves.toMatchObject({ revoked: false, attester: identityBob.id }) }) it('verify credentials signed by a light DID', async () => { - const { getSignCallback, authentication } = makeSigningKeyTool('ed25519') + const { getSigners, authentication } = await makeSigningKeyTool('ed25519') identityDave = Did.createLightDidDocument({ authentication, }) @@ -561,7 +562,7 @@ describe('Presentations', () => { c: 'c', }, [legitimation], - getSignCallback(identityDave) + await getSigners(identityDave) ) // check proof on complete data @@ -592,7 +593,7 @@ describe('Presentations', () => { }) it('throws if signature is by unrelated did', async () => { - const { getSignCallback, authentication } = makeSigningKeyTool('ed25519') + const { getSigners, authentication } = await makeSigningKeyTool('ed25519') identityDave = Did.createLightDidDocument({ authentication, }) @@ -609,7 +610,8 @@ describe('Presentations', () => { const presentation = await Credential.createPresentation({ credential, - signCallback: getSignCallback(identityDave), + signers: await getSigners(identityDave), + didDocument: identityDave, }) await expect( @@ -638,7 +640,8 @@ describe('Presentations', () => { // sign presentation using Alice's authentication verification method const presentation = await Credential.createPresentation({ credential, - signCallback: keyAlice.getSignCallback(identityAlice), + signers: await keyAlice.getSigners(identityAlice), + didDocument: identityAlice, }) // but replace signer key reference with authentication verification method of light did presentation.claimerSignature.keyUri = `${identityDave.id}${ @@ -654,7 +657,7 @@ describe('Presentations', () => { }) it('fail to verify credentials signed by a light DID after it has been migrated and deleted', async () => { - const migratedAndDeleted = makeSigningKeyTool('ed25519') + const migratedAndDeleted = await makeSigningKeyTool('ed25519') migratedAndDeletedLightDid = Did.createLightDidDocument({ authentication: migratedAndDeleted.authentication, }) @@ -668,7 +671,7 @@ describe('Presentations', () => { c: 'c', }, [legitimation], - migratedAndDeleted.getSignCallback(migratedAndDeletedLightDid) + await migratedAndDeleted.getSigners(migratedAndDeletedLightDid) ) // check proof on complete data @@ -686,7 +689,7 @@ describe('Presentations', () => { identityBob.id, {}, [], - keyAlice.getSignCallback(identityAlice) + await keyAlice.getSigners(identityAlice) ) expect(Credential.isICredential(presentation)).toBe(true) delete (presentation as Partial).claimHashes @@ -699,7 +702,7 @@ describe('Presentations', () => { identityBob.id, {}, [], - keyAlice.getSignCallback(identityAlice) + await keyAlice.getSigners(identityAlice) ) expect(() => Credential.verifyAgainstAttestation(attestation, credential) @@ -721,7 +724,7 @@ describe('Presentations', () => { identityBob.id, {}, [], - keyAlice.getSignCallback(identityAlice) + await keyAlice.getSigners(identityAlice) ) expect(Credential.getHash(credential)).toEqual(attestation.claimHash) }) @@ -822,25 +825,25 @@ describe('create presentation', () => { } beforeAll(async () => { - const { keypair } = makeSigningKeyTool() + const { keypair } = await makeSigningKeyTool() attester = await createLocalDemoFullDidFromKeypair(keypair) - unmigratedClaimerKey = makeSigningKeyTool() + unmigratedClaimerKey = await makeSigningKeyTool() unmigratedClaimerLightDid = Did.createLightDidDocument({ authentication: unmigratedClaimerKey.authentication, }) - const migratedClaimerKey = makeSigningKeyTool() + const migratedClaimerKey = await makeSigningKeyTool() migratedClaimerLightDid = Did.createLightDidDocument({ authentication: migratedClaimerKey.authentication, }) // Change also the authentication key of the full DID to properly verify signature verification, // so that it uses a completely different key and the credential is still correctly verified. - newKeyForMigratedClaimerDid = makeSigningKeyTool() + newKeyForMigratedClaimerDid = await makeSigningKeyTool() migratedClaimerFullDid = createMinimalFullDidFromLightDid( migratedClaimerLightDid, { ...newKeyForMigratedClaimerDid.keypair } ) - migratedThenDeletedKey = makeSigningKeyTool('ed25519') + migratedThenDeletedKey = await makeSigningKeyTool('ed25519') migratedThenDeletedClaimerLightDid = Did.createLightDidDocument({ authentication: migratedThenDeletedKey.authentication, }) @@ -873,10 +876,11 @@ describe('create presentation', () => { const presentation = await Credential.createPresentation({ credential, selectedAttributes: ['name'], - signCallback: newKeyForMigratedClaimerDid.getSignCallback( + signers: await newKeyForMigratedClaimerDid.getSigners( migratedClaimerFullDid ), challenge, + didDocument: migratedClaimerFullDid, }) await expect( Credential.verifyPresentation(presentation, { @@ -902,10 +906,9 @@ describe('create presentation', () => { const presentation = await Credential.createPresentation({ credential, selectedAttributes: ['name'], - signCallback: unmigratedClaimerKey.getSignCallback( - unmigratedClaimerLightDid - ), + signers: await unmigratedClaimerKey.getSigners(unmigratedClaimerLightDid), challenge, + didDocument: unmigratedClaimerLightDid, }) await expect( Credential.verifyPresentation(presentation, { @@ -933,10 +936,11 @@ describe('create presentation', () => { credential, selectedAttributes: ['name'], // Use of full DID to sign the presentation. - signCallback: newKeyForMigratedClaimerDid.getSignCallback( + signers: await newKeyForMigratedClaimerDid.getSigners( migratedClaimerFullDid ), challenge, + didDocument: migratedClaimerFullDid, }) await expect( Credential.verifyPresentation(presentation, { @@ -965,10 +969,11 @@ describe('create presentation', () => { credential, selectedAttributes: ['name'], // Still using the light DID, which should fail since it has been migrated - signCallback: newKeyForMigratedClaimerDid.getSignCallback( + signers: await newKeyForMigratedClaimerDid.getSigners( migratedClaimerLightDid ), challenge, + didDocument: migratedClaimerLightDid, }) await expect( Credential.verifyPresentation(att, { @@ -996,10 +1001,11 @@ describe('create presentation', () => { credential, selectedAttributes: ['name'], // Still using the light DID, which should fail since it has been migrated and then deleted - signCallback: migratedThenDeletedKey.getSignCallback( + signers: await migratedThenDeletedKey.getSigners( migratedThenDeletedClaimerLightDid ), challenge, + didDocument: migratedThenDeletedClaimerLightDid, }) await expect( Credential.verifyPresentation(presentation, { diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 54ed56cea..671893630 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -21,12 +21,13 @@ import { ConfigService } from '@kiltprotocol/config' import { Attestation, CType } from '@kiltprotocol/core' import { isDidSignature, + resolve, dereference, signatureFromJson, - signatureToJson, verifyDidSignature, } from '@kiltprotocol/did' import type { + DidUrl, Did, Hash, IAttestation, @@ -35,10 +36,11 @@ import type { ICredential, ICredentialPresentation, IDelegationNode, - SignCallback, + SignerInterface, DereferenceDidUrl, + DidDocument, } from '@kiltprotocol/types' -import { Crypto, DataUtils, SDKErrors } from '@kiltprotocol/utils' +import { Crypto, DataUtils, SDKErrors, Signers } from '@kiltprotocol/utils' import * as Claim from './Claim.js' import { hashClaimContents } from './Claim.js' @@ -505,28 +507,33 @@ function getAttributes(credential: ICredential): Set { return new Set(Object.keys(credential.claim.contents)) } +const { verifiableOnChain, byDid } = Signers.select + /** * Creates a public presentation which can be sent to a verifier. * This presentation is signed. * * @param presentationOptions The additional options to use upon presentation generation. * @param presentationOptions.credential The credential to create the presentation for. - * @param presentationOptions.signCallback The callback to sign the presentation. + * @param presentationOptions.signers An array of signer interfaces, one of which will be selected to sign the presentation. * @param presentationOptions.selectedAttributes All properties of the claim which have been requested by the verifier and therefore must be publicly presented. * @param presentationOptions.challenge Challenge which will be part of the presentation signature. * If not specified, all attributes are shown. If set to an empty array, we hide all attributes inside the claim for the presentation. + * @param presentationOptions.didDocument The credential owner's DID document; if omitted, it will be resolved from the blockchain. * @returns A deep copy of the Credential with all but `publicAttributes` removed. */ export async function createPresentation({ credential, - signCallback, + signers, selectedAttributes, challenge, + didDocument, }: { credential: ICredential - signCallback: SignCallback + signers: SignerInterface[] selectedAttributes?: string[] challenge?: string + didDocument?: DidDocument }): Promise { // filter attributes that are not in public attributes const excludedClaimProperties = selectedAttributes @@ -541,16 +548,30 @@ export async function createPresentation({ excludedClaimProperties ) - const signature = await signCallback({ + if (!didDocument) { + didDocument = (await resolve(credential.claim.owner)).didDocument + } + if (!didDocument) { + throw new Error('claimer DID cannot be resolved') + } + const signer = await Signers.selectSigner( + signers, + verifiableOnChain(), + byDid(didDocument, { verificationRelationship: 'authentication' }) + ) + if (!signer) { + throw new Error('no suitable signer available') + } + + const signature = await signer?.sign({ data: makeSigningData(presentation, challenge), - did: credential.claim.owner, - verificationRelationship: 'authentication', }) return { ...presentation, claimerSignature: { - ...signatureToJson(signature), + signature: Crypto.u8aToHex(signature), + keyUri: signer.id as DidUrl, ...(challenge && { challenge }), }, } diff --git a/packages/types/src/Imported.ts b/packages/types/src/Imported.ts index f83228443..0bd426ebb 100644 --- a/packages/types/src/Imported.ts +++ b/packages/types/src/Imported.ts @@ -15,3 +15,4 @@ export type { HexString } from '@polkadot/util/types' export type { Prefix } from '@polkadot/util-crypto/address/types' export type { SubmittableExtrinsic } from '@polkadot/api/promise/types' export type { KeyringPair } from '@polkadot/keyring/types' +export type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 339559dad..1bb3c6dcf 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -5,39 +5,65 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { createSigner as ed25519Signer } from '@kiltprotocol/eddsa-jcs-2022' -import { createSigner as sr25519Signer } from '@kiltprotocol/sr25519-jcs-2023' -import { createSigner as es256kSigner } from '@kiltprotocol/ecdsa-secp256k1-jcs-2023' -import type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' -import { KeyringPair } from '@kiltprotocol/types' +import { decodePair } from '@polkadot/keyring/pair/decode' import { + cryptoWaitReady, encodeAddress, randomAsHex, secp256k1Sign, + sr25519Sign, } from '@polkadot/util-crypto' -import { Keypair } from '@polkadot/util-crypto/types' -import { decodePair } from '@polkadot/keyring/pair/decode' +import type { Keypair } from '@polkadot/util-crypto/types' + +import { + createSigner as es256kSignerWrapped, + cryptosuite as es256kSuite, +} from '@kiltprotocol/ecdsa-secp256k1-jcs-2023' +import { + createSigner as ed25519SignerWrapped, + cryptosuite as ed25519Suite, +} from '@kiltprotocol/eddsa-jcs-2022' +import type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' +import { cryptosuite as sr25519Suite } from '@kiltprotocol/sr25519-jcs-2023' + +import type { DidDocument, KeyringPair } from '@kiltprotocol/types' + +export const ALGORITHMS = Object.freeze({ + ECRECOVER_SECP256K1_BLAKE2B: 'Ecrecover-Secp256k1-Blake2b', // could also be called ES256K-R-Blake2b + ECRECOVER_SECP256K1_KECCAK: 'Ecrecover-Secp256k1-Keccak', // could also be called ES256K-R-Keccak + ES256K: es256kSuite.requiredAlgorithm, + SR25519: sr25519Suite.requiredAlgorithm, + ED25519: ed25519Suite.requiredAlgorithm, +}) + +export const DID_PALLET_SUPPORTED_ALGORITHMS = Object.freeze([ + ALGORITHMS.ED25519, + ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, + ALGORITHMS.SR25519, +]) /** * Signer that produces an ECDSA signature over a Blake2b-256 digest of the message using the secp256k1 curve. * The signature has a recovery bit appended to the end, allowing public key recovery. * * @param root0 - * @param root0.seed * @param root0.keyUri + * @param root0.publicKey + * @param root0.secretKey */ export async function polkadotEcdsaSigner({ - seed, + secretKey, keyUri, }: { - seed: Uint8Array + publicKey?: Uint8Array + secretKey: Uint8Array keyUri: string }): Promise { return { id: keyUri, - algorithm: 'Ecrecover-Secp256k1-Blake2b', // could also be called ES256K-R-Blake2b + algorithm: ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, sign: async ({ data }) => { - return secp256k1Sign(data, { secretKey: seed }, 'blake2') + return secp256k1Sign(data, { secretKey }, 'blake2') }, } } @@ -47,22 +73,94 @@ export async function polkadotEcdsaSigner({ * The signature has a recovery bit appended to the end, allowing public key recovery. * * @param input - * @param input.seed * @param input.keyUri + * @param input.publicKey + * @param input.secretKey * @returns */ export async function ethereumEcdsaSigner({ - seed, + secretKey, keyUri, }: { - seed: Uint8Array + publicKey?: Uint8Array + secretKey: Uint8Array keyUri: string }): Promise { return { id: keyUri, - algorithm: 'Ecrecover-Secp256k1-Keccak', // could also be called ES256K-R-Keccak + algorithm: ALGORITHMS.ECRECOVER_SECP256K1_KECCAK, sign: async ({ data }) => { - return secp256k1Sign(data, { secretKey: seed }, 'keccak') + return secp256k1Sign(data, { secretKey }, 'keccak') + }, + } +} + +/** + * Signer that produces an ES256K signature over the message. + * + * @param input + * @param input.keyUri + * @param input.publicKey + * @param input.secretKey + * @returns + */ +export async function es256kSigner({ + secretKey, + keyUri, +}: { + publicKey?: Uint8Array + secretKey: Uint8Array + keyUri: string +}): Promise { + // only exists to map secretKey to seed + return es256kSignerWrapped({ seed: secretKey, keyUri }) +} + +/** + * Signer that produces an Ed25519 signature over the message. + * + * @param input + * @param input.keyUri + * @param input.publicKey + * @param input.secretKey + * @returns + */ +export async function ed25519Signer({ + secretKey, + keyUri, +}: { + publicKey?: Uint8Array + secretKey: Uint8Array + keyUri: string +}): Promise { + // polkadot ed25519 private keys are a concatenation of private and public key for some reason + return ed25519SignerWrapped({ seed: secretKey.slice(0, 32), keyUri }) +} + +/** + * Signer that produces an Sr25519 signature over the message. + * + * @param input + * @param input.keyUri + * @param input.publicKey + * @param input.secretKey + * @returns + */ +export async function sr25519Signer({ + secretKey, + keyUri, + publicKey, +}: { + publicKey: Uint8Array + secretKey: Uint8Array + keyUri: string +}): Promise { + await cryptoWaitReady() + return { + id: keyUri, + algorithm: ALGORITHMS.SR25519, + sign: async ({ data }: { data: Uint8Array }) => { + return sr25519Sign(data, { secretKey, publicKey }) }, } } @@ -75,11 +173,11 @@ function extractPk(pair: KeyringPair): Uint8Array { } const signerFactory = { - Ed25519: ed25519Signer, - Sr25519: sr25519Signer, - Es256K: es256kSigner, - 'Ecrecover-Secp256k1-Blake2b': polkadotEcdsaSigner, - 'Ecrecover-Secp256k1-Keccak': ethereumEcdsaSigner, + [ALGORITHMS.ED25519]: ed25519Signer, + [ALGORITHMS.SR25519]: sr25519Signer, + [ALGORITHMS.ES256K]: es256kSigner, + [ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B]: polkadotEcdsaSigner, + [ALGORITHMS.ECRECOVER_SECP256K1_KECCAK]: ethereumEcdsaSigner, } /** @@ -98,11 +196,12 @@ export async function signerFromKeypair({ keyUri?: string }): Promise { const makeSigner: (x: { - seed: Uint8Array + secretKey: Uint8Array + publicKey: Uint8Array keyUri: string }) => Promise = signerFactory[algorithm] if (typeof makeSigner !== 'function') { - throw new Error('unknown algorithm') + throw new Error(`unknown algorithm ${algorithm}`) } if (!('secretKey' in keypair) && 'encodePkcs8' in keypair) { @@ -113,7 +212,11 @@ export async function signerFromKeypair({ sign: async (signData) => { // TODO: can probably be optimized; but care must be taken to respect keyring locking const secretKey = extractPk(keypair) - const { sign } = await makeSigner({ seed: secretKey, keyUri: id }) + const { sign } = await makeSigner({ + secretKey, + publicKey: keypair.publicKey, + keyUri: id, + }) return sign(signData) }, } @@ -121,7 +224,8 @@ export async function signerFromKeypair({ const { secretKey, publicKey } = keypair return makeSigner({ - seed: secretKey, + secretKey, + publicKey, keyUri: keyUri ?? encodeAddress(publicKey, 38), }) } @@ -129,15 +233,15 @@ export async function signerFromKeypair({ function algsForKeyType(keyType: string): string[] { switch (keyType.toLowerCase()) { case 'ed25519': - return ['Ed25519'] + return [ALGORITHMS.ED25519] case 'sr25519': - return ['Sr25519'] + return [ALGORITHMS.SR25519] case 'ecdsa': case 'secpk256k1': return [ - 'Ecrecover-Secp256k1-Blake2b', - 'Ecrecover-Secp256k1-Keccak', - 'ES256K', + ALGORITHMS.ES256K, + ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, + ALGORITHMS.ECRECOVER_SECP256K1_KECCAK, ] default: return [] @@ -169,3 +273,84 @@ export async function getSignersForKeypair({ }) ) } + +export interface SignerSelector { + (signer: SignerInterface): boolean +} + +/** + * @param signers + * @param selectors + */ +export async function selectSigners( + signers: readonly SignerInterface[], + ...selectors: readonly SignerSelector[] +): Promise { + return signers.filter((signer) => { + return selectors.every((selector) => selector(signer)) + }) +} + +/** + * @param signers + * @param selectors + */ +export async function selectSigner( + signers: readonly SignerInterface[], + ...selectors: readonly SignerSelector[] +): Promise { + return signers.find((signer) => { + return selectors.every((selector) => selector(signer)) + }) +} + +function byId(id: string): SignerSelector { + return (signer) => signer.id === id +} + +function byAlgorithm(algorithms: readonly string[]): SignerSelector { + return (signer) => + algorithms.some( + (algorithm) => algorithm.toLowerCase() === signer.algorithm.toLowerCase() + ) +} + +function byDid( + didDocument: DidDocument, + { + controller, + verificationRelationship, + }: { verificationRelationship?: string; controller?: string } = {} +): SignerSelector { + return (signer) => { + const vm = didDocument.verificationMethod?.find( + ({ id }) => id === signer.id || didDocument.id + id === signer.id // deal with relative DID URLs as ids + ) + if (!vm) { + return false + } + if (controller && controller !== vm.controller) { + return false + } + if ( + typeof verificationRelationship === 'string' && + didDocument[verificationRelationship]?.some?.( + (id: string) => id === signer.id || didDocument.id + id === signer.id // deal with relative DID URLs as ids + ) !== true + ) { + return false + } + return true + } +} + +function verifiableOnChain(): SignerSelector { + return byAlgorithm(DID_PALLET_SUPPORTED_ALGORITHMS) +} + +export const select = { + byId, + byAlgorithm, + byDid, + verifiableOnChain, +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ba73308f9..72796680f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -16,6 +16,7 @@ export * as UUID from './UUID.js' export * as DataUtils from './DataUtils.js' export * as SDKErrors from './SDKErrors.js' export * as JsonSchema from './json-schema/index.js' +export * as Signers from './Signers.js' export { Caip19, Caip2 } from './CAIP/index.js' export { ss58Format } from './ss58Format.js' export { cbor } from './cbor.js' diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index 704732e70..d2357a6d5 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -27,6 +27,7 @@ import type { ICType, KiltAddress, KiltKeyringPair, + SignerInterface, SubmittableExtrinsic, } from '@kiltprotocol/types' import { Crypto } from '@kiltprotocol/utils' @@ -446,17 +447,11 @@ describe('issuance', () => { let issuanceSuite: KiltAttestationV1Suite let toBeSigned: CredentialStub - const didSigner: KiltAttestationProofV1.DidSigner = { - did: attestedVc.issuer, - signer: async () => ({ - signature: new Uint8Array(32), - verificationMethod: { - controller: attestedVc.issuer, - type: 'Multikey', - id: '#test', - publicKeyMultibase: 'zasd', - }, - }), + const { issuer } = attestedVc + const signer: SignerInterface = { + sign: async () => new Uint8Array(32), + algorithm: 'Sr25519', + id: `${issuer}#1`, } const transactionHandler: KiltAttestationProofV1.TxHandler = { account: attester, @@ -484,7 +479,8 @@ describe('issuance', () => { let newCred: Partial = await issuanceSuite.anchorCredential( { ...toBeSigned }, - didSigner, + issuer, + [signer], transactionHandler ) newCred = await vcjs.issue({ @@ -542,7 +538,8 @@ describe('issuance', () => { { ...toBeSigned, }, - didSigner, + issuer, + [signer], transactionHandler ) newCred = (await vcjs.issue({ diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.ts index 5dab087a1..c5a522467 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.ts @@ -19,7 +19,12 @@ import { Types, KiltCredentialV1, } from '@kiltprotocol/core' -import type { ICType } from '@kiltprotocol/types' +import type { + DidDocument, + Did, + ICType, + SignerInterface, +} from '@kiltprotocol/types' import { Caip2 } from '@kiltprotocol/utils' import type { DocumentLoader, JsonLdObj } from '../documentLoader.js' @@ -198,14 +203,16 @@ export class KiltAttestationV1Suite extends LinkedDataProof { * You can then add a proof about the successful attestation to the credential using `createProof`. * * @param input A partial [[KiltCredentialV1]]; `credentialSubject` is required. - * @param didSigner Signer interface to be passed to [[issue]], containing the attester's `did` and a `signer` callback which authorizes the on-chain anchoring of the credential with the attester's signature. + * @param issuer The DID Document or, alternatively, the DID of the issuer. + * @param signers Signer interfaces to be passed to [[issue]], one of which will be selected to authorize the on-chain anchoring of the credential with the issuer's signature. * @param transactionHandler Transaction handler interface to be passed to [[issue]] containing the submitter `address` that's going to cover the transaction fees as well as either a `signer` or `signAndSubmit` callback handling extrinsic signing and submission. * * @returns A copy of the input updated to fit the [[KiltCredentialV1]] and to align with the attestation record (concerns, e.g., the `issuanceDate` which is set to the block time at which the credential was anchored). */ public async anchorCredential( input: CredentialStub, - didSigner: KiltAttestationProofV1.DidSigner, + issuer: DidDocument | Did, + signers: SignerInterface[], transactionHandler: KiltAttestationProofV1.TxHandler ): Promise> { const { credentialSubject, type } = input @@ -236,8 +243,9 @@ export class KiltAttestationV1Suite extends LinkedDataProof { const { proof, ...credential } = await KiltAttestationProofV1.issue( credentialStub, + issuer, { - didSigner, + signers, transactionHandler, } ) diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 7312b3193..088000917 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -10,10 +10,9 @@ import type { NewDidEncryptionKey } from '@kiltprotocol/did' import type { DidDocument, - KeyringPair, KiltEncryptionKeypair, KiltKeyringPair, - SignCallback, + SignerInterface, } from '@kiltprotocol/types' const { kilt } = window @@ -33,26 +32,6 @@ const { ConfigService.set({ submitTxResolveOn: Blockchain.IS_IN_BLOCK }) -function makeSignCallback( - keypair: KeyringPair -): (didDocument: DidDocument) => SignCallback { - return (didDocument) => { - return async function sign({ data, verificationRelationship }) { - const authKeyId = didDocument[verificationRelationship]?.[0] - const authKey = didDocument.verificationMethod?.find( - ({ id }) => id === authKeyId - ) - if (authKeyId === undefined || authKey === undefined) { - throw new Error( - `No verification method for purpose "${verificationRelationship}" found in DID "${didDocument.id}"` - ) - } - const signature = keypair.sign(data, { withType: false }) - return { signature, verificationMethod: authKey } - } - } -} - type StoreDidCallback = Parameters['2'] function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { @@ -67,21 +46,32 @@ function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { } } -function makeSigningKeypair( +async function makeSigningKeypair( seed: string, type: KiltKeyringPair['type'] = 'sr25519' -): { +): Promise<{ keypair: KiltKeyringPair - getSignCallback: (didDocument: DidDocument) => SignCallback + getSigners: (didDocument: DidDocument) => Promise storeDidCallback: StoreDidCallback -} { +}> { const keypair = Crypto.makeKeypairFromUri(seed, type) - const getSignCallback = makeSignCallback(keypair) + + const getSigners: ( + didDocument: DidDocument + ) => Promise = async (didDocument) => { + return ( + await Promise.all( + didDocument.verificationMethod?.map(({ id }) => + kilt.Utils.Signers.getSignersForKeypair({ keypair, keyUri: id }) + ) ?? [] + ) + ).flat() + } const storeDidCallback = makeStoreDidCallback(keypair) return { keypair, - getSignCallback, + getSigners, storeDidCallback, } } @@ -138,8 +128,8 @@ async function runAll() { 'receive clutch item involve chaos clutch furnace arrest claw isolate okay together' const payer = Crypto.makeKeypairFromUri(FaucetSeed) - const { keypair: aliceKeypair, getSignCallback: aliceSign } = - makeSigningKeypair('//Alice') + const { keypair: aliceKeypair, getSigners: aliceSign } = + await makeSigningKeypair('//Alice') const aliceEncryptionKey = makeEncryptionKeypair('//Alice//enc') const alice = await createFullDidFromKeypair( payer, @@ -150,7 +140,7 @@ async function runAll() { throw new Error('Impossible: alice has no encryptionKey') console.log('alice setup done') - const { keypair: bobKeypair } = makeSigningKeypair('//Bob') + const { keypair: bobKeypair } = await makeSigningKeypair('//Bob') const bobEncryptionKey = makeEncryptionKeypair('//Bob//enc') const bob = await createFullDidFromKeypair( payer, @@ -182,7 +172,7 @@ async function runAll() { // Chain DID workflow -> creation & deletion console.log('DID workflow started') - const { keypair, getSignCallback, storeDidCallback } = makeSigningKeypair( + const { keypair, getSigners, storeDidCallback } = await makeSigningKeypair( '//Foo', 'ed25519' ) @@ -217,7 +207,7 @@ async function runAll() { const deleteTx = await Did.authorizeTx( fullDid.id, api.tx.did.delete(BalanceUtils.toFemtoKilt(0)), - getSignCallback(fullDid), + await getSigners(fullDid), payer.address ) await Blockchain.signAndSubmitTx(deleteTx, payer) @@ -243,7 +233,7 @@ async function runAll() { const cTypeStoreTx = await Did.authorizeTx( alice.id, api.tx.ctype.add(CType.toChain(DriversLicense)), - aliceSign(alice), + await aliceSign(alice), payer.address ) await Blockchain.signAndSubmitTx(cTypeStoreTx, payer) @@ -275,8 +265,8 @@ async function runAll() { throw new Error('Claim content inside Credential mismatching') } - const issued = await KiltAttestationProofV1.issue(credential, { - didSigner: { did: alice.id, signer: aliceSign(alice) }, + const issued = await KiltAttestationProofV1.issue(credential, alice.id, { + signers: await aliceSign(alice), transactionHandler: { account: payer.address, signAndSubmit: async (tx) => { diff --git a/tests/integration/AccountLinking.spec.ts b/tests/integration/AccountLinking.spec.ts index 61badec16..8decfb193 100644 --- a/tests/integration/AccountLinking.spec.ts +++ b/tests/integration/AccountLinking.spec.ts @@ -48,8 +48,8 @@ describe('When there is an on-chain DID', () => { describe('and a tx sender willing to link its account', () => { beforeAll(async () => { - didKey = makeSigningKeyTool() - newDidKey = makeSigningKeyTool() + didKey = await makeSigningKeyTool() + newDidKey = await makeSigningKeyTool() did = await createFullDidFromSeed(paymentAccount, didKey.keypair) newDid = await createFullDidFromSeed(paymentAccount, newDidKey.keypair) }, 40_000) @@ -67,7 +67,7 @@ describe('When there is an on-chain DID', () => { const signedTx = await Did.authorizeTx( did.id, associateSenderTx, - didKey.getSignCallback(did), + await didKey.getSigners(did), paymentAccount.address ) const balanceBefore = ( @@ -99,7 +99,7 @@ describe('When there is an on-chain DID', () => { const signedTx = await Did.authorizeTx( newDid.id, associateSenderTx, - newDidKey.getSignCallback(newDid), + await newDidKey.getSigners(newDid), paymentAccount.address ) const balanceBefore = ( @@ -159,11 +159,13 @@ describe('When there is an on-chain DID', () => { skip = true return } - const keyTool = makeSigningKeyTool(keyType as KiltKeyringPair['type']) + const keyTool = await makeSigningKeyTool( + keyType as KiltKeyringPair['type'] + ) keypair = keyTool.keypair keypairChain = Did.accountToChain(keypair.address) - didKey = makeSigningKeyTool() - newDidKey = makeSigningKeyTool() + didKey = await makeSigningKeyTool() + newDidKey = await makeSigningKeyTool() did = await createFullDidFromSeed(paymentAccount, didKey.keypair) newDid = await createFullDidFromSeed(paymentAccount, newDidKey.keypair) }, 40_000) @@ -180,7 +182,7 @@ describe('When there is an on-chain DID', () => { const signedTx = await Did.authorizeTx( did.id, api.tx.didLookup.associateAccount(...args), - didKey.getSignCallback(did), + await didKey.getSigners(did), paymentAccount.address ) const balanceBefore = ( @@ -218,7 +220,7 @@ describe('When there is an on-chain DID', () => { const signedTx = await Did.authorizeTx( newDid.id, api.tx.didLookup.associateAccount(...args), - newDidKey.getSignCallback(newDid), + await newDidKey.getSigners(newDid), paymentAccount.address ) const balanceBefore = ( @@ -250,7 +252,7 @@ describe('When there is an on-chain DID', () => { const signedTx = await Did.authorizeTx( newDid.id, removeLinkTx, - newDidKey.getSignCallback(newDid), + await newDidKey.getSigners(newDid), paymentAccount.address ) const balanceBefore = ( @@ -290,8 +292,8 @@ describe('When there is an on-chain DID', () => { genericAccount.address, BalanceUtils.convertToTxUnit(new BN(10), 1) ) - didKey = makeSigningKeyTool() - newDidKey = makeSigningKeyTool() + didKey = await makeSigningKeyTool() + newDidKey = await makeSigningKeyTool() did = await createFullDidFromSeed(paymentAccount, didKey.keypair) newDid = await createFullDidFromSeed(paymentAccount, newDidKey.keypair) }, 40_000) @@ -305,7 +307,7 @@ describe('When there is an on-chain DID', () => { const signedTx = await Did.authorizeTx( did.id, api.tx.didLookup.associateAccount(...args), - didKey.getSignCallback(did), + await didKey.getSigners(did), paymentAccount.address ) const balanceBefore = ( @@ -338,7 +340,7 @@ describe('When there is an on-chain DID', () => { const signedTx = await Did.authorizeTx( did.id, web3NameClaimTx, - didKey.getSignCallback(did), + await didKey.getSigners(did), paymentAccount.address ) await submitTx(signedTx, paymentAccount) diff --git a/tests/integration/Attestation.spec.ts b/tests/integration/Attestation.spec.ts index ed2340558..b024a2719 100644 --- a/tests/integration/Attestation.spec.ts +++ b/tests/integration/Attestation.spec.ts @@ -48,9 +48,9 @@ beforeAll(async () => { beforeAll(async () => { tokenHolder = await createEndowedTestAccount() - attesterKey = makeSigningKeyTool() - anotherAttesterKey = makeSigningKeyTool() - claimerKey = makeSigningKeyTool() + attesterKey = await makeSigningKeyTool() + anotherAttesterKey = await makeSigningKeyTool() + claimerKey = await makeSigningKeyTool() attester = await createFullDidFromSeed(tokenHolder, attesterKey.keypair) anotherAttester = await createFullDidFromSeed( tokenHolder, @@ -79,7 +79,7 @@ describe('handling attestations that do not exist', () => { const authorized = await Did.authorizeTx( attester.id, draft, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await expect(submitTx(authorized, tokenHolder)).rejects.toMatchObject({ @@ -93,7 +93,7 @@ describe('handling attestations that do not exist', () => { const authorized = await Did.authorizeTx( attester.id, draft, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await expect(submitTx(authorized, tokenHolder)).rejects.toMatchObject({ @@ -110,7 +110,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const tx = await Did.authorizeTx( attester.id, api.tx.ctype.add(CType.toChain(driversLicenseCType)), - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(tx, tokenHolder) @@ -126,7 +126,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const credential = Credential.fromClaim(claim) const presentation = await Credential.createPresentation({ credential, - signCallback: claimerKey.getSignCallback(claimer), + signers: await claimerKey.getSigners(claimer), }) expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( @@ -148,7 +148,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const presentation = await Credential.createPresentation({ credential, - signCallback: claimerKey.getSignCallback(claimer), + signers: await claimerKey.getSigners(claimer), }) await expect( Credential.verifySignature(presentation) @@ -167,7 +167,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedStoreTx, tokenHolder) @@ -209,7 +209,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const presentation = await Credential.createPresentation({ credential, - signCallback: claimerKey.getSignCallback(claimer), + signers: await claimerKey.getSigners(claimer), }) await expect( Credential.verifySignature(presentation) @@ -219,7 +219,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { presentation, attester.id ) - const { keypair, getSignCallback } = makeSigningKeyTool() + const { keypair, getSigners } = await makeSigningKeyTool() const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -229,7 +229,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - getSignCallback(attester), + await getSigners(attester), keypair.address ) await expect( @@ -268,7 +268,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) @@ -294,7 +294,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { credential = Credential.fromClaim(claim) const presentation = await Credential.createPresentation({ credential, - signCallback: claimerKey.getSignCallback(claimer), + signers: await claimerKey.getSigners(claimer), }) attestation = Attestation.fromCredentialAndDid(credential, attester.id) const storeTx = api.tx.attestation.add( @@ -305,7 +305,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedStoreTx, tokenHolder) @@ -328,7 +328,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) @@ -348,10 +348,6 @@ describe('When there is an attester, claimer and ctype drivers license', () => { claimer.id ) const fakeCredential = Credential.fromClaim(claim) - await Credential.createPresentation({ - credential, - signCallback: claimerKey.getSignCallback(claimer), - }) expect(() => Credential.verifyAgainstAttestation(attestation, fakeCredential) @@ -363,7 +359,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedRevokeTx = await Did.authorizeTx( claimer.id, revokeTx, - claimerKey.getSignCallback(claimer), + await claimerKey.getSigners(claimer), tokenHolder.address ) @@ -393,7 +389,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedRevokeTx = await Did.authorizeTx( attester.id, revokeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedRevokeTx, tokenHolder) @@ -415,7 +411,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedRemoveTx = await Did.authorizeTx( attester.id, removeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedRemoveTx, tokenHolder) @@ -444,7 +440,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedStoreTx, tokenHolder) @@ -463,10 +459,6 @@ describe('When there is an attester, claimer and ctype drivers license', () => { attester.id ) const credential1 = Credential.fromClaim(licenseAuthorization) - await Credential.createPresentation({ - credential: credential1, - signCallback: claimerKey.getSignCallback(claimer), - }) const licenseAuthorizationGranted = Attestation.fromCredentialAndDid( credential1, anotherAttester.id @@ -479,7 +471,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx = await Did.authorizeTx( anotherAttester.id, storeTx, - anotherAttesterKey.getSignCallback(anotherAttester), + await anotherAttesterKey.getSigners(anotherAttester), tokenHolder.address ) await submitTx(authorizedStoreTx, tokenHolder) @@ -493,10 +485,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const credential2 = Credential.fromClaim(iBelieveICanDrive, { legitimations: [credential1], }) - await Credential.createPresentation({ - credential: credential2, - signCallback: claimerKey.getSignCallback(claimer), - }) + const licenseGranted = Attestation.fromCredentialAndDid( credential2, attester.id @@ -509,7 +498,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const authorizedStoreTx2 = await Did.authorizeTx( attester.id, storeTx2, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedStoreTx2, tokenHolder) @@ -540,6 +529,15 @@ describe('When there is an attester, claimer and ctype drivers license', () => { credential1 ) ).not.toThrow() + + const presentation = await Credential.createPresentation({ + credential: credential2, + signers: await claimerKey.getSigners(claimer), + }) + + await expect( + Credential.verifyPresentation(presentation) + ).resolves.toMatchObject({ revoked: false }) }, 70_000) }) }) diff --git a/tests/integration/Balance.spec.ts b/tests/integration/Balance.spec.ts index c05d58cc0..be275c138 100644 --- a/tests/integration/Balance.spec.ts +++ b/tests/integration/Balance.spec.ts @@ -88,10 +88,10 @@ describe('When there are haves and have-nots', () => { let faucet: KeyringPair beforeAll(async () => { - bobbyBroke = makeSigningKeyTool().keypair + bobbyBroke = (await makeSigningKeyTool()).keypair richieRich = devAlice faucet = devFaucet - stormyD = makeSigningKeyTool().keypair + stormyD = (await makeSigningKeyTool()).keypair }) it('can transfer tokens from the rich to the poor', async () => { diff --git a/tests/integration/Blockchain.spec.ts b/tests/integration/Blockchain.spec.ts index d54729eb0..b8eb5adf9 100644 --- a/tests/integration/Blockchain.spec.ts +++ b/tests/integration/Blockchain.spec.ts @@ -26,7 +26,7 @@ describe('Chain returns specific errors, that we check for', () => { let charlie: KeyringPair beforeAll(async () => { faucet = devFaucet - testIdentity = makeSigningKeyTool().keypair + testIdentity = (await makeSigningKeyTool()).keypair charlie = devCharlie const transferTx = api.tx.balances.transfer( diff --git a/tests/integration/Ctypes.spec.ts b/tests/integration/Ctypes.spec.ts index 52fdea9b2..2c88584a2 100644 --- a/tests/integration/Ctypes.spec.ts +++ b/tests/integration/Ctypes.spec.ts @@ -46,18 +46,18 @@ describe('When there is an CtypeCreator and a verifier', () => { beforeAll(async () => { paymentAccount = await createEndowedTestAccount() - key = makeSigningKeyTool() + key = await makeSigningKeyTool() ctypeCreator = await createFullDidFromSeed(paymentAccount, key.keypair) }, 60_000) it('should not be possible to create a claim type w/o tokens', async () => { const cType = makeCType() - const { keypair, getSignCallback } = makeSigningKeyTool() + const { keypair, getSigners } = await makeSigningKeyTool() const storeTx = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx = await Did.authorizeTx( ctypeCreator.id, storeTx, - getSignCallback(ctypeCreator), + await getSigners(ctypeCreator), keypair.address ) await expect(submitTx(authorizedStoreTx, keypair)).rejects.toThrowError() @@ -73,7 +73,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const authorizedStoreTx = await Did.authorizeTx( ctypeCreator.id, storeTx, - key.getSignCallback(ctypeCreator), + await key.getSigners(ctypeCreator), paymentAccount.address ) await submitTx(authorizedStoreTx, paymentAccount) @@ -94,7 +94,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const authorizedStoreTx = await Did.authorizeTx( ctypeCreator.id, storeTx, - key.getSignCallback(ctypeCreator), + await key.getSigners(ctypeCreator), paymentAccount.address ) await submitTx(authorizedStoreTx, paymentAccount) @@ -103,7 +103,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const authorizedStoreTx2 = await Did.authorizeTx( ctypeCreator.id, storeTx2, - key.getSignCallback(ctypeCreator), + await key.getSigners(ctypeCreator), paymentAccount.address ) await expect( diff --git a/tests/integration/Delegation.spec.ts b/tests/integration/Delegation.spec.ts index ecc1efd7d..773ec86c7 100644 --- a/tests/integration/Delegation.spec.ts +++ b/tests/integration/Delegation.spec.ts @@ -21,7 +21,7 @@ import type { ICType, IDelegationNode, KiltKeyringPair, - SignCallback, + SignerInterface, } from '@kiltprotocol/types' import { Permission, PermissionType } from '@kiltprotocol/types' @@ -54,7 +54,7 @@ let attesterKey: KeyTool async function writeHierarchy( delegator: DidDocument, cTypeId: ICType['$id'], - sign: SignCallback + signers: SignerInterface[] ): Promise { const rootNode = DelegationNode.newRoot({ account: delegator.id, @@ -66,7 +66,7 @@ async function writeHierarchy( const authorizedStoreTx = await Did.authorizeTx( delegator.id, storeTx, - sign, + signers, paymentAccount.address ) await submitTx(authorizedStoreTx, paymentAccount) @@ -79,8 +79,8 @@ async function addDelegation( parentId: DelegationNode['id'], delegator: DidDocument, delegate: DidDocument, - delegatorSign: SignCallback, - delegateSign: SignCallback, + delegatorSign: SignerInterface[], + delegateSign: SignerInterface[], permissions: PermissionType[] = [Permission.ATTEST, Permission.DELEGATE] ): Promise { const delegationNode = DelegationNode.newNode({ @@ -107,9 +107,9 @@ beforeAll(async () => { beforeAll(async () => { paymentAccount = await createEndowedTestAccount() - rootKey = makeSigningKeyTool() - claimerKey = makeSigningKeyTool() - attesterKey = makeSigningKeyTool() + rootKey = await makeSigningKeyTool() + claimerKey = await makeSigningKeyTool() + attesterKey = await makeSigningKeyTool() attester = await createFullDidFromSeed(paymentAccount, attesterKey.keypair) root = await createFullDidFromSeed(paymentAccount, rootKey.keypair) claimer = await createFullDidFromSeed(paymentAccount, claimerKey.keypair) @@ -120,7 +120,7 @@ beforeAll(async () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), paymentAccount.address ) await submitTx(authorizedStoreTx, paymentAccount) @@ -135,15 +135,15 @@ it('should be possible to delegate attestation rights', async () => { const rootNode = await writeHierarchy( root, driversLicenseCType.$id, - rootKey.getSignCallback(root) + await rootKey.getSigners(root) ) const delegatedNode = await addDelegation( rootNode.id, rootNode.id, root, attester, - rootKey.getSignCallback(root), - attesterKey.getSignCallback(attester) + await rootKey.getSigners(root), + await attesterKey.getSigners(attester) ) await expect(rootNode.verify()).resolves.not.toThrow() await expect(delegatedNode.verify()).resolves.not.toThrow() @@ -157,15 +157,15 @@ describe('and attestation rights have been delegated', () => { rootNode = await writeHierarchy( root, driversLicenseCType.$id, - rootKey.getSignCallback(root) + await rootKey.getSigners(root) ) delegatedNode = await addDelegation( rootNode.id, rootNode.id, root, attester, - rootKey.getSignCallback(root), - attesterKey.getSignCallback(attester) + await rootKey.getSigners(root), + await attesterKey.getSigners(attester) ) await expect(rootNode.verify()).resolves.not.toThrow() @@ -187,7 +187,7 @@ describe('and attestation rights have been delegated', () => { }) const presentation = await Credential.createPresentation({ credential, - signCallback: claimerKey.getSignCallback(claimer), + signers: await claimerKey.getSigners(claimer), }) expect(() => Credential.verifyDataIntegrity(credential)).not.toThrow() await expect( @@ -207,7 +207,7 @@ describe('and attestation rights have been delegated', () => { const authorizedStoreTx = await Did.authorizeTx( attester.id, storeTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), paymentAccount.address ) await submitTx(authorizedStoreTx, paymentAccount) @@ -226,7 +226,7 @@ describe('and attestation rights have been delegated', () => { const authorizedStoreTx2 = await Did.authorizeTx( root.id, revokeTx, - rootKey.getSignCallback(root), + await rootKey.getSigners(root), paymentAccount.address ) await submitTx(authorizedStoreTx2, paymentAccount) @@ -242,19 +242,19 @@ describe('and attestation rights have been delegated', () => { describe('revocation', () => { let delegator: DidDocument - let delegatorSign: SignCallback + let delegatorSign: SignerInterface[] let firstDelegate: DidDocument - let firstDelegateSign: SignCallback + let firstDelegateSign: SignerInterface[] let secondDelegate: DidDocument - let secondDelegateSign: SignCallback + let secondDelegateSign: SignerInterface[] - beforeAll(() => { + beforeAll(async () => { delegator = root - delegatorSign = rootKey.getSignCallback(root) + delegatorSign = await rootKey.getSigners(root) firstDelegate = attester - firstDelegateSign = attesterKey.getSignCallback(attester) + firstDelegateSign = await attesterKey.getSigners(attester) secondDelegate = claimer - secondDelegateSign = claimerKey.getSignCallback(claimer) + secondDelegateSign = await claimerKey.getSigners(claimer) }) it('delegator can revoke but not remove delegation', async () => { @@ -389,23 +389,23 @@ describe('Deposit claiming', () => { const rootNode = await writeHierarchy( root, driversLicenseCType.$id, - rootKey.getSignCallback(root) + await rootKey.getSigners(root) ) const delegatedNode = await addDelegation( rootNode.id, rootNode.id, root, root, - rootKey.getSignCallback(root), - rootKey.getSignCallback(root) + await rootKey.getSigners(root), + await rootKey.getSigners(root) ) const subDelegatedNode = await addDelegation( rootNode.id, delegatedNode.id, root, root, - rootKey.getSignCallback(root), - rootKey.getSignCallback(root) + await rootKey.getSigners(root), + await rootKey.getSigners(root) ) expect(await DelegationNode.fetch(delegatedNode.id)).not.toBeNull() @@ -449,15 +449,15 @@ describe('hierarchyDetails', () => { const rootNode = await writeHierarchy( root, driversLicenseCType.$id, - rootKey.getSignCallback(root) + await rootKey.getSigners(root) ) const delegatedNode = await addDelegation( rootNode.id, rootNode.id, root, attester, - rootKey.getSignCallback(root), - attesterKey.getSignCallback(attester) + await rootKey.getSigners(root), + await attesterKey.getSigners(attester) ) const details = await delegatedNode.getHierarchyDetails() diff --git a/tests/integration/Deposit.spec.ts b/tests/integration/Deposit.spec.ts index fbfa20d60..00d84feb3 100644 --- a/tests/integration/Deposit.spec.ts +++ b/tests/integration/Deposit.spec.ts @@ -17,7 +17,7 @@ import type { ICredential, KeyringPair, KiltKeyringPair, - SignCallback, + SignerInterface, SubmittableExtrinsic, } from '@kiltprotocol/types' import { @@ -45,7 +45,7 @@ let storedEndpointsCount: BN async function checkDeleteFullDid( identity: KiltKeyringPair, fullDid: DidDocument, - sign: SignCallback + sign: SignerInterface[] ): Promise { storedEndpointsCount = await api.query.did.didEndpointsCount( Did.toChain(fullDid.id) @@ -106,7 +106,7 @@ async function checkReclaimFullDid( async function checkRemoveFullDidAttestation( identity: KiltKeyringPair, fullDid: DidDocument, - sign: SignCallback, + sign: SignerInterface[], credential: ICredential ): Promise { attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) @@ -149,7 +149,7 @@ async function checkRemoveFullDidAttestation( async function checkReclaimFullDidAttestation( identity: KiltKeyringPair, fullDid: DidDocument, - sign: SignCallback, + sign: SignerInterface[], credential: ICredential ): Promise { attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) @@ -191,7 +191,7 @@ async function checkReclaimFullDidAttestation( async function checkDeletedDidReclaimAttestation( identity: KiltKeyringPair, fullDid: DidDocument, - sign: SignCallback, + sign: SignerInterface[], credential: ICredential ): Promise { attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) @@ -224,7 +224,7 @@ async function checkDeletedDidReclaimAttestation( async function checkWeb3Deposit( identity: KiltKeyringPair, fullDid: DidDocument, - sign: SignCallback + sign: SignerInterface[] ): Promise { const web3Name = 'test-web3name' const balanceBeforeClaiming = ( @@ -278,18 +278,20 @@ beforeAll(async () => { }, 30_000) beforeAll(async () => { - keys = new Array(10).fill(0).map(() => makeSigningKeyTool()) + keys = await Promise.all( + new Array(10).fill(0).map(() => makeSigningKeyTool()) + ) const testAddresses = keys.map((val) => val.keypair.address) await endowAccounts(devFaucet, testAddresses) - const claimer = makeSigningKeyTool() + const claimer = await makeSigningKeyTool() const claimerLightDid = await createMinimalLightDidFromKeypair( claimer.keypair ) - const attesterKey = makeSigningKeyTool() + const attesterKey = await makeSigningKeyTool() const attester = await createFullDidFromSeed(devFaucet, attesterKey.keypair) const ctypeExists = await isCtypeOnChain(driversLicenseCType) @@ -297,7 +299,7 @@ beforeAll(async () => { const extrinsic = await Did.authorizeTx( attester.id, api.tx.ctype.add(CType.toChain(driversLicenseCType)), - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), devFaucet.address ) await submitTx(extrinsic, devFaucet) @@ -317,7 +319,7 @@ beforeAll(async () => { credential = Credential.fromClaim(claim) await Credential.createPresentation({ credential, - signCallback: claimer.getSignCallback(claimerLightDid), + signers: await claimer.getSigners(claimerLightDid), }) }, 120_000) @@ -392,7 +394,7 @@ describe('Different deposits scenarios', () => { await checkDeleteFullDid( keys[0].keypair, testFullDidOne, - keys[0].getSignCallback(testFullDidOne) + await keys[0].getSigners(testFullDidOne) ) ).toBe(true) }, 45_000) @@ -406,7 +408,7 @@ describe('Different deposits scenarios', () => { await checkRemoveFullDidAttestation( keys[2].keypair, testFullDidThree, - keys[2].getSignCallback(testFullDidThree), + await keys[2].getSigners(testFullDidThree), credential ) ).toBe(true) @@ -416,7 +418,7 @@ describe('Different deposits scenarios', () => { await checkReclaimFullDidAttestation( keys[3].keypair, testFullDidFour, - keys[3].getSignCallback(testFullDidFour), + await keys[3].getSigners(testFullDidFour), credential ) ).toBe(true) @@ -426,7 +428,7 @@ describe('Different deposits scenarios', () => { await checkDeleteFullDid( keys[4].keypair, testFullDidFive, - keys[4].getSignCallback(testFullDidFive) + await keys[4].getSigners(testFullDidFive) ) ).toBe(true) }, 90_000) @@ -440,7 +442,7 @@ describe('Different deposits scenarios', () => { await checkRemoveFullDidAttestation( keys[6].keypair, testFullDidSeven, - keys[6].getSignCallback(testFullDidSeven), + await keys[6].getSigners(testFullDidSeven), credential ) ).toBe(true) @@ -450,7 +452,7 @@ describe('Different deposits scenarios', () => { await checkReclaimFullDidAttestation( keys[7].keypair, testFullDidEight, - keys[7].getSignCallback(testFullDidEight), + await keys[7].getSigners(testFullDidEight), credential ) ).toBe(true) @@ -459,7 +461,7 @@ describe('Different deposits scenarios', () => { await checkDeletedDidReclaimAttestation( keys[8].keypair, testFullDidNine, - keys[8].getSignCallback(testFullDidNine), + await keys[8].getSigners(testFullDidNine), credential ) }, 120_000) @@ -468,7 +470,7 @@ describe('Different deposits scenarios', () => { await checkWeb3Deposit( keys[9].keypair, testFullDidTen, - keys[9].getSignCallback(testFullDidTen) + await keys[9].getSigners(testFullDidTen) ) ).toBe(true) }, 120_000) diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index f4499c318..3bb15547e 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -11,7 +11,7 @@ import type { KiltKeyringPair, ResolutionResult, Service, - SignCallback, + SignerInterface, VerificationMethod, } from '@kiltprotocol/types' @@ -51,12 +51,12 @@ beforeAll(async () => { describe('write and didDeleteTx', () => { let did: DidDocument let key: KeyTool - let signCallback: SignCallback + let signers: SignerInterface[] beforeAll(async () => { - key = makeSigningKeyTool() + key = await makeSigningKeyTool() did = await createMinimalLightDidFromKeypair(key.keypair) - signCallback = key.getSignCallback(did) + signers = await key.getSigners(did) }) it('fails to create a new DID on chain with a different submitter than the one in the creation operation', async () => { @@ -111,7 +111,16 @@ describe('write and didDeleteTx', () => { const { document: fullDidDocument } = Did.linkedInfoFromChain(fullDidLinkedInfo) - expect(fullDidDocument).toMatchObject(>{ + // this is to make sure we have signers for the full DID available (same keys, but different id) + signers.push( + ...signers.map(({ algorithm, sign }) => ({ + id: fullDidDocument.id + fullDidDocument.authentication?.[0], + algorithm, + sign, + })) + ) + + expect(fullDidDocument).toMatchObject({ id: fullDid, service: [ { @@ -144,7 +153,9 @@ describe('write and didDeleteTx', () => { }, 60_000) it('should return no results for empty accounts', async () => { - const emptyDid = Did.getFullDid(makeSigningKeyTool().keypair.address) + const emptyDid = Did.getFullDid( + (await makeSigningKeyTool()).keypair.address + ) const encodedDid = Did.toChain(emptyDid) expect((await api.call.did.query(encodedDid)).isSome).toBe(false) @@ -166,7 +177,7 @@ describe('write and didDeleteTx', () => { let submittable = await Did.authorizeTx( fullDid.id, call, - signCallback, + signers, // Use a different account than the submitter one otherAccount.address ) @@ -182,7 +193,7 @@ describe('write and didDeleteTx', () => { submittable = await Did.authorizeTx( fullDid.id, call, - signCallback, + signers, paymentAccount.address ) @@ -213,7 +224,7 @@ describe('write and didDeleteTx', () => { const submittable = await Did.authorizeTx( fullDid.id, call, - signCallback, + signers, paymentAccount.address ) @@ -230,7 +241,7 @@ describe('write and didDeleteTx', () => { }) it('creates and updates DID, and then reclaims the deposit back', async () => { - const { keypair, getSignCallback, storeDidCallback } = makeSigningKeyTool() + const { keypair, getSigners, storeDidCallback } = await makeSigningKeyTool() const newDid = await createMinimalLightDidFromKeypair(keypair) const tx = await getStoreTxFromDidDocument( @@ -247,7 +258,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { ) let { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - const newKey = makeSigningKeyTool() + const newKey = await makeSigningKeyTool() const updateAuthenticationKeyCall = api.tx.did.setAuthenticationKey( Did.publicKeyToChain(newKey.authentication[0]) @@ -255,7 +266,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const tx2 = await Did.authorizeTx( fullDid.id, updateAuthenticationKeyCall, - getSignCallback(fullDid), + await getSigners(fullDid), paymentAccount.address ) await submitTx(tx2, paymentAccount) @@ -280,7 +291,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const tx3 = await Did.authorizeTx( fullDid.id, updateEndpointCall, - newKey.getSignCallback(fullDid), + await newKey.getSigners(fullDid), paymentAccount.address ) await submitTx(tx3, paymentAccount) @@ -300,7 +311,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const tx4 = await Did.authorizeTx( fullDid.id, removeEndpointCall, - newKey.getSignCallback(fullDid), + await newKey.getSigners(fullDid), paymentAccount.address ) await submitTx(tx4, paymentAccount) @@ -326,7 +337,9 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { describe('DID migration', () => { it('migrates light DID with ed25519 auth key and encryption key', async () => { - const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') + const { storeDidCallback, authentication } = await makeSigningKeyTool( + 'ed25519' + ) const { keyAgreement } = makeEncryptionKeyTool( '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ) @@ -387,7 +400,7 @@ describe('DID migration', () => { }) it('migrates light DID with sr25519 auth key', async () => { - const { authentication, storeDidCallback } = makeSigningKeyTool() + const { authentication, storeDidCallback } = await makeSigningKeyTool() const lightDid = Did.createLightDidDocument({ authentication, }) @@ -438,7 +451,9 @@ describe('DID migration', () => { }) it('migrates light DID with ed25519 auth key, encryption key, and services', async () => { - const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') + const { storeDidCallback, authentication } = await makeSigningKeyTool( + 'ed25519' + ) const { keyAgreement } = makeEncryptionKeyTool( '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' ) @@ -530,10 +545,12 @@ describe('DID migration', () => { describe('DID authorization', () => { // Light DIDs cannot authorize extrinsics let did: DidDocument - const { getSignCallback, storeDidCallback, authentication } = - makeSigningKeyTool('ed25519') + let signers: SignerInterface[] beforeAll(async () => { + const { getSigners, storeDidCallback, authentication } = + await makeSigningKeyTool('ed25519') + const createTx = await Did.getStoreTx( { authentication, @@ -552,6 +569,7 @@ describe('DID authorization', () => { ) ) did = Did.linkedInfoFromChain(didLinkedInfo).document + signers = await getSigners(did) }, 60_000) it('authorizes ctype creation with DID signature', async () => { @@ -560,7 +578,7 @@ describe('DID authorization', () => { const tx = await Did.authorizeTx( did.id, call, - getSignCallback(did), + signers, paymentAccount.address ) await submitTx(tx, paymentAccount) @@ -577,7 +595,7 @@ describe('DID authorization', () => { const tx = await Did.authorizeTx( did.id, deleteCall, - getSignCallback(did), + signers, paymentAccount.address ) await submitTx(tx, paymentAccount) @@ -585,9 +603,9 @@ describe('DID authorization', () => { const cType = CType.fromProperties(UUID.generate(), {}) const call = api.tx.ctype.add(CType.toChain(cType)) const tx2 = await Did.authorizeTx( - did.id, + did, // this is to trick the signer into signing the tx although the DID has been deactivated call, - getSignCallback(did), + signers, paymentAccount.address ) await expect(submitTx(tx2, paymentAccount)).rejects.toMatchObject({ @@ -602,7 +620,7 @@ describe('DID authorization', () => { describe('DID management batching', () => { describe('FullDidCreationBuilder', () => { it('Build a complete full DID', async () => { - const { storeDidCallback, authentication } = makeSigningKeyTool() + const { storeDidCallback, authentication } = await makeSigningKeyTool() const extrinsic = await Did.getStoreTx( { authentication, @@ -745,7 +763,7 @@ describe('DID management batching', () => { }) it('Build a minimal full DID with an Ecdsa key', async () => { - const { keypair, storeDidCallback } = makeSigningKeyTool('ecdsa') + const { keypair, storeDidCallback } = await makeSigningKeyTool('ecdsa') const didAuthKey: Did.NewDidVerificationKey = { publicKey: keypair.publicKey, type: 'ecdsa', @@ -787,8 +805,8 @@ describe('DID management batching', () => { describe('FullDidUpdateBuilder', () => { it('Build from a complete full DID and remove everything but the authentication key', async () => { - const { keypair, getSignCallback, storeDidCallback, authentication } = - makeSigningKeyTool() + const { keypair, getSigners, storeDidCallback, authentication } = + await makeSigningKeyTool() const createTx = await Did.getStoreTx( { @@ -870,7 +888,7 @@ describe('DID management batching', () => { api.tx.did.removeServiceEndpoint('id-1'), api.tx.did.removeServiceEndpoint('id-2'), ], - sign: getSignCallback(initialFullDid), + signers: await getSigners(initialFullDid), submitter: paymentAccount.address, }) await submitTx(extrinsic, paymentAccount) @@ -904,11 +922,11 @@ describe('DID management batching', () => { }, 40_000) it('Correctly handles rotation of the authentication key', async () => { - const { authentication, getSignCallback, storeDidCallback } = - makeSigningKeyTool() + const { authentication, getSigners, storeDidCallback } = + await makeSigningKeyTool() const { authentication: [newAuthKey], - } = makeSigningKeyTool('ed25519') + } = await makeSigningKeyTool('ed25519') const createTx = await Did.getStoreTx( { authentication }, @@ -948,7 +966,7 @@ describe('DID management batching', () => { }) ), ], - sign: getSignCallback(initialFullDid), + signers: await getSigners(initialFullDid), submitter: paymentAccount.address, }) @@ -995,8 +1013,8 @@ describe('DID management batching', () => { }, 40_000) it('simple `batch` succeeds despite failures of some extrinsics', async () => { - const { authentication, getSignCallback, storeDidCallback } = - makeSigningKeyTool() + const { authentication, getSigners, storeDidCallback } = + await makeSigningKeyTool() const tx = await Did.getStoreTx( { authentication, @@ -1038,7 +1056,7 @@ describe('DID management batching', () => { }) ), ], - sign: getSignCallback(fullDid), + signers: await getSigners(fullDid), submitter: paymentAccount.address, }) // Now the second operation fails but the batch succeeds @@ -1080,8 +1098,8 @@ describe('DID management batching', () => { }, 60_000) it('batchAll fails if any extrinsics fails', async () => { - const { authentication, getSignCallback, storeDidCallback } = - makeSigningKeyTool() + const { authentication, getSigners, storeDidCallback } = + await makeSigningKeyTool() const createTx = await Did.getStoreTx( { authentication, @@ -1122,7 +1140,7 @@ describe('DID management batching', () => { }) ), ], - sign: getSignCallback(fullDid), + signers: await getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1157,7 +1175,7 @@ describe('DID extrinsics batching', () => { let key: KeyTool beforeAll(async () => { - key = makeSigningKeyTool() + key = await makeSigningKeyTool() fullDid = await createFullDidFromSeed(paymentAccount, key.keypair) }, 50_000) @@ -1180,7 +1198,7 @@ describe('DID extrinsics batching', () => { delegationRevocationTx, delegationStoreTx, ], - sign: key.getSignCallback(fullDid), + signers: await key.getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1210,7 +1228,7 @@ describe('DID extrinsics batching', () => { delegationRevocationTx, delegationStoreTx, ], - sign: key.getSignCallback(fullDid), + signers: await key.getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1229,7 +1247,7 @@ describe('DID extrinsics batching', () => { const authorizedTx = await Did.authorizeTx( fullDid.id, web3NameClaimTx, - key.getSignCallback(fullDid), + await key.getSigners(fullDid), paymentAccount.address ) await submitTx(authorizedTx, paymentAccount) @@ -1240,7 +1258,7 @@ describe('DID extrinsics batching', () => { batchFunction: api.tx.utility.batch, did: fullDid.id, extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], - sign: key.getSignCallback(fullDid), + signers: await key.getSigners(fullDid), submitter: paymentAccount.address, }) await submitTx(tx, paymentAccount) @@ -1288,7 +1306,7 @@ describe('DID extrinsics batching', () => { ctype2Creation, delegationHierarchyRemoval, ], - sign: key.getSignCallback(fullDid), + signers: await key.getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1315,13 +1333,14 @@ describe('DID extrinsics batching', () => { describe('Runtime constraints', () => { let testAuthKey: Did.NewDidVerificationKey - const { keypair, storeDidCallback } = makeSigningKeyTool('ed25519') - + let storeDidCallback: Did.GetStoreTxSignCallback beforeAll(async () => { + const tool = await makeSigningKeyTool('ed25519') testAuthKey = { - publicKey: keypair.publicKey, + publicKey: tool.keypair.publicKey, type: 'ed25519', } + storeDidCallback = tool.storeDidCallback }) describe('DID creation', () => { it('should not be possible to create a DID with too many encryption keys', async () => { diff --git a/tests/integration/ErrorHandler.spec.ts b/tests/integration/ErrorHandler.spec.ts index b440c7821..ff055653e 100644 --- a/tests/integration/ErrorHandler.spec.ts +++ b/tests/integration/ErrorHandler.spec.ts @@ -39,7 +39,7 @@ beforeAll(async () => { beforeAll(async () => { paymentAccount = await createEndowedTestAccount() - key = makeSigningKeyTool() + key = await makeSigningKeyTool() someDid = await createFullDidFromSeed(paymentAccount, key.keypair) }, 60_000) @@ -87,7 +87,7 @@ it('records an extrinsic error when ctype does not exist', async () => { const tx = await Did.authorizeTx( someDid.id, storeTx, - key.getSignCallback(someDid), + await key.getSigners(someDid), paymentAccount.address ) await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ diff --git a/tests/integration/PublicCredentials.spec.ts b/tests/integration/PublicCredentials.spec.ts index 4de6374a6..428b02905 100644 --- a/tests/integration/PublicCredentials.spec.ts +++ b/tests/integration/PublicCredentials.spec.ts @@ -51,7 +51,7 @@ async function issueCredential( const authorizedStoreTx = await Did.authorizeTx( attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential)), - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedStoreTx, tokenHolder) @@ -60,7 +60,7 @@ async function issueCredential( beforeAll(async () => { api = await initializeApi() tokenHolder = await createEndowedTestAccount() - attesterKey = makeSigningKeyTool() + attesterKey = await makeSigningKeyTool() attester = await createFullDidFromSeed(tokenHolder, attesterKey.keypair) const ctypeExists = await isCtypeOnChain(nftNameCType) @@ -68,7 +68,7 @@ beforeAll(async () => { const tx = await Did.authorizeTx( attester.id, api.tx.ctype.add(CType.toChain(nftNameCType)), - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(tx, tokenHolder) @@ -149,7 +149,7 @@ describe('When there is an attester and ctype NFT name', () => { batchFunction: api.tx.utility.batchAll, did: attester.id, extrinsics: credentialCreationTxs, - sign: attesterKey.getSignCallback(attester), + signers: await attesterKey.getSigners(attester), submitter: tokenHolder.address, }) await submitTx(authorizedBatch, tokenHolder) @@ -178,7 +178,7 @@ describe('When there is an attester and ctype NFT name', () => { const authorizedTx = await Did.authorizeTx( attester.id, revocationTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedTx, tokenHolder) @@ -213,7 +213,7 @@ describe('When there is an attester and ctype NFT name', () => { const authorizedTx = await Did.authorizeTx( attester.id, unrevocationTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedTx, tokenHolder) @@ -248,7 +248,7 @@ describe('When there is an attester and ctype NFT name', () => { const authorizedTx = await Did.authorizeTx( attester.id, removalTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedTx, tokenHolder) @@ -435,7 +435,7 @@ describe('When there is an issued public credential', () => { const authorizedTx = await Did.authorizeTx( attester.id, revocationTx, - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address ) await submitTx(authorizedTx, tokenHolder) @@ -491,7 +491,7 @@ describe('When there is a batch which contains a credential creation', () => { await Did.authorizeTx( attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential1)), - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address, { txCounter: currentAttesterNonce.addn(1) } ), @@ -499,7 +499,7 @@ describe('When there is a batch which contains a credential creation', () => { await Did.authorizeTx( attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential2)), - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address, { txCounter: currentAttesterNonce.addn(2) } ), @@ -509,7 +509,7 @@ describe('When there is a batch which contains a credential creation', () => { api.tx.publicCredentials.add( PublicCredentials.toChain(credential3) ), - attesterKey.getSignCallback(attester), + await attesterKey.getSigners(attester), tokenHolder.address, { txCounter: currentAttesterNonce.addn(3) } ), diff --git a/tests/integration/Web3Names.spec.ts b/tests/integration/Web3Names.spec.ts index 2904bab2c..8f682d01d 100644 --- a/tests/integration/Web3Names.spec.ts +++ b/tests/integration/Web3Names.spec.ts @@ -43,8 +43,8 @@ describe('When there is an Web3NameCreator and a payer', () => { beforeAll(async () => { nick = `nick_${randomAsHex(2)}` differentNick = `different_${randomAsHex(2)}` - w3nCreatorKey = makeSigningKeyTool() - otherW3NCreatorKey = makeSigningKeyTool() + w3nCreatorKey = await makeSigningKeyTool() + otherW3NCreatorKey = await makeSigningKeyTool() paymentAccount = await createEndowedTestAccount() otherPaymentAccount = await createEndowedTestAccount() w3nCreator = await createFullDidFromSeed( @@ -66,11 +66,11 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should not be possible to create a w3n name w/o tokens', async () => { const tx = api.tx.web3Names.claim(nick) - const bobbyBroke = makeSigningKeyTool().keypair + const bobbyBroke = (await makeSigningKeyTool()).keypair const authorizedTx = await Did.authorizeTx( w3nCreator.id, tx, - w3nCreatorKey.getSignCallback(w3nCreator), + await w3nCreatorKey.getSigners(w3nCreator), bobbyBroke.address ) @@ -84,7 +84,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const authorizedTx = await Did.authorizeTx( w3nCreator.id, tx, - w3nCreatorKey.getSignCallback(w3nCreator), + await w3nCreatorKey.getSigners(w3nCreator), paymentAccount.address ) @@ -109,7 +109,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const authorizedTx = await Did.authorizeTx( otherWeb3NameCreator.id, tx, - otherW3NCreatorKey.getSignCallback(otherWeb3NameCreator), + await otherW3NCreatorKey.getSigners(otherWeb3NameCreator), paymentAccount.address ) @@ -126,7 +126,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const authorizedTx = await Did.authorizeTx( w3nCreator.id, tx, - w3nCreatorKey.getSignCallback(w3nCreator), + await w3nCreatorKey.getSigners(w3nCreator), paymentAccount.address ) @@ -158,7 +158,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const prepareAuthorizedTx = await Did.authorizeTx( w3nCreator.id, prepareTx, - w3nCreatorKey.getSignCallback(w3nCreator), + await w3nCreatorKey.getSigners(w3nCreator), paymentAccount.address ) await submitTx(prepareAuthorizedTx, paymentAccount) @@ -167,7 +167,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const authorizedTx = await Did.authorizeTx( w3nCreator.id, tx, - w3nCreatorKey.getSignCallback(w3nCreator), + await w3nCreatorKey.getSigners(w3nCreator), paymentAccount.address ) await submitTx(authorizedTx, paymentAccount) diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts index 93d888092..9d3559fea 100644 --- a/tests/integration/utils.ts +++ b/tests/integration/utils.ts @@ -10,6 +10,7 @@ import type { ApiPromise } from '@polkadot/api' import { BN } from '@polkadot/util' +import { randomAsU8a, encodeAddress } from '@polkadot/util-crypto' import { GenericContainer, StartedTestContainer, Wait } from 'testcontainers' @@ -109,7 +110,7 @@ export const devBob = Crypto.makeKeypairFromUri('//Bob') export const devCharlie = Crypto.makeKeypairFromUri('//Charlie') export function addressFromRandom(): KiltAddress { - return makeSigningKeyTool('ed25519').keypair.address + return encodeAddress(randomAsU8a()) } export async function isCtypeOnChain(cType: ICType): Promise { @@ -187,7 +188,7 @@ export async function fundAccount( export async function createEndowedTestAccount( amount: BN = ENDOWMENT ): Promise { - const { keypair } = makeSigningKeyTool() + const { keypair } = await makeSigningKeyTool() await fundAccount(keypair.address, amount) return keypair } diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 074dc8921..ea488eec3 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -15,7 +15,7 @@ import type { KiltAddress, KiltEncryptionKeypair, KiltKeyringPair, - SignCallback, + SignerInterface, SubmittableExtrinsic, UriFragment, VerificationMethod, @@ -33,7 +33,7 @@ import type { NewService, } from '@kiltprotocol/did' -import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { Crypto, Signers, SDKErrors } from '@kiltprotocol/utils' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' import * as Did from '@kiltprotocol/did' @@ -121,40 +121,6 @@ export function makeEncryptionKeyTool(seed: string): EncryptionKeyTool { } } -export type KeyToolSignCallback = (didDocument: DidDocument) => SignCallback - -/** - * Generates a callback that can be used for signing. - * - * @param keypair The keypair to use for signing. - * @returns The callback. - */ -export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { - return (didDocument) => - async function sign({ data, verificationRelationship }) { - const keyId = didDocument[verificationRelationship]?.[0] - if (keyId === undefined) { - throw new Error( - `Verification method for relationship "${verificationRelationship}" not found in DID "${didDocument.id}"` - ) - } - const verificationMethod = didDocument.verificationMethod?.find( - (vm) => vm.id === keyId - ) - if (verificationMethod === undefined) { - throw new Error( - `Verification method for relationship "${verificationRelationship}" not found in DID "${didDocument.id}"` - ) - } - const signature = keypair.sign(data, { withType: false }) - - return { - signature, - verificationMethod, - } - } -} - type StoreDidCallback = Parameters['2'] /** @@ -179,7 +145,7 @@ export function makeStoreDidCallback( export interface KeyTool { keypair: KiltKeyringPair - getSignCallback: KeyToolSignCallback + getSigners: (doc: DidDocument) => Promise storeDidCallback: StoreDidCallback authentication: [NewLightDidVerificationKey] } @@ -190,16 +156,27 @@ export interface KeyTool { * @param type The type to use for the keypair. * @returns The keypair, matching sign callback, a key usable as DID authentication key. */ -export function makeSigningKeyTool( +export async function makeSigningKeyTool( type: KiltKeyringPair['type'] = 'sr25519' -): KeyTool { +): Promise { const keypair = Crypto.makeKeypairFromSeed(undefined, type) - const getSignCallback = makeSignCallback(keypair) + const getSigners: ( + didDocument: DidDocument + ) => Promise = async (didDocument) => { + return ( + await Promise.all( + didDocument.verificationMethod?.map(({ id }) => + Signers.getSignersForKeypair({ keypair, keyUri: didDocument.id + id }) + ) ?? [] + ) + ).flat() + } + const storeDidCallback = makeStoreDidCallback(keypair) return { keypair, - getSignCallback, + getSigners, storeDidCallback, authentication: [keypair as NewLightDidVerificationKey], } diff --git a/yarn.lock b/yarn.lock index 1ac7b6ba2..6d27e7cf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2037,6 +2037,37 @@ __metadata: languageName: unknown linkType: soft +"@kiltprotocol/ecdsa-secp256k1-jcs-2023@npm:0.0.1-rc.2": + version: 0.0.1-rc.2 + resolution: "@kiltprotocol/ecdsa-secp256k1-jcs-2023@npm:0.0.1-rc.2" + dependencies: + "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.0.1-rc.2 + "@noble/curves": ^1.1.0 + "@scure/base": ^1.1.1 + checksum: ba083c296ddfb54f917e3bb6751e5e69089d11a22dfa395a8b2c912edb5282abf6837823541040c91e889e807672df5374a2523f9bab9908cb672cbbf3cf8f22 + languageName: node + linkType: hard + +"@kiltprotocol/eddsa-jcs-2022@npm:0.0.1-rc.2": + version: 0.0.1-rc.2 + resolution: "@kiltprotocol/eddsa-jcs-2022@npm:0.0.1-rc.2" + dependencies: + "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.0.1-rc.2 + "@noble/curves": ^1.1.0 + "@scure/base": ^1.1.1 + checksum: dd96d116e4580e3d6aa611bbe6d0e3010c69032073ee27a69e1867bcd8fbc95e7054710c81655584575b36e9eedd37f8e530fd55010891934f5afba43fbd801d + languageName: node + linkType: hard + +"@kiltprotocol/jcs-data-integrity-proofs-common@npm:^0.0.1-rc.2": + version: 0.0.1-rc.2 + resolution: "@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.0.1-rc.2" + dependencies: + canonicalize: ^2.0.0 + checksum: f342051b94d4eff1cfbe2ed2445432e91180d4fcdb5a354312f197fd292b29109b9bd2d9de1bcfec53a1ecd1a61155badb54c761a2a4de95e33205ebc9c3de2f + languageName: node + linkType: hard + "@kiltprotocol/legacy-credentials@workspace:packages/legacy-credentials": version: 0.0.0-use.local resolution: "@kiltprotocol/legacy-credentials@workspace:packages/legacy-credentials" @@ -2071,6 +2102,17 @@ __metadata: languageName: unknown linkType: soft +"@kiltprotocol/sr25519-jcs-2023@npm:0.0.1-rc.2": + version: 0.0.1-rc.2 + resolution: "@kiltprotocol/sr25519-jcs-2023@npm:0.0.1-rc.2" + dependencies: + "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.0.1-rc.2 + "@polkadot/util-crypto": ^12.3.2 + "@scure/base": ^1.1.1 + checksum: 573ba2dee062d0af9f18b3805a8156987c613af22f271df5c6f28b332e428efaebedc248e2a88aeffde43bfd224ab1df66928949ebce09d871fd517931ef1d52 + languageName: node + linkType: hard + "@kiltprotocol/type-definitions@workspace:*, @kiltprotocol/type-definitions@workspace:packages/type-definitions": version: 0.0.0-use.local resolution: "@kiltprotocol/type-definitions@workspace:packages/type-definitions" @@ -2098,6 +2140,9 @@ __metadata: version: 0.0.0-use.local resolution: "@kiltprotocol/utils@workspace:packages/utils" dependencies: + "@kiltprotocol/ecdsa-secp256k1-jcs-2023": 0.0.1-rc.2 + "@kiltprotocol/eddsa-jcs-2022": 0.0.1-rc.2 + "@kiltprotocol/sr25519-jcs-2023": 0.0.1-rc.2 "@kiltprotocol/types": "workspace:*" "@polkadot/api": ^10.4.0 "@polkadot/keyring": ^12.0.0 @@ -2140,7 +2185,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.0.0": +"@noble/curves@npm:1.0.0, @noble/curves@npm:^1.1.0": version: 1.0.0 resolution: "@noble/curves@npm:1.0.0" dependencies: @@ -2472,7 +2517,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/util-crypto@npm:12.2.1, @polkadot/util-crypto@npm:^12.0.0, @polkadot/util-crypto@npm:^12.2.1": +"@polkadot/util-crypto@npm:12.2.1, @polkadot/util-crypto@npm:^12.0.0, @polkadot/util-crypto@npm:^12.2.1, @polkadot/util-crypto@npm:^12.3.2": version: 12.2.1 resolution: "@polkadot/util-crypto@npm:12.2.1" dependencies: @@ -2661,7 +2706,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:1.1.1": +"@scure/base@npm:1.1.1, @scure/base@npm:^1.1.1": version: 1.1.1 resolution: "@scure/base@npm:1.1.1" checksum: b4fc810b492693e7e8d0107313ac74c3646970c198bbe26d7332820886fa4f09441991023ec9aa3a2a51246b74409ab5ebae2e8ef148bbc253da79ac49130309 @@ -4038,6 +4083,13 @@ __metadata: languageName: node linkType: hard +"canonicalize@npm:^2.0.0": + version: 2.0.0 + resolution: "canonicalize@npm:2.0.0" + checksum: 541dee6e53c06e81b11241eba76197b6837b3e2a5951a175f57d75eb4c59599ec68566ee88aa2fb3dac6e6ca57d674ca3c0d9c75a855176ce78f0555b26caf39 + languageName: node + linkType: hard + "cbor-web@npm:^9.0.0": version: 9.0.0 resolution: "cbor-web@npm:9.0.0" From 3b4c75ed84d09f2cd7f7b80bca3d130c7e9144e9 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 23 Oct 2023 10:08:14 +0200 Subject: [PATCH 03/16] feat: check for sign capability in byDid --- .../core/src/delegation/DelegationNode.ts | 6 +- packages/legacy-credentials/src/Credential.ts | 12 ++-- packages/utils/src/Signers.ts | 63 ++++++++++++++----- 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index b25a9bb8d..32cec214e 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -275,16 +275,16 @@ export class DelegationNode implements IDelegationNode { ) if (!signer) { throw new Error( - 'no signer available for on-chain verifiable signatures by an authentication key' + `Unable to sign: No signer given for on-chain verifiable signatures by an authentication key related to ${delegateDid.id}` ) } - const delegateSignature = await signer.sign({ + const signature = await signer.sign({ data: this.generateHash(), }) return Did.didSignatureToChain({ - signature: delegateSignature, algorithm: signer.algorithm, + signature, }) } diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 671893630..31e93e9b7 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -530,8 +530,8 @@ export async function createPresentation({ didDocument, }: { credential: ICredential - signers: SignerInterface[] - selectedAttributes?: string[] + signers: readonly SignerInterface[] + selectedAttributes?: readonly string[] challenge?: string didDocument?: DidDocument }): Promise { @@ -552,7 +552,9 @@ export async function createPresentation({ didDocument = (await resolve(credential.claim.owner)).didDocument } if (!didDocument) { - throw new Error('claimer DID cannot be resolved') + throw new Error( + `Unable to sign: Failed to resolve claimer DID ${credential.claim.owner}` + ) } const signer = await Signers.selectSigner( signers, @@ -560,7 +562,9 @@ export async function createPresentation({ byDid(didDocument, { verificationRelationship: 'authentication' }) ) if (!signer) { - throw new Error('no suitable signer available') + throw new Error( + `Unable to sign: No signer given for on-chain verifiable signatures by an authentication key related to ${didDocument.id}` + ) } const signature = await signer?.sign({ diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 1bb3c6dcf..701c67325 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -26,7 +26,12 @@ import { import type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' import { cryptosuite as sr25519Suite } from '@kiltprotocol/sr25519-jcs-2023' -import type { DidDocument, KeyringPair } from '@kiltprotocol/types' +import type { + DidDocument, + DidUrl, + KeyringPair, + UriFragment, +} from '@kiltprotocol/types' export const ALGORITHMS = Object.freeze({ ECRECOVER_SECP256K1_BLAKE2B: 'Ecrecover-Secp256k1-Blake2b', // could also be called ES256K-R-Blake2b @@ -322,25 +327,55 @@ function byDid( verificationRelationship, }: { verificationRelationship?: string; controller?: string } = {} ): SignerSelector { - return (signer) => { - const vm = didDocument.verificationMethod?.find( - ({ id }) => id === signer.id || didDocument.id + id === signer.id // deal with relative DID URLs as ids + let eligibleVMs = didDocument.verificationMethod + // TODO: not super happy about this throwing; can I attach a diagnostics property to the returned function instead that will inform why this will never select a signer? + if (!Array.isArray(eligibleVMs) || eligibleVMs.length === 0) { + throw new Error( + `DID ${didDocument.id} not fit for signing: No verification methods are associated with the signer DID document. It may be that this DID has been deactivated.` ) - if (!vm) { - return false + } + if (controller) { + eligibleVMs = eligibleVMs.filter( + ({ controller: ctr }) => controller === ctr + ) + } + // helps deal with relative DID URLs as ids + function absoluteId(id: string): DidUrl { + if (id.startsWith(didDocument.id)) { + return id as DidUrl } - if (controller && controller !== vm.controller) { - return false + if (id.startsWith('#')) { + return `${didDocument.id}${id as UriFragment}` } + return `${didDocument.id}#${id}` + } + let eligibleIds = eligibleVMs.map(({ id }) => absoluteId(id)) + if (typeof verificationRelationship === 'string') { if ( - typeof verificationRelationship === 'string' && - didDocument[verificationRelationship]?.some?.( - (id: string) => id === signer.id || didDocument.id + id === signer.id // deal with relative DID URLs as ids - ) !== true + !Array.isArray(didDocument[verificationRelationship]) || + didDocument[verificationRelationship].length === 0 ) { - return false + throw new Error( + `DID ${didDocument.id} not fit for signing: No verification methods available for the requested verification relationship ("${verificationRelationship}").` + ) } - return true + eligibleIds = eligibleIds.filter((eligibleId) => + didDocument[verificationRelationship].some?.( + (VrId: string) => VrId === eligibleId || absoluteId(VrId) === eligibleId // TODO: check if leading equality check does indeed help increase performance + ) + ) + } + if (eligibleIds.length === 0) { + throw new Error( + `DID ${ + didDocument.id + } not fit for signing: The verification methods associated with this DID's document do not match the requested controller and/or verification relationship: ${JSON.stringify( + { controller, verificationRelationship } + )}.` + ) + } + return ({ id }) => { + return eligibleIds.includes(id as DidUrl) } } From 9230dc549e16398e3db57008096b036b201b9c5c Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 23 Oct 2023 11:41:12 +0200 Subject: [PATCH 04/16] chore: improve error reporting --- .../core/src/delegation/DelegationNode.ts | 10 ++++-- packages/did/src/DidDetails/FullDidDetails.ts | 22 ++++++++++--- packages/legacy-credentials/src/Credential.ts | 10 ++++-- packages/utils/src/SDKErrors.ts | 32 +++++++++++++++++-- packages/utils/src/Signers.ts | 8 +++-- 5 files changed, 66 insertions(+), 16 deletions(-) diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index 32cec214e..deaf899b5 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -274,9 +274,13 @@ export class DelegationNode implements IDelegationNode { }) ) if (!signer) { - throw new Error( - `Unable to sign: No signer given for on-chain verifiable signatures by an authentication key related to ${delegateDid.id}` - ) + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: delegateDid.id, + verificationRelationship: 'authentication', + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + }, + }) } const signature = await signer.sign({ data: this.generateHash(), diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 340a7c623..9e00a9ba3 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -162,7 +162,7 @@ export async function authorizeTx( didDocument = (await resolve(didUri)).didDocument as DidDocument } if (!didDocument?.id) { - throw new Error('failed to resolve signer DID') + throw new SDKErrors.DidNotFoundError('failed to resolve signer DID') } const signer = await Signers.selectSigner( signers, @@ -170,7 +170,13 @@ export async function authorizeTx( byDid(didDocument, { verificationRelationship }) ) if (typeof signer === 'undefined') { - throw new Error('incompatible signers') // TODO: improve error message + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: didDocument.id, + verificationRelationship, + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + }, + }) } return generateDidAuthenticatedTx({ @@ -286,7 +292,7 @@ export async function authorizeBatch({ didDocument = (await resolve(didUri)).didDocument } if (typeof didDocument?.id !== 'string') { - throw new Error('failed to resolve signer DID') + throw new SDKErrors.DidNotFoundError('failed to resolve signer DID') } const promises = groups.map(async (group, batchIndex) => { @@ -299,10 +305,16 @@ export async function authorizeBatch({ const signer = await Signers.selectSigner( signers, verifiableOnChain(), - byDid(didDocument!, { verificationRelationship }) + byDid(didDocument as DidDocument, { verificationRelationship }) ) if (typeof signer === 'undefined') { - throw new Error('incompatible signers') // TODO: improve error message + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: (didDocument as DidDocument).id, + verificationRelationship, + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + }, + }) } return generateDidAuthenticatedTx({ diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 31e93e9b7..f2bf987ff 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -562,9 +562,13 @@ export async function createPresentation({ byDid(didDocument, { verificationRelationship: 'authentication' }) ) if (!signer) { - throw new Error( - `Unable to sign: No signer given for on-chain verifiable signatures by an authentication key related to ${didDocument.id}` - ) + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: didDocument.id, + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + verificationRelationship: 'authentication', + }, + }) } const signature = await signer?.sign({ diff --git a/packages/utils/src/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index cf9bec169..f2b3d443b 100644 --- a/packages/utils/src/SDKErrors.ts +++ b/packages/utils/src/SDKErrors.ts @@ -13,6 +13,8 @@ /* eslint-disable max-classes-per-file */ +import type { SignerInterface } from '@kiltprotocol/types' + export class SDKError extends Error { constructor(message?: string, options?: ErrorOptions) { super(message, options) @@ -45,8 +47,6 @@ export class EncryptionError extends SDKError {} export class DidError extends SDKError {} -export class DidExporterError extends SDKError {} - export class DidBatchError extends SDKError {} export class DidNotFoundError extends SDKError {} @@ -113,6 +113,34 @@ export class ClaimNonceMapMalformedError extends SDKError { export class SignatureMalformedError extends SDKError {} +export class NoSuitableSignerError extends SDKError { + constructor( + message?: string, + options?: ErrorOptions & { + signerRequirements?: Record + availableSigners?: SignerInterface[] + } + ) { + const { signerRequirements, availableSigners } = options ?? {} + const msgs = [message ?? 'No suitable signers provided to this function.'] + if (signerRequirements) { + msgs.push( + `Expected signer matching conditions ${JSON.stringify( + signerRequirements, + null, + 2 + )}.` + ) + } + if (availableSigners) { + msgs.push( + `Signers available: ${JSON.stringify(availableSigners, null, 2)}.` + ) + } + super(msgs.join('\n'), options) + } +} + export class DidSubjectMismatchError extends SDKError { constructor(actual: string, expected: string) { super( diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 701c67325..1979a6e61 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -33,6 +33,8 @@ import type { UriFragment, } from '@kiltprotocol/types' +import { DidError } from './SDKErrors.js' + export const ALGORITHMS = Object.freeze({ ECRECOVER_SECP256K1_BLAKE2B: 'Ecrecover-Secp256k1-Blake2b', // could also be called ES256K-R-Blake2b ECRECOVER_SECP256K1_KECCAK: 'Ecrecover-Secp256k1-Keccak', // could also be called ES256K-R-Keccak @@ -330,7 +332,7 @@ function byDid( let eligibleVMs = didDocument.verificationMethod // TODO: not super happy about this throwing; can I attach a diagnostics property to the returned function instead that will inform why this will never select a signer? if (!Array.isArray(eligibleVMs) || eligibleVMs.length === 0) { - throw new Error( + throw new DidError( `DID ${didDocument.id} not fit for signing: No verification methods are associated with the signer DID document. It may be that this DID has been deactivated.` ) } @@ -355,7 +357,7 @@ function byDid( !Array.isArray(didDocument[verificationRelationship]) || didDocument[verificationRelationship].length === 0 ) { - throw new Error( + throw new DidError( `DID ${didDocument.id} not fit for signing: No verification methods available for the requested verification relationship ("${verificationRelationship}").` ) } @@ -366,7 +368,7 @@ function byDid( ) } if (eligibleIds.length === 0) { - throw new Error( + throw new DidError( `DID ${ didDocument.id } not fit for signing: The verification methods associated with this DID's document do not match the requested controller and/or verification relationship: ${JSON.stringify( From 90222b703f49a98750b666f4e9e50ceba087674e Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 23 Oct 2023 14:47:49 +0200 Subject: [PATCH 05/16] chore: linting --- packages/did/src/Did.chain.ts | 58 ++++---- packages/did/src/Did.signature.spec.ts | 1 - packages/legacy-credentials/src/Credential.ts | 12 +- packages/types/src/Imported.ts | 1 - packages/types/src/Signers.ts | 12 ++ packages/types/src/index.ts | 1 + packages/utils/src/Signers.ts | 138 +++++++++++------- 7 files changed, 133 insertions(+), 90 deletions(-) create mode 100644 packages/types/src/Signers.ts diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index ecf1b749e..15409e1ea 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -445,6 +445,35 @@ export async function getStoreTx( return api.tx.did.create(encoded, encodedSignature) } +/** + * Compiles an enum-type key-value pair representation of a signature created with a signer associated with a full DID verification method. Required for creating full DID signed extrinsics. + * + * @param input Signature and algorithm. + * @param input.algorithm Descriptor of the signature algorithm used by the signer. + * @param input.signature The signature generated by the signer. + * @returns Data restructured to allow SCALE encoding by polkadot api. + */ +export function didSignatureToChain({ + algorithm, + signature, +}: { + algorithm: string + signature: Uint8Array +}): EncodedSignature { + const lower = algorithm.toLowerCase() + const keyType = + lower === Signers.ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B.toLowerCase() + ? 'ecdsa' + : lower + if (!isValidVerificationMethodType(keyType)) { + throw new SDKErrors.DidError( + `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` + ) + } + + return { [keyType]: signature } as EncodedSignature +} + export interface SigningOptions { signer: SignerInterface } @@ -491,32 +520,3 @@ export async function generateDidAuthenticatedTx({ }) return api.tx.did.submitDidCall(signableCall, encodedSignature) } - -/** - * Compiles an enum-type key-value pair representation of a signature created with a signer associated with a full DID verification method. Required for creating full DID signed extrinsics. - * - * @param input Signature and algorithm. - * @param input.algorithm Descriptor of the signature algorithm used by the signer. - * @param input.signature The signature generated by the signer. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ -export function didSignatureToChain({ - algorithm, - signature, -}: { - algorithm: string - signature: Uint8Array -}): EncodedSignature { - const lower = algorithm.toLowerCase() - const keyType = - lower === Signers.ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B.toLowerCase() - ? 'ecdsa' - : lower - if (!isValidVerificationMethodType(keyType)) { - throw new SDKErrors.DidError( - `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` - ) - } - - return { [keyType]: signature } as EncodedSignature -} diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 66d96bfef..145fc8164 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -181,7 +181,6 @@ describe('light DID', () => { const signature = await authenticationSigner.sign({ data: Crypto.coToUInt8(SIGNED_STRING), }) - await expect( verifyDidSignature({ message: SIGNED_STRING, diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index f2bf987ff..490cca510 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -548,10 +548,10 @@ export async function createPresentation({ excludedClaimProperties ) - if (!didDocument) { - didDocument = (await resolve(credential.claim.owner)).didDocument - } - if (!didDocument) { + const didDoc = + didDocument ?? (await resolve(credential.claim.owner)).didDocument + + if (!didDoc) { throw new Error( `Unable to sign: Failed to resolve claimer DID ${credential.claim.owner}` ) @@ -559,12 +559,12 @@ export async function createPresentation({ const signer = await Signers.selectSigner( signers, verifiableOnChain(), - byDid(didDocument, { verificationRelationship: 'authentication' }) + byDid(didDoc, { verificationRelationship: 'authentication' }) ) if (!signer) { throw new SDKErrors.NoSuitableSignerError(undefined, { signerRequirements: { - did: didDocument.id, + did: didDoc.id, algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, verificationRelationship: 'authentication', }, diff --git a/packages/types/src/Imported.ts b/packages/types/src/Imported.ts index 0bd426ebb..f83228443 100644 --- a/packages/types/src/Imported.ts +++ b/packages/types/src/Imported.ts @@ -15,4 +15,3 @@ export type { HexString } from '@polkadot/util/types' export type { Prefix } from '@polkadot/util-crypto/address/types' export type { SubmittableExtrinsic } from '@polkadot/api/promise/types' export type { KeyringPair } from '@polkadot/keyring/types' -export type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' diff --git a/packages/types/src/Signers.ts b/packages/types/src/Signers.ts new file mode 100644 index 000000000..2987afb5c --- /dev/null +++ b/packages/types/src/Signers.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +export type SignerInterface = { + algorithm: string + id: string + sign: (input: { data: Uint8Array }) => Promise +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 83fde92b1..0b119e907 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -26,3 +26,4 @@ export * from './CryptoCallbacks.js' export * from './DidResolver.js' export * from './PublicCredential.js' export * from './Imported.js' +export * from './Signers.js' diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 1979a6e61..3b238d44b 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -53,18 +53,19 @@ export const DID_PALLET_SUPPORTED_ALGORITHMS = Object.freeze([ * Signer that produces an ECDSA signature over a Blake2b-256 digest of the message using the secp256k1 curve. * The signature has a recovery bit appended to the end, allowing public key recovery. * - * @param root0 - * @param root0.keyUri - * @param root0.publicKey - * @param root0.secretKey + * @param input Holds all function arguments. + * @param input.keyUri Sets the signer's id property. + * @param input.secretKey A 32 byte ECDSA secret key on the secp256k1 curve. + * @param input.publicKey The corresponding public key. May be omitted. + * @returns A signer interface capable of making ECDSA signatures with recovery bit added. */ export async function polkadotEcdsaSigner({ secretKey, - keyUri, + keyUri, // TODO: I think this should just be called id }: { - publicKey?: Uint8Array - secretKey: Uint8Array keyUri: string + secretKey: Uint8Array + publicKey?: Uint8Array }): Promise { return { id: keyUri, @@ -79,19 +80,19 @@ export async function polkadotEcdsaSigner({ * Signer that produces an ECDSA signature over a Keccak-256 digest of the message using the secp256k1 curve. * The signature has a recovery bit appended to the end, allowing public key recovery. * - * @param input - * @param input.keyUri - * @param input.publicKey - * @param input.secretKey - * @returns + * @param input Holds all function arguments. + * @param input.keyUri Sets the signer's id property. + * @param input.secretKey A 32 byte ECDSA secret key on the secp256k1 curve. + * @param input.publicKey The corresponding public key. May be omitted. + * @returns A signer interface capable of making ECDSA signatures with recovery bit added. */ export async function ethereumEcdsaSigner({ secretKey, keyUri, }: { - publicKey?: Uint8Array - secretKey: Uint8Array keyUri: string + secretKey: Uint8Array + publicKey?: Uint8Array }): Promise { return { id: keyUri, @@ -105,19 +106,19 @@ export async function ethereumEcdsaSigner({ /** * Signer that produces an ES256K signature over the message. * - * @param input - * @param input.keyUri - * @param input.publicKey - * @param input.secretKey - * @returns + * @param input Holds all function arguments. + * @param input.keyUri Sets the signer's id property. + * @param input.secretKey A 32 byte ECDSA secret key on the secp256k1 curve. + * @param input.publicKey The corresponding public key. May be omitted. + * @returns A signer interface capable of making ES256K signatures. */ export async function es256kSigner({ secretKey, keyUri, }: { - publicKey?: Uint8Array - secretKey: Uint8Array keyUri: string + secretKey: Uint8Array + publicKey?: Uint8Array }): Promise { // only exists to map secretKey to seed return es256kSignerWrapped({ seed: secretKey, keyUri }) @@ -126,19 +127,19 @@ export async function es256kSigner({ /** * Signer that produces an Ed25519 signature over the message. * - * @param input - * @param input.keyUri - * @param input.publicKey - * @param input.secretKey - * @returns + * @param input Holds all function arguments. + * @param input.keyUri Sets the signer's id property. + * @param input.secretKey A 32 byte Ed25519 secret key. Some key representations append the public key to the private key; to allow these, all bytes after the 32nd byte will be dropped. + * @param input.publicKey The corresponding public key. May be omitted. + * @returns A signer interface capable of making Ed25519 signatures. */ export async function ed25519Signer({ secretKey, keyUri, }: { - publicKey?: Uint8Array - secretKey: Uint8Array keyUri: string + secretKey: Uint8Array + publicKey?: Uint8Array }): Promise { // polkadot ed25519 private keys are a concatenation of private and public key for some reason return ed25519SignerWrapped({ seed: secretKey.slice(0, 32), keyUri }) @@ -147,11 +148,11 @@ export async function ed25519Signer({ /** * Signer that produces an Sr25519 signature over the message. * - * @param input - * @param input.keyUri - * @param input.publicKey - * @param input.secretKey - * @returns + * @param input Holds all function arguments. + * @param input.keyUri Sets the signer's id property. + * @param input.secretKey A 64 byte Sr25519 secret key. + * @param input.publicKey The corresponding 32 byte public key. + * @returns A signer interface capable of making Sr25519 signatures. */ export async function sr25519Signer({ secretKey, @@ -188,14 +189,17 @@ const signerFactory = { } /** - * @param root0 - * @param root0.keypair - * @param root0.algorithm - * @param root0.keyUri + * Creates a signer interface based on an existing keypair and an algorithm descriptor. + * + * @param input Holds all function arguments. + * @param input.keyUri Sets the signer's id property. + * @param input.keypair A polkadot {@link KeyringPair} or combination of `secretKey` & `publicKey`. + * @param input.algorithm An algorithm identifier from the {@link ALGORITHMS} map. + * @returns A signer interface. */ export async function signerFromKeypair({ - keypair, keyUri, + keypair, algorithm, }: { keypair: Keypair | KeyringPair @@ -256,19 +260,22 @@ function algsForKeyType(keyType: string): string[] { } /** - * @param root0 - * @param root0.keypair - * @param root0.type - * @param root0.keyUri + * Based on an existing keypair and its type, creates all available signers that work with this key type. + * + * @param input Holds all function arguments. + * @param input.keyUri Sets the signer's id property. + * @param input.keypair A polkadot {@link KeyringPair} or combination of `secretKey` & `publicKey`. + * @param input.type If `keypair` is not a {@link KeyringPair}, provide the key type here; otherwise, this is ignored. + * @returns An array of signer interfaces based on the keypair and type. */ export async function getSignersForKeypair({ + keyUri, keypair, type = (keypair as KeyringPair).type, - keyUri, }: { + keyUri?: string keypair: Keypair | KeyringPair type?: string - keyUri?: string }): Promise { if (!type) { throw new Error('type is required if keypair.type is not given') @@ -282,12 +289,15 @@ export async function getSignersForKeypair({ } export interface SignerSelector { - (signer: SignerInterface): boolean + (signer: SignerInterface): boolean // TODO: allow async } /** - * @param signers - * @param selectors + * Filters signer interfaces, returning only those accepted by all selectors. + * + * @param signers An array of signer interfaces. + * @param selectors One or more selector callbacks, receiving a signer as input and returning `true` in case it meets selection criteria. + * @returns An array of those signers for which all selectors returned `true`. */ export async function selectSigners( signers: readonly SignerInterface[], @@ -299,8 +309,11 @@ export async function selectSigners( } /** - * @param signers - * @param selectors + * Finds a suiteable signer interfaces in an array of signers, returning the first signer accepted by all selectors. + * + * @param signers An array of signer interfaces. + * @param selectors One or more selector callbacks, receiving a signer as input and returning `true` in case it meets selection criteria. + * @returns The first signer for which all selectors returned `true`, or `undefined` if none meet selection criteria. */ export async function selectSigner( signers: readonly SignerInterface[], @@ -311,17 +324,36 @@ export async function selectSigner( }) } -function byId(id: string): SignerSelector { - return (signer) => signer.id === id +/** + * Select signers based on (key) ids. + * + * @param ids Allowed signer/key ids to filter for. + * @returns A selector identifying signers whose id property is in `ids`. + */ +function byId(ids: readonly string[]): SignerSelector { + return ({ id }) => ids.includes(id) } - +/** + * Select signers based on algorithm identifiers. + * + * @param algorithms Allowed algorithms to filter for. + * @returns A selector identifying signers whose algorithm property is in `algorithms`. + */ function byAlgorithm(algorithms: readonly string[]): SignerSelector { return (signer) => algorithms.some( (algorithm) => algorithm.toLowerCase() === signer.algorithm.toLowerCase() ) } - +/** + * Select signers based on the association of key ids with a given DID. + * + * @param didDocument DidDocument of the DID, on which the signer id must be listed as a verification method. + * @param options Additional optional filter criteria. + * @param options.verificationRelationship If set, the signer id must be listed under this verification relationship on the DidDocument. + * @param options.controller If set, only verificationMethods with this controller are considered. + * @returns A selector identifying signers whose id is associated with the DidDocument. + */ function byDid( didDocument: DidDocument, { From efdb535017bc2585b0f5a7fbc039755ed52d84c6 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 23 Oct 2023 16:27:19 +0200 Subject: [PATCH 06/16] chore: remove all uses of SignCallback --- .../credentialsV1/KiltAttestationProofV1.ts | 6 --- packages/did/src/Did.signature.spec.ts | 53 +++++++------------ 2 files changed, 19 insertions(+), 40 deletions(-) diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index 7529c95e9..8067e75c5 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -48,7 +48,6 @@ import type { ICType, IDelegationNode, KiltAddress, - SignExtrinsicCallback, SignerInterface, } from '@kiltprotocol/types' import * as CType from '../ctype/index.js' @@ -661,11 +660,6 @@ export type AttestationHandler = ( timestamp?: number }> -export interface DidSigner { - did: Did - signer: SignExtrinsicCallback -} - export type TxHandler = { account: KiltAddress signAndSubmit?: AttestationHandler diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 145fc8164..8b6f3bbda 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -9,14 +9,13 @@ import type { KiltKeyringPair, KeyringPair, DidDocument, - SignCallback, DidUrl, DidSignature, DereferenceResult, SignerInterface, } from '@kiltprotocol/types' -import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { Crypto, SDKErrors, Signers } from '@kiltprotocol/utils' import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' import type { NewLightDidVerificationKey } from './DidDetails' @@ -286,8 +285,8 @@ describe('light DID', () => { describe('full DID', () => { let keypair: KiltKeyringPair let did: DidDocument - let sign: SignCallback - beforeAll(() => { + let signer: SignerInterface & { id: DidUrl } + beforeAll(async () => { keypair = Crypto.makeKeypairFromSeed() did = { id: `did:kilt:${keypair.address}`, @@ -301,15 +300,11 @@ describe('full DID', () => { }, ], } - sign = async ({ data, did: signingDid }) => ({ - signature: keypair.sign(data), - verificationMethod: { - id: '#0x12345', - controller: signingDid, - type: 'Multikey', - publicKeyMultibase: keypairToMultibaseKey(keypair), - }, - }) + signer = (await Signers.signerFromKeypair({ + keypair, + keyUri: `${did.id}#0x12345`, + algorithm: 'Ed25519', + })) as SignerInterface & { id: DidUrl } }) beforeEach(() => { @@ -336,16 +331,14 @@ describe('full DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await signer.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -353,16 +346,14 @@ describe('full DID', () => { it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, verificationMethod } = await sign({ + const signature = await signer.sign({ data: SIGNED_BYTES, - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -375,16 +366,14 @@ describe('full DID', () => { contentStream: { id: did.id }, }) const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await signer.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() @@ -396,16 +385,14 @@ describe('full DID', () => { dereferencingMetadata: { error: 'notFound' }, }) const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await signer.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() @@ -413,10 +400,8 @@ describe('full DID', () => { it('accepts signature of full did for light did if enabled', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const signature = await signer.sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', }) const authKey = did.verificationMethod?.find( @@ -438,7 +423,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedSigner, expectedVerificationRelationship: 'authentication', }) @@ -448,7 +433,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedSigner, allowUpgraded: true, expectedVerificationRelationship: 'authentication', From 74de98da09958d1e0056c660c5fa159540810c31 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 23 Oct 2023 17:38:15 +0200 Subject: [PATCH 07/16] refactor!: replace GetStoreTxSignCallback with signer interface --- packages/did/src/Did.chain.ts | 38 +++++++++------ packages/utils/src/SDKErrors.ts | 2 +- tests/bundle/bundle-test.ts | 34 ++++++-------- tests/integration/Deposit.spec.ts | 10 ++-- tests/integration/Did.spec.ts | 78 ++++++++++++++----------------- tests/testUtils/TestUtils.ts | 46 +++++++++--------- 6 files changed, 101 insertions(+), 107 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 15409e1ea..2e4ab7b70 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -22,11 +22,8 @@ import type { KiltAddress, Service, SignerInterface, - SignRequestData, - SignResponseData, SubmittableExtrinsic, UriFragment, - VerificationMethod, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' @@ -329,14 +326,6 @@ interface GetStoreTxInput { service?: NewService[] } -type GetStoreTxSignCallbackResponse = Pick & { - // We don't need the key ID to dispatch the tx. - verificationMethod: Pick -} -export type GetStoreTxSignCallback = ( - signData: Omit -) => Promise - /** * Create a DID creation operation which includes the information provided. * @@ -350,14 +339,15 @@ export type GetStoreTxSignCallback = ( * * @param input The DID keys and services to store. * @param submitter The KILT address authorized to submit the creation operation. - * @param sign The sign callback. The authentication key has to be used. + * @param signers An array of signer interfaces. A suitable signer will be selected if available. + * The signer has to use the authentication public key encoded as a Kilt Address or as a hex string as its id. * * @returns The SubmittableExtrinsic for the DID creation operation. */ export async function getStoreTx( input: GetStoreTxInput, submitter: KiltAddress, - sign: GetStoreTxSignCallback + signers: readonly SignerInterface[] ): Promise { const api = ConfigService.get('api') @@ -435,9 +425,27 @@ export async function getStoreTx( .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) .toU8a() - const { signature } = await sign({ + const signer = await Signers.selectSigner( + signers, + Signers.select.verifiableOnChain(), + Signers.select.byId([did, Crypto.u8aToHex(authenticationKey.publicKey)]) + ) + + if (!signer) { + throw new SDKErrors.NoSuitableSignerError( + 'Did creation requires an account signer where the address is equal to the Did identifier (did:kilt:{identifier}).', + { + availableSigners: signers, + signerRequirements: { + id: [did, Crypto.u8aToHex(authenticationKey.publicKey)], // TODO: we could compute the key id and accept it too, or accept light Dids as signers + algorithm: [Signers.DID_PALLET_SUPPORTED_ALGORITHMS], + }, + } + ) + } + + const signature = await signer.sign({ data: encoded, - verificationRelationship: 'authentication', }) const encodedSignature = { [authenticationKey.type]: signature, diff --git a/packages/utils/src/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index f2b3d443b..6844d45a1 100644 --- a/packages/utils/src/SDKErrors.ts +++ b/packages/utils/src/SDKErrors.ts @@ -118,7 +118,7 @@ export class NoSuitableSignerError extends SDKError { message?: string, options?: ErrorOptions & { signerRequirements?: Record - availableSigners?: SignerInterface[] + availableSigners?: readonly SignerInterface[] } ) { const { signerRequirements, availableSigners } = options ?? {} diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 088000917..39c6dffd7 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -32,27 +32,13 @@ const { ConfigService.set({ submitTxResolveOn: Blockchain.IS_IN_BLOCK }) -type StoreDidCallback = Parameters['2'] - -function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { - return async function sign({ data }) { - const signature = keypair.sign(data, { withType: false }) - return { - signature, - verificationMethod: { - publicKeyMultibase: Did.keypairToMultibaseKey(keypair), - }, - } - } -} - async function makeSigningKeypair( seed: string, type: KiltKeyringPair['type'] = 'sr25519' ): Promise<{ keypair: KiltKeyringPair getSigners: (didDocument: DidDocument) => Promise - storeDidCallback: StoreDidCallback + storeDidSigners: SignerInterface[] }> { const keypair = Crypto.makeKeypairFromUri(seed, type) @@ -67,12 +53,15 @@ async function makeSigningKeypair( ) ).flat() } - const storeDidCallback = makeStoreDidCallback(keypair) + const storeDidSigners = await kilt.Utils.Signers.getSignersForKeypair({ + keypair, + keyUri: keypair.address, + }) return { keypair, getSigners, - storeDidCallback, + storeDidSigners, } } @@ -93,7 +82,10 @@ async function createFullDidFromKeypair( encryptionKey: NewDidEncryptionKey ) { const api = ConfigService.get('api') - const sign = makeStoreDidCallback(keypair) + const signers = await kilt.Utils.Signers.getSignersForKeypair({ + keypair, + keyUri: keypair.address, + }) const storeTx = await Did.getStoreTx( { @@ -103,7 +95,7 @@ async function createFullDidFromKeypair( keyAgreement: [encryptionKey], }, payer.address, - sign + signers ) await Blockchain.signAndSubmitTx(storeTx, payer) @@ -172,7 +164,7 @@ async function runAll() { // Chain DID workflow -> creation & deletion console.log('DID workflow started') - const { keypair, getSigners, storeDidCallback } = await makeSigningKeypair( + const { keypair, getSigners, storeDidSigners } = await makeSigningKeypair( '//Foo', 'ed25519' ) @@ -180,7 +172,7 @@ async function runAll() { const didStoreTx = await Did.getStoreTx( { authentication: [keypair] }, payer.address, - storeDidCallback + storeDidSigners ) await Blockchain.signAndSubmitTx(didStoreTx, payer) diff --git a/tests/integration/Deposit.spec.ts b/tests/integration/Deposit.spec.ts index 00d84feb3..c8cb8b48d 100644 --- a/tests/integration/Deposit.spec.ts +++ b/tests/integration/Deposit.spec.ts @@ -361,27 +361,27 @@ describe('Different deposits scenarios', () => { testFullDidFive = await createFullDidFromLightDid( keys[4].keypair, testDidFive, - keys[4].storeDidCallback + [keys[4].storeDidSigner] ) testFullDidSix = await createFullDidFromLightDid( keys[5].keypair, testDidSix, - keys[5].storeDidCallback + [keys[5].storeDidSigner] ) testFullDidSeven = await createFullDidFromLightDid( keys[6].keypair, testDidSeven, - keys[6].storeDidCallback + [keys[6].storeDidSigner] ) testFullDidEight = await createFullDidFromLightDid( keys[7].keypair, testDidEight, - keys[7].storeDidCallback + [keys[7].storeDidSigner] ) testFullDidNine = await createFullDidFromLightDid( keys[8].keypair, testDidNine, - keys[8].storeDidCallback + [keys[8].storeDidSigner] ) testFullDidTen = await createFullDidFromSeed( keys[9].keypair, diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index 3bb15547e..b1955060e 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -61,11 +61,9 @@ describe('write and didDeleteTx', () => { it('fails to create a new DID on chain with a different submitter than the one in the creation operation', async () => { const otherAccount = devBob - const tx = await getStoreTxFromDidDocument( - did, - otherAccount.address, - key.storeDidCallback - ) + const tx = await getStoreTxFromDidDocument(did, otherAccount.address, [ + key.storeDidSigner, + ]) await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ isBadOrigin: true, @@ -96,11 +94,9 @@ describe('write and didDeleteTx', () => { ], } - const tx = await Did.getStoreTx( - input, - paymentAccount.address, - key.storeDidCallback - ) + const tx = await Did.getStoreTx(input, paymentAccount.address, [ + key.storeDidSigner, + ]) await submitTx(tx, paymentAccount) @@ -241,14 +237,12 @@ describe('write and didDeleteTx', () => { }) it('creates and updates DID, and then reclaims the deposit back', async () => { - const { keypair, getSigners, storeDidCallback } = await makeSigningKeyTool() + const { keypair, getSigners, storeDidSigner } = await makeSigningKeyTool() const newDid = await createMinimalLightDidFromKeypair(keypair) - const tx = await getStoreTxFromDidDocument( - newDid, - paymentAccount.address, - storeDidCallback - ) + const tx = await getStoreTxFromDidDocument(newDid, paymentAccount.address, [ + storeDidSigner, + ]) await submitTx(tx, paymentAccount) @@ -337,7 +331,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { describe('DID migration', () => { it('migrates light DID with ed25519 auth key and encryption key', async () => { - const { storeDidCallback, authentication } = await makeSigningKeyTool( + const { storeDidSigner, authentication } = await makeSigningKeyTool( 'ed25519' ) const { keyAgreement } = makeEncryptionKeyTool( @@ -351,7 +345,7 @@ describe('DID migration', () => { const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(storeTx, paymentAccount) @@ -400,7 +394,7 @@ describe('DID migration', () => { }) it('migrates light DID with sr25519 auth key', async () => { - const { authentication, storeDidCallback } = await makeSigningKeyTool() + const { authentication, storeDidSigner } = await makeSigningKeyTool() const lightDid = Did.createLightDidDocument({ authentication, }) @@ -408,7 +402,7 @@ describe('DID migration', () => { const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(storeTx, paymentAccount) @@ -451,7 +445,7 @@ describe('DID migration', () => { }) it('migrates light DID with ed25519 auth key, encryption key, and services', async () => { - const { storeDidCallback, authentication } = await makeSigningKeyTool( + const { storeDidSigner, authentication } = await makeSigningKeyTool( 'ed25519' ) const { keyAgreement } = makeEncryptionKeyTool( @@ -473,7 +467,7 @@ describe('DID migration', () => { const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(storeTx, paymentAccount) @@ -548,7 +542,7 @@ describe('DID authorization', () => { let signers: SignerInterface[] beforeAll(async () => { - const { getSigners, storeDidCallback, authentication } = + const { getSigners, storeDidSigner, authentication } = await makeSigningKeyTool('ed25519') const createTx = await Did.getStoreTx( @@ -558,7 +552,7 @@ describe('DID authorization', () => { capabilityDelegation: authentication, }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) const didLinkedInfo = await api.call.did.query( @@ -620,7 +614,7 @@ describe('DID authorization', () => { describe('DID management batching', () => { describe('FullDidCreationBuilder', () => { it('Build a complete full DID', async () => { - const { storeDidCallback, authentication } = await makeSigningKeyTool() + const { storeDidSigner, authentication } = await makeSigningKeyTool() const extrinsic = await Did.getStoreTx( { authentication, @@ -669,7 +663,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( @@ -763,7 +757,7 @@ describe('DID management batching', () => { }) it('Build a minimal full DID with an Ecdsa key', async () => { - const { keypair, storeDidCallback } = await makeSigningKeyTool('ecdsa') + const { keypair, storeDidSigner } = await makeSigningKeyTool('ecdsa') const didAuthKey: Did.NewDidVerificationKey = { publicKey: keypair.publicKey, type: 'ecdsa', @@ -772,7 +766,7 @@ describe('DID management batching', () => { const extrinsic = await Did.getStoreTx( { authentication: [didAuthKey] }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(extrinsic, paymentAccount) @@ -805,7 +799,7 @@ describe('DID management batching', () => { describe('FullDidUpdateBuilder', () => { it('Build from a complete full DID and remove everything but the authentication key', async () => { - const { keypair, getSigners, storeDidCallback, authentication } = + const { keypair, getSigners, storeDidSigner, authentication } = await makeSigningKeyTool() const createTx = await Did.getStoreTx( @@ -847,7 +841,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) @@ -922,7 +916,7 @@ describe('DID management batching', () => { }, 40_000) it('Correctly handles rotation of the authentication key', async () => { - const { authentication, getSigners, storeDidCallback } = + const { authentication, getSigners, storeDidSigner } = await makeSigningKeyTool() const { authentication: [newAuthKey], @@ -931,7 +925,7 @@ describe('DID management batching', () => { const createTx = await Did.getStoreTx( { authentication }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) @@ -1013,7 +1007,7 @@ describe('DID management batching', () => { }, 40_000) it('simple `batch` succeeds despite failures of some extrinsics', async () => { - const { authentication, getSigners, storeDidCallback } = + const { authentication, getSigners, storeDidSigner } = await makeSigningKeyTool() const tx = await Did.getStoreTx( { @@ -1027,7 +1021,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) // Create the full DIgetStoreTx await submitTx(tx, paymentAccount) @@ -1098,7 +1092,7 @@ describe('DID management batching', () => { }, 60_000) it('batchAll fails if any extrinsics fails', async () => { - const { authentication, getSigners, storeDidCallback } = + const { authentication, getSigners, storeDidSigner } = await makeSigningKeyTool() const createTx = await Did.getStoreTx( { @@ -1112,7 +1106,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( @@ -1333,14 +1327,14 @@ describe('DID extrinsics batching', () => { describe('Runtime constraints', () => { let testAuthKey: Did.NewDidVerificationKey - let storeDidCallback: Did.GetStoreTxSignCallback + let storeDidSigner: SignerInterface beforeAll(async () => { const tool = await makeSigningKeyTool('ed25519') testAuthKey = { publicKey: tool.keypair.publicKey, type: 'ed25519', } - storeDidCallback = tool.storeDidCallback + storeDidSigner = tool.storeDidSigner }) describe('DID creation', () => { it('should not be possible to create a DID with too many encryption keys', async () => { @@ -1357,7 +1351,7 @@ describe('Runtime constraints', () => { keyAgreement: newKeyAgreementKeys, }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) // One more than the maximum newKeyAgreementKeys.push({ @@ -1372,7 +1366,7 @@ describe('Runtime constraints', () => { }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"The number of key agreement keys in the creation operation is greater than the maximum allowed, which is 10"` @@ -1394,7 +1388,7 @@ describe('Runtime constraints', () => { service: newServiceEndpoints, }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) // One more than the maximum newServiceEndpoints.push({ @@ -1410,7 +1404,7 @@ describe('Runtime constraints', () => { }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Cannot store more than 25 services per DID"` diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index ea488eec3..6e0de40be 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -25,7 +25,6 @@ import type { BaseNewDidKey, ChainDidKey, DidVerificationMethodType, - GetStoreTxSignCallback, LightDidSupportedVerificationKeyType, NewLightDidVerificationKey, NewDidVerificationKey, @@ -129,24 +128,24 @@ type StoreDidCallback = Parameters['2'] * @param keypair The keypair to use for signing. * @returns The callback. */ -export function makeStoreDidCallback( +export async function makeStoreDidSigner( keypair: KiltKeyringPair -): StoreDidCallback { - return async function sign({ data }) { - const signature = keypair.sign(data, { withType: false }) - return { - signature, - verificationMethod: { - publicKeyMultibase: Did.keypairToMultibaseKey(keypair), - }, - } - } +): Promise { + const signers = await Signers.getSignersForKeypair({ + keypair, + keyUri: keypair.address, + }) + const signer = await Signers.selectSigner( + signers, + Signers.select.verifiableOnChain() + ) + return signer as SignerInterface } export interface KeyTool { keypair: KiltKeyringPair getSigners: (doc: DidDocument) => Promise - storeDidCallback: StoreDidCallback + storeDidSigner: SignerInterface authentication: [NewLightDidVerificationKey] } @@ -172,12 +171,12 @@ export async function makeSigningKeyTool( ).flat() } - const storeDidCallback = makeStoreDidCallback(keypair) + const storeDidSigner = await makeStoreDidSigner(keypair) return { keypair, getSigners, - storeDidCallback, + storeDidSigner, authentication: [keypair as NewLightDidVerificationKey], } } @@ -389,14 +388,15 @@ export async function createLocalDemoFullDidFromLightDid( * * @param input The DID Document to store. * @param submitter The KILT address authorized to submit the creation operation. - * @param sign The sign callback. The authentication key has to be used. + * @param signers An array of signer interfaces. A suitable signer will be selected if available. + * The signer has to use the authentication public key encoded as a Kilt Address or as a hex string as its id. * * @returns The SubmittableExtrinsic for the DID creation operation. */ export async function getStoreTxFromDidDocument( input: DidDocument, submitter: KiltAddress, - sign: GetStoreTxSignCallback + signers: readonly SignerInterface[] ): Promise { const { authentication, @@ -457,14 +457,14 @@ export async function getStoreTxFromDidDocument( service, } - return Did.getStoreTx(storeTxInput, submitter, sign) + return Did.getStoreTx(storeTxInput, submitter, signers) } // It takes the auth key from the light DID and use it as attestation and delegation key as well. export async function createFullDidFromLightDid( payer: KiltKeyringPair, lightDidForId: DidDocument, - sign: StoreDidCallback + signer: StoreDidCallback ): Promise { const api = ConfigService.get('api') const fullDidDocumentToBeCreated = lightDidForId @@ -479,14 +479,14 @@ export async function createFullDidFromLightDid( const tx = await getStoreTxFromDidDocument( fullDidDocumentToBeCreated, payer.address, - sign + signer ) await Blockchain.signAndSubmitTx(tx, payer) const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( Did.toChain(fullDidDocumentToBeCreated.id) ) - const { document } = await Did.linkedInfoFromChain(encodedDidDetails) + const { document } = Did.linkedInfoFromChain(encodedDidDetails) return document } @@ -495,6 +495,6 @@ export async function createFullDidFromSeed( keypair: KiltKeyringPair ): Promise { const lightDid = await createMinimalLightDidFromKeypair(keypair) - const sign = makeStoreDidCallback(keypair) - return createFullDidFromLightDid(payer, lightDid, sign) + const signer = await makeStoreDidSigner(keypair) + return createFullDidFromLightDid(payer, lightDid, [signer]) } From 672e251b9621c4b9b6625095fc88edca07c6373e Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 23 Oct 2023 17:38:41 +0200 Subject: [PATCH 08/16] test: fix bundle tests --- packages/did/src/DidDetails/FullDidDetails.ts | 2 ++ tests/bundle/bundle-test.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 9e00a9ba3..def2e08a0 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -176,6 +176,7 @@ export async function authorizeTx( verificationRelationship, algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, }, + availableSigners: signers, }) } @@ -314,6 +315,7 @@ export async function authorizeBatch({ verificationRelationship, algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, }, + availableSigners: signers, }) } diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 39c6dffd7..f2f39dcc2 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -48,7 +48,10 @@ async function makeSigningKeypair( return ( await Promise.all( didDocument.verificationMethod?.map(({ id }) => - kilt.Utils.Signers.getSignersForKeypair({ keypair, keyUri: id }) + kilt.Utils.Signers.getSignersForKeypair({ + keypair, + keyUri: didDocument.id + id, + }) ) ?? [] ) ).flat() From 977fb4361f56c43f9bff116eb5bcea88de15a82a Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 25 Oct 2023 12:51:45 +0200 Subject: [PATCH 09/16] chore: make all signers params readonly --- packages/core/src/credentialsV1/KiltAttestationProofV1.ts | 2 +- packages/core/src/delegation/DelegationNode.ts | 2 +- packages/did/src/DidDetails/FullDidDetails.ts | 4 ++-- packages/legacy-credentials/src/Credential.spec.ts | 2 +- packages/vc-export/src/suites/KiltAttestationProofV1.ts | 2 +- tests/integration/Delegation.spec.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index 8067e75c5..1a1388542 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -667,7 +667,7 @@ export type TxHandler = { } export type IssueOpts = { - signers: SignerInterface[] + signers: readonly SignerInterface[] transactionHandler: TxHandler } & Parameters[4] diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index deaf899b5..dc1d59fd9 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -263,7 +263,7 @@ export class DelegationNode implements IDelegationNode { */ public async delegateSign( delegateDid: DidDocument, - signers: SignerInterface[] + signers: readonly SignerInterface[] ): Promise { const { byDid, verifiableOnChain } = Signers.select const signer = await Signers.selectSigner( diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index def2e08a0..e5da8be91 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -128,7 +128,7 @@ const { verifiableOnChain, byDid } = Signers.select export async function authorizeTx( did: Did | DidDocument, extrinsic: Extrinsic, - signers: SignerInterface[], + signers: readonly SignerInterface[], submitterAccount: KiltAddress, { txCounter, @@ -255,7 +255,7 @@ export async function authorizeBatch({ did: Did | DidDocument extrinsics: Extrinsic[] nonce?: BN - signers: SignerInterface[] + signers: readonly SignerInterface[] submitter: KiltAddress }): Promise { if (extrinsics.length === 0) { diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 5b034bdce..19b36b49d 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -471,7 +471,7 @@ describe('Presentations', () => { attesterDid: KiltDid, contents: IClaim['contents'], legitimations: ICredential[], - signers: SignerInterface[] + signers: readonly SignerInterface[] ): Promise<[ICredentialPresentation, IAttestation]> { // create claim const claim = Claim.fromCTypeAndClaimContents( diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.ts index c5a522467..4516780e9 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.ts @@ -212,7 +212,7 @@ export class KiltAttestationV1Suite extends LinkedDataProof { public async anchorCredential( input: CredentialStub, issuer: DidDocument | Did, - signers: SignerInterface[], + signers: readonly SignerInterface[], transactionHandler: KiltAttestationProofV1.TxHandler ): Promise> { const { credentialSubject, type } = input diff --git a/tests/integration/Delegation.spec.ts b/tests/integration/Delegation.spec.ts index 773ec86c7..11edacd7e 100644 --- a/tests/integration/Delegation.spec.ts +++ b/tests/integration/Delegation.spec.ts @@ -54,7 +54,7 @@ let attesterKey: KeyTool async function writeHierarchy( delegator: DidDocument, cTypeId: ICType['$id'], - signers: SignerInterface[] + signers: readonly SignerInterface[] ): Promise { const rootNode = DelegationNode.newRoot({ account: delegator.id, From 7f45f8021b940b5ae4ef6b525b2e7e00b2b29519 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 25 Oct 2023 13:55:39 +0200 Subject: [PATCH 10/16] feat: generic SignerInterface --- packages/did/src/Did.chain.ts | 25 +++--- packages/did/src/Did.signature.spec.ts | 8 +- .../did/src/DidDetails/FullDidDetails.spec.ts | 1 + packages/did/src/DidDetails/FullDidDetails.ts | 5 +- packages/types/src/Signers.ts | 9 ++- packages/utils/src/Signers.ts | 81 ++++++++++--------- .../src/suites/KiltAttestationProofV1.spec.ts | 3 +- tests/bundle/bundle-test.ts | 11 ++- tests/testUtils/TestUtils.ts | 18 +++-- 9 files changed, 95 insertions(+), 66 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 2e4ab7b70..35c234458 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -19,6 +19,7 @@ import type { BN, Deposit, Did, + DidUrl, KiltAddress, Service, SignerInterface, @@ -291,14 +292,6 @@ export function serviceFromChain( } } -export type AuthorizeCallInput = { - did: Did - txCounter: AnyNumber - call: Extrinsic - submitter: KiltAddress - blockNumber?: AnyNumber -} - export function publicKeyToChain( key: NewDidVerificationKey ): EncodedVerificationKey @@ -482,8 +475,18 @@ export function didSignatureToChain({ return { [keyType]: signature } as EncodedSignature } -export interface SigningOptions { - signer: SignerInterface +export type AuthorizeCallInput = { + did: Did + txCounter: AnyNumber + call: Extrinsic + submitter: KiltAddress + blockNumber?: AnyNumber + signer: SignerInterface< + | typeof Signers.ALGORITHMS.SR25519 + | typeof Signers.ALGORITHMS.ED25519 + | typeof Signers.ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, + DidUrl + > } /** @@ -506,7 +509,7 @@ export async function generateDidAuthenticatedTx({ txCounter, submitter, blockNumber, -}: AuthorizeCallInput & SigningOptions): Promise { +}: AuthorizeCallInput): Promise { const api = ConfigService.get('api') const signableCall = api.registry.createType( diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 8b6f3bbda..77adface4 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -38,7 +38,7 @@ jest describe('light DID', () => { let keypair: KiltKeyringPair let did: DidDocument - let authenticationSigner: SignerInterface + let authenticationSigner: SignerInterface beforeAll(async () => { const keyTool = await makeSigningKeyTool() keypair = keyTool.keypair @@ -285,7 +285,7 @@ describe('light DID', () => { describe('full DID', () => { let keypair: KiltKeyringPair let did: DidDocument - let signer: SignerInterface & { id: DidUrl } + let signer: SignerInterface beforeAll(async () => { keypair = Crypto.makeKeypairFromSeed() did = { @@ -300,11 +300,11 @@ describe('full DID', () => { }, ], } - signer = (await Signers.signerFromKeypair({ + signer = await Signers.signerFromKeypair({ keypair, keyUri: `${did.id}#0x12345`, algorithm: 'Ed25519', - })) as SignerInterface & { id: DidUrl } + }) }) beforeEach(() => { diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 3d14af0fb..95f8b3ef1 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -36,6 +36,7 @@ jest.mock('../DidResolver/DidResolver', () => { ...jest.requireActual('../DidResolver/DidResolver'), resolve: jest.fn(), dereference: jest.fn(), + resolveRepresentation: jest.fn(), } }) jest.mock('../Did.chain') diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index e5da8be91..07dd25ce1 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -12,6 +12,7 @@ import { BN } from '@polkadot/util' import type { DidDocument, Did, + DidUrl, KiltAddress, SignatureVerificationRelationship, SignerInterface, @@ -182,7 +183,7 @@ export async function authorizeTx( return generateDidAuthenticatedTx({ did: didUri, - signer, + signer: signer as SignerInterface, call: extrinsic, txCounter: txCounter || (await getNextNonce(didUri)), submitter: submitterAccount, @@ -321,7 +322,7 @@ export async function authorizeBatch({ return generateDidAuthenticatedTx({ did: didUri, - signer, + signer: signer as SignerInterface, call, txCounter, submitter, diff --git a/packages/types/src/Signers.ts b/packages/types/src/Signers.ts index 2987afb5c..dbe15db31 100644 --- a/packages/types/src/Signers.ts +++ b/packages/types/src/Signers.ts @@ -5,8 +5,11 @@ * found in the LICENSE file in the root directory of this source tree. */ -export type SignerInterface = { - algorithm: string - id: string +export type SignerInterface< + Alg extends string = string, + Id extends string = string +> = { + algorithm: Alg + id: Id sign: (input: { data: Uint8Array }) => Promise } diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 3b238d44b..1a2f1593c 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -23,10 +23,10 @@ import { createSigner as ed25519SignerWrapped, cryptosuite as ed25519Suite, } from '@kiltprotocol/eddsa-jcs-2022' -import type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' import { cryptosuite as sr25519Suite } from '@kiltprotocol/sr25519-jcs-2023' import type { + SignerInterface, DidDocument, DidUrl, KeyringPair, @@ -59,14 +59,16 @@ export const DID_PALLET_SUPPORTED_ALGORITHMS = Object.freeze([ * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making ECDSA signatures with recovery bit added. */ -export async function polkadotEcdsaSigner({ +export async function polkadotEcdsaSigner({ secretKey, keyUri, // TODO: I think this should just be called id }: { - keyUri: string + keyUri: Id secretKey: Uint8Array publicKey?: Uint8Array -}): Promise { +}): Promise< + SignerInterface +> { return { id: keyUri, algorithm: ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, @@ -86,14 +88,14 @@ export async function polkadotEcdsaSigner({ * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making ECDSA signatures with recovery bit added. */ -export async function ethereumEcdsaSigner({ +export async function ethereumEcdsaSigner({ secretKey, keyUri, }: { - keyUri: string + keyUri: Id secretKey: Uint8Array publicKey?: Uint8Array -}): Promise { +}): Promise> { return { id: keyUri, algorithm: ALGORITHMS.ECRECOVER_SECP256K1_KECCAK, @@ -112,16 +114,18 @@ export async function ethereumEcdsaSigner({ * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making ES256K signatures. */ -export async function es256kSigner({ +export async function es256kSigner({ secretKey, keyUri, }: { - keyUri: string + keyUri: Id secretKey: Uint8Array publicKey?: Uint8Array -}): Promise { +}): Promise> { // only exists to map secretKey to seed - return es256kSignerWrapped({ seed: secretKey, keyUri }) + return es256kSignerWrapped({ seed: secretKey, keyUri }) as Promise< + SignerInterface + > } /** @@ -133,16 +137,19 @@ export async function es256kSigner({ * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making Ed25519 signatures. */ -export async function ed25519Signer({ +export async function ed25519Signer({ secretKey, keyUri, }: { - keyUri: string + keyUri: Id secretKey: Uint8Array publicKey?: Uint8Array -}): Promise { +}): Promise> { // polkadot ed25519 private keys are a concatenation of private and public key for some reason - return ed25519SignerWrapped({ seed: secretKey.slice(0, 32), keyUri }) + return ed25519SignerWrapped({ + seed: secretKey.slice(0, 32), + keyUri, + }) as Promise> } /** @@ -154,15 +161,15 @@ export async function ed25519Signer({ * @param input.publicKey The corresponding 32 byte public key. * @returns A signer interface capable of making Sr25519 signatures. */ -export async function sr25519Signer({ +export async function sr25519Signer({ secretKey, keyUri, publicKey, }: { publicKey: Uint8Array secretKey: Uint8Array - keyUri: string -}): Promise { + keyUri: Id +}): Promise> { await cryptoWaitReady() return { id: keyUri, @@ -197,26 +204,26 @@ const signerFactory = { * @param input.algorithm An algorithm identifier from the {@link ALGORITHMS} map. * @returns A signer interface. */ -export async function signerFromKeypair({ +export async function signerFromKeypair({ keyUri, keypair, algorithm, }: { keypair: Keypair | KeyringPair - algorithm: string - keyUri?: string -}): Promise { - const makeSigner: (x: { + algorithm: Alg + keyUri?: Id +}): Promise> { + const makeSigner = signerFactory[algorithm] as (x: { secretKey: Uint8Array publicKey: Uint8Array - keyUri: string - }) => Promise = signerFactory[algorithm] + keyUri: Id + }) => Promise> if (typeof makeSigner !== 'function') { throw new Error(`unknown algorithm ${algorithm}`) } if (!('secretKey' in keypair) && 'encodePkcs8' in keypair) { - const id = keyUri ?? keypair.address + const id = keyUri ?? (keypair.address as Id) return { id, algorithm, @@ -237,7 +244,7 @@ export async function signerFromKeypair({ return makeSigner({ secretKey, publicKey, - keyUri: keyUri ?? encodeAddress(publicKey, 38), + keyUri: keyUri ?? (encodeAddress(publicKey, 38) as Id), }) } @@ -268,21 +275,21 @@ function algsForKeyType(keyType: string): string[] { * @param input.type If `keypair` is not a {@link KeyringPair}, provide the key type here; otherwise, this is ignored. * @returns An array of signer interfaces based on the keypair and type. */ -export async function getSignersForKeypair({ +export async function getSignersForKeypair({ keyUri, keypair, type = (keypair as KeyringPair).type, }: { - keyUri?: string + keyUri?: Id keypair: Keypair | KeyringPair type?: string -}): Promise { +}): Promise>> { if (!type) { throw new Error('type is required if keypair.type is not given') } const algorithms = algsForKeyType(type) return Promise.all( - algorithms.map>(async (algorithm) => { + algorithms.map>>(async (algorithm) => { return signerFromKeypair({ keypair, keyUri, algorithm }) }) ) @@ -299,10 +306,10 @@ export interface SignerSelector { * @param selectors One or more selector callbacks, receiving a signer as input and returning `true` in case it meets selection criteria. * @returns An array of those signers for which all selectors returned `true`. */ -export async function selectSigners( - signers: readonly SignerInterface[], +export async function selectSigners( + signers: readonly T[], ...selectors: readonly SignerSelector[] -): Promise { +): Promise { return signers.filter((signer) => { return selectors.every((selector) => selector(signer)) }) @@ -315,10 +322,10 @@ export async function selectSigners( * @param selectors One or more selector callbacks, receiving a signer as input and returning `true` in case it meets selection criteria. * @returns The first signer for which all selectors returned `true`, or `undefined` if none meet selection criteria. */ -export async function selectSigner( - signers: readonly SignerInterface[], +export async function selectSigner( + signers: readonly T[], ...selectors: readonly SignerSelector[] -): Promise { +): Promise { return signers.find((signer) => { return selectors.every((selector) => selector(signer)) }) diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index d2357a6d5..d57a63899 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -23,6 +23,7 @@ import { ConfigService } from '@kiltprotocol/config' import * as Did from '@kiltprotocol/did' import type { DidDocument, + DidUrl, HexString, ICType, KiltAddress, @@ -448,7 +449,7 @@ describe('issuance', () => { let toBeSigned: CredentialStub const { issuer } = attestedVc - const signer: SignerInterface = { + const signer: SignerInterface<'Sr25519', DidUrl> = { sign: async () => new Uint8Array(32), algorithm: 'Sr25519', id: `${issuer}#1`, diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index f2f39dcc2..3138f879f 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -10,6 +10,7 @@ import type { NewDidEncryptionKey } from '@kiltprotocol/did' import type { DidDocument, + DidUrl, KiltEncryptionKeypair, KiltKeyringPair, SignerInterface, @@ -37,20 +38,24 @@ async function makeSigningKeypair( type: KiltKeyringPair['type'] = 'sr25519' ): Promise<{ keypair: KiltKeyringPair - getSigners: (didDocument: DidDocument) => Promise + getSigners: ( + didDocument: DidDocument + ) => Promise>> storeDidSigners: SignerInterface[] }> { const keypair = Crypto.makeKeypairFromUri(seed, type) const getSigners: ( didDocument: DidDocument - ) => Promise = async (didDocument) => { + ) => Promise>> = async ( + didDocument + ) => { return ( await Promise.all( didDocument.verificationMethod?.map(({ id }) => kilt.Utils.Signers.getSignersForKeypair({ keypair, - keyUri: didDocument.id + id, + keyUri: `${didDocument.id}${id}`, }) ) ?? [] ) diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 6e0de40be..23f20464f 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -10,6 +10,7 @@ import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' import type { DecryptCallback, DidDocument, + DidUrl, EncryptCallback, KeyringPair, KiltAddress, @@ -130,7 +131,7 @@ type StoreDidCallback = Parameters['2'] */ export async function makeStoreDidSigner( keypair: KiltKeyringPair -): Promise { +): Promise> { const signers = await Signers.getSignersForKeypair({ keypair, keyUri: keypair.address, @@ -139,12 +140,14 @@ export async function makeStoreDidSigner( signers, Signers.select.verifiableOnChain() ) - return signer as SignerInterface + return signer! } export interface KeyTool { keypair: KiltKeyringPair - getSigners: (doc: DidDocument) => Promise + getSigners: ( + doc: DidDocument + ) => Promise>> storeDidSigner: SignerInterface authentication: [NewLightDidVerificationKey] } @@ -161,11 +164,16 @@ export async function makeSigningKeyTool( const keypair = Crypto.makeKeypairFromSeed(undefined, type) const getSigners: ( didDocument: DidDocument - ) => Promise = async (didDocument) => { + ) => Promise>> = async ( + didDocument + ) => { return ( await Promise.all( didDocument.verificationMethod?.map(({ id }) => - Signers.getSignersForKeypair({ keypair, keyUri: didDocument.id + id }) + Signers.getSignersForKeypair({ + keypair, + keyUri: `${didDocument.id}${id}`, + }) ) ?? [] ) ).flat() From b92155ad1fda0433767696ee0f8e86940aa9b6d9 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 25 Oct 2023 14:12:51 +0200 Subject: [PATCH 11/16] feat: allow overriding selectSigner(s) result type --- packages/did/src/DidDetails/FullDidDetails.ts | 8 +++---- packages/utils/src/Signers.ts | 22 ++++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 07dd25ce1..6d1991cf3 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -165,7 +165,7 @@ export async function authorizeTx( if (!didDocument?.id) { throw new SDKErrors.DidNotFoundError('failed to resolve signer DID') } - const signer = await Signers.selectSigner( + const signer = await Signers.selectSigner>( signers, verifiableOnChain(), byDid(didDocument, { verificationRelationship }) @@ -183,7 +183,7 @@ export async function authorizeTx( return generateDidAuthenticatedTx({ did: didUri, - signer: signer as SignerInterface, + signer, call: extrinsic, txCounter: txCounter || (await getNextNonce(didUri)), submitter: submitterAccount, @@ -304,7 +304,7 @@ export async function authorizeBatch({ const { verificationRelationship } = group - const signer = await Signers.selectSigner( + const signer = await Signers.selectSigner>( signers, verifiableOnChain(), byDid(didDocument as DidDocument, { verificationRelationship }) @@ -322,7 +322,7 @@ export async function authorizeBatch({ return generateDidAuthenticatedTx({ did: didUri, - signer: signer as SignerInterface, + signer, call, txCounter, submitter, diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 1a2f1593c..56549f22e 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -306,11 +306,14 @@ export interface SignerSelector { * @param selectors One or more selector callbacks, receiving a signer as input and returning `true` in case it meets selection criteria. * @returns An array of those signers for which all selectors returned `true`. */ -export async function selectSigners( - signers: readonly T[], +export async function selectSigners< + SelectedSigners extends AllSigners, // eslint-disable-line no-use-before-define + AllSigners extends SignerInterface = SignerInterface +>( + signers: readonly AllSigners[], ...selectors: readonly SignerSelector[] -): Promise { - return signers.filter((signer) => { +): Promise { + return signers.filter((signer): signer is SelectedSigners => { return selectors.every((selector) => selector(signer)) }) } @@ -322,11 +325,14 @@ export async function selectSigners( * @param selectors One or more selector callbacks, receiving a signer as input and returning `true` in case it meets selection criteria. * @returns The first signer for which all selectors returned `true`, or `undefined` if none meet selection criteria. */ -export async function selectSigner( - signers: readonly T[], +export async function selectSigner< + SelectedSigner extends AllSigners, // eslint-disable-line no-use-before-define + AllSigners extends SignerInterface = SignerInterface +>( + signers: readonly AllSigners[], ...selectors: readonly SignerSelector[] -): Promise { - return signers.find((signer) => { +): Promise { + return signers.find((signer): signer is SelectedSigner => { return selectors.every((selector) => selector(signer)) }) } From ec004412fde02a6b49fd8d06252d5b1e471e6fda Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 25 Oct 2023 14:55:20 +0200 Subject: [PATCH 12/16] refactor: rename signer parameter keyUri to id --- packages/did/src/Did.signature.spec.ts | 2 +- packages/utils/src/Signers.ts | 64 +++++++++++++------------- tests/bundle/bundle-test.ts | 6 +-- tests/testUtils/TestUtils.ts | 4 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 77adface4..73e3ea40b 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -302,7 +302,7 @@ describe('full DID', () => { } signer = await Signers.signerFromKeypair({ keypair, - keyUri: `${did.id}#0x12345`, + id: `${did.id}#0x12345`, algorithm: 'Ed25519', }) }) diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 56549f22e..a1975e905 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -54,23 +54,23 @@ export const DID_PALLET_SUPPORTED_ALGORITHMS = Object.freeze([ * The signature has a recovery bit appended to the end, allowing public key recovery. * * @param input Holds all function arguments. - * @param input.keyUri Sets the signer's id property. + * @param input.id Sets the signer's id property. * @param input.secretKey A 32 byte ECDSA secret key on the secp256k1 curve. * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making ECDSA signatures with recovery bit added. */ export async function polkadotEcdsaSigner({ secretKey, - keyUri, // TODO: I think this should just be called id + id, }: { - keyUri: Id + id: Id secretKey: Uint8Array publicKey?: Uint8Array }): Promise< SignerInterface > { return { - id: keyUri, + id, algorithm: ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, sign: async ({ data }) => { return secp256k1Sign(data, { secretKey }, 'blake2') @@ -83,21 +83,21 @@ export async function polkadotEcdsaSigner({ * The signature has a recovery bit appended to the end, allowing public key recovery. * * @param input Holds all function arguments. - * @param input.keyUri Sets the signer's id property. + * @param input.id Sets the signer's id property. * @param input.secretKey A 32 byte ECDSA secret key on the secp256k1 curve. * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making ECDSA signatures with recovery bit added. */ export async function ethereumEcdsaSigner({ secretKey, - keyUri, + id, }: { - keyUri: Id + id: Id secretKey: Uint8Array publicKey?: Uint8Array }): Promise> { return { - id: keyUri, + id, algorithm: ALGORITHMS.ECRECOVER_SECP256K1_KECCAK, sign: async ({ data }) => { return secp256k1Sign(data, { secretKey }, 'keccak') @@ -109,21 +109,21 @@ export async function ethereumEcdsaSigner({ * Signer that produces an ES256K signature over the message. * * @param input Holds all function arguments. - * @param input.keyUri Sets the signer's id property. + * @param input.id Sets the signer's id property. * @param input.secretKey A 32 byte ECDSA secret key on the secp256k1 curve. * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making ES256K signatures. */ export async function es256kSigner({ secretKey, - keyUri, + id, }: { - keyUri: Id + id: Id secretKey: Uint8Array publicKey?: Uint8Array }): Promise> { // only exists to map secretKey to seed - return es256kSignerWrapped({ seed: secretKey, keyUri }) as Promise< + return es256kSignerWrapped({ seed: secretKey, keyUri: id }) as Promise< SignerInterface > } @@ -132,23 +132,23 @@ export async function es256kSigner({ * Signer that produces an Ed25519 signature over the message. * * @param input Holds all function arguments. - * @param input.keyUri Sets the signer's id property. + * @param input.id Sets the signer's id property. * @param input.secretKey A 32 byte Ed25519 secret key. Some key representations append the public key to the private key; to allow these, all bytes after the 32nd byte will be dropped. * @param input.publicKey The corresponding public key. May be omitted. * @returns A signer interface capable of making Ed25519 signatures. */ export async function ed25519Signer({ secretKey, - keyUri, + id, }: { - keyUri: Id + id: Id secretKey: Uint8Array publicKey?: Uint8Array }): Promise> { // polkadot ed25519 private keys are a concatenation of private and public key for some reason return ed25519SignerWrapped({ seed: secretKey.slice(0, 32), - keyUri, + keyUri: id, }) as Promise> } @@ -156,23 +156,23 @@ export async function ed25519Signer({ * Signer that produces an Sr25519 signature over the message. * * @param input Holds all function arguments. - * @param input.keyUri Sets the signer's id property. + * @param input.id Sets the signer's id property. * @param input.secretKey A 64 byte Sr25519 secret key. * @param input.publicKey The corresponding 32 byte public key. * @returns A signer interface capable of making Sr25519 signatures. */ export async function sr25519Signer({ secretKey, - keyUri, + id, publicKey, }: { publicKey: Uint8Array secretKey: Uint8Array - keyUri: Id + id: Id }): Promise> { await cryptoWaitReady() return { - id: keyUri, + id, algorithm: ALGORITHMS.SR25519, sign: async ({ data }: { data: Uint8Array }) => { return sr25519Sign(data, { secretKey, publicKey }) @@ -199,33 +199,33 @@ const signerFactory = { * Creates a signer interface based on an existing keypair and an algorithm descriptor. * * @param input Holds all function arguments. - * @param input.keyUri Sets the signer's id property. + * @param input.id Sets the signer's id property. * @param input.keypair A polkadot {@link KeyringPair} or combination of `secretKey` & `publicKey`. * @param input.algorithm An algorithm identifier from the {@link ALGORITHMS} map. * @returns A signer interface. */ export async function signerFromKeypair({ - keyUri, + id, keypair, algorithm, }: { keypair: Keypair | KeyringPair algorithm: Alg - keyUri?: Id + id?: Id }): Promise> { const makeSigner = signerFactory[algorithm] as (x: { secretKey: Uint8Array publicKey: Uint8Array - keyUri: Id + id: Id }) => Promise> if (typeof makeSigner !== 'function') { throw new Error(`unknown algorithm ${algorithm}`) } if (!('secretKey' in keypair) && 'encodePkcs8' in keypair) { - const id = keyUri ?? (keypair.address as Id) + const signerId = id ?? (keypair.address as Id) return { - id, + id: signerId, algorithm, sign: async (signData) => { // TODO: can probably be optimized; but care must be taken to respect keyring locking @@ -233,7 +233,7 @@ export async function signerFromKeypair({ const { sign } = await makeSigner({ secretKey, publicKey: keypair.publicKey, - keyUri: id, + id: signerId, }) return sign(signData) }, @@ -244,7 +244,7 @@ export async function signerFromKeypair({ return makeSigner({ secretKey, publicKey, - keyUri: keyUri ?? (encodeAddress(publicKey, 38) as Id), + id: id ?? (encodeAddress(publicKey, 38) as Id), }) } @@ -270,17 +270,17 @@ function algsForKeyType(keyType: string): string[] { * Based on an existing keypair and its type, creates all available signers that work with this key type. * * @param input Holds all function arguments. - * @param input.keyUri Sets the signer's id property. + * @param input.id Sets the signer's id property. * @param input.keypair A polkadot {@link KeyringPair} or combination of `secretKey` & `publicKey`. * @param input.type If `keypair` is not a {@link KeyringPair}, provide the key type here; otherwise, this is ignored. * @returns An array of signer interfaces based on the keypair and type. */ export async function getSignersForKeypair({ - keyUri, + id, keypair, type = (keypair as KeyringPair).type, }: { - keyUri?: Id + id?: Id keypair: Keypair | KeyringPair type?: string }): Promise>> { @@ -290,7 +290,7 @@ export async function getSignersForKeypair({ const algorithms = algsForKeyType(type) return Promise.all( algorithms.map>>(async (algorithm) => { - return signerFromKeypair({ keypair, keyUri, algorithm }) + return signerFromKeypair({ keypair, id, algorithm }) }) ) } diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 3138f879f..63e8faf12 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -55,7 +55,7 @@ async function makeSigningKeypair( didDocument.verificationMethod?.map(({ id }) => kilt.Utils.Signers.getSignersForKeypair({ keypair, - keyUri: `${didDocument.id}${id}`, + id: `${didDocument.id}${id}`, }) ) ?? [] ) @@ -63,7 +63,7 @@ async function makeSigningKeypair( } const storeDidSigners = await kilt.Utils.Signers.getSignersForKeypair({ keypair, - keyUri: keypair.address, + id: keypair.address, }) return { @@ -92,7 +92,7 @@ async function createFullDidFromKeypair( const api = ConfigService.get('api') const signers = await kilt.Utils.Signers.getSignersForKeypair({ keypair, - keyUri: keypair.address, + id: keypair.address, }) const storeTx = await Did.getStoreTx( diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 23f20464f..e3f4d62dd 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -134,7 +134,7 @@ export async function makeStoreDidSigner( ): Promise> { const signers = await Signers.getSignersForKeypair({ keypair, - keyUri: keypair.address, + id: keypair.address, }) const signer = await Signers.selectSigner( signers, @@ -172,7 +172,7 @@ export async function makeSigningKeyTool( didDocument.verificationMethod?.map(({ id }) => Signers.getSignersForKeypair({ keypair, - keyUri: `${didDocument.id}${id}`, + id: `${didDocument.id}${id}`, }) ) ?? [] ) From edc234daf26efcf2ac4665416381d973f06e5275 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 25 Oct 2023 15:48:32 +0200 Subject: [PATCH 13/16] chore: various review suggestions --- packages/did/src/DidDetails/FullDidDetails.ts | 65 +++++++++---------- packages/utils/src/Signers.ts | 12 ++-- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 6d1991cf3..fe6b0fc81 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -113,6 +113,26 @@ async function getNextNonce(did: Did): Promise { return increaseNonce(currentNonce) } +async function conditionalLoadDocument( + didOrDidDocument: Did | DidDocument +): Promise { + if (typeof didOrDidDocument === 'string') { + const { didDocument, didDocumentMetadata } = await resolve(didOrDidDocument) + if (!didDocument || didDocumentMetadata.deactivated === true) { + throw new SDKErrors.DidNotFoundError('Failed to resolve signer DID') + } + return didDocument + } + if (typeof didOrDidDocument.id === 'string') { + return didOrDidDocument + } + throw new SDKErrors.InvalidDidFormatError( + `Expected a valid DID or DID Document, got ${JSON.stringify( + didOrDidDocument + )}` + ) +} + const { verifiableOnChain, byDid } = Signers.select /** @@ -137,16 +157,9 @@ export async function authorizeTx( txCounter?: BN } = {} ): Promise { - let didUri: Did - let didDocument: DidDocument | undefined - if (typeof did === 'string') { - didUri = did - } else { - didUri = did.id - didDocument = did - } + const didDocument = await conditionalLoadDocument(did) - if (parse(didUri).type === 'light') { + if (parse(didDocument.id).type === 'light') { throw new SDKErrors.DidError( `An extrinsic can only be authorized with a full DID, not with "${did}"` ) @@ -159,12 +172,6 @@ export async function authorizeTx( ) } - if (!didDocument) { - didDocument = (await resolve(didUri)).didDocument as DidDocument - } - if (!didDocument?.id) { - throw new SDKErrors.DidNotFoundError('failed to resolve signer DID') - } const signer = await Signers.selectSigner>( signers, verifiableOnChain(), @@ -182,10 +189,10 @@ export async function authorizeTx( } return generateDidAuthenticatedTx({ - did: didUri, + did: didDocument.id, signer, call: extrinsic, - txCounter: txCounter || (await getNextNonce(didUri)), + txCounter: txCounter || (await getNextNonce(didDocument.id)), submitter: submitterAccount, }) } @@ -265,16 +272,10 @@ export async function authorizeBatch({ ) } - let didUri: Did - let didDocument: DidDocument | undefined - if (typeof did === 'string') { - didUri = did - } else { - didUri = did.id - didDocument = did - } + // resolve DID document beforehand to avoid resolving in loop + const didDocument = await conditionalLoadDocument(did) - if (parse(didUri).type === 'light') { + if (parse(didDocument.id).type === 'light') { throw new SDKErrors.DidError( `An extrinsic can only be authorized with a full DID, not with "${did}"` ) @@ -287,15 +288,7 @@ export async function authorizeBatch({ } const groups = groupExtrinsicsByVerificationRelationship(extrinsics) - const firstNonce = nonce || (await getNextNonce(didUri)) - - // resolve DID document beforehand to avoid resolving in loop - if (!didDocument) { - didDocument = (await resolve(didUri)).didDocument - } - if (typeof didDocument?.id !== 'string') { - throw new SDKErrors.DidNotFoundError('failed to resolve signer DID') - } + const firstNonce = nonce || (await getNextNonce(didDocument.id)) const promises = groups.map(async (group, batchIndex) => { const list = group.extrinsics @@ -321,7 +314,7 @@ export async function authorizeBatch({ } return generateDidAuthenticatedTx({ - did: didUri, + did: didDocument.id, signer, call, txCounter, diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index a1975e905..5b9a1f8d3 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -313,9 +313,9 @@ export async function selectSigners< signers: readonly AllSigners[], ...selectors: readonly SignerSelector[] ): Promise { - return signers.filter((signer): signer is SelectedSigners => { - return selectors.every((selector) => selector(signer)) - }) + return signers.filter((signer): signer is SelectedSigners => + selectors.every((selector) => selector(signer)) + ) } /** @@ -332,9 +332,9 @@ export async function selectSigner< signers: readonly AllSigners[], ...selectors: readonly SignerSelector[] ): Promise { - return signers.find((signer): signer is SelectedSigner => { - return selectors.every((selector) => selector(signer)) - }) + return signers.find((signer): signer is SelectedSigner => + selectors.every((selector) => selector(signer)) + ) } /** From b39f32b314e79479b55b0d604cf6a25474fbd516 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 25 Oct 2023 16:38:22 +0200 Subject: [PATCH 14/16] chore: rename byId to bySignerId --- packages/did/src/Did.chain.ts | 5 ++++- packages/did/src/Did.utils.ts | 2 +- packages/utils/src/Signers.ts | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 35c234458..9f6f8c442 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -421,7 +421,10 @@ export async function getStoreTx( const signer = await Signers.selectSigner( signers, Signers.select.verifiableOnChain(), - Signers.select.byId([did, Crypto.u8aToHex(authenticationKey.publicKey)]) + Signers.select.bySignerId([ + did, + Crypto.u8aToHex(authenticationKey.publicKey), + ]) ) if (!signer) { diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index bf44a0d40..6a09aeea7 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -339,7 +339,7 @@ export function getFullDid( * Builds the of a full DID if it is created with the authentication verification method derived from the provided public key. * * @param verificationMethod The DID verification method. - * @returns The expected full DID . + * @returns The expected full DID. */ export function getFullDidFromVerificationMethod( verificationMethod: Pick diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 5b9a1f8d3..aac797d2b 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -343,7 +343,7 @@ export async function selectSigner< * @param ids Allowed signer/key ids to filter for. * @returns A selector identifying signers whose id property is in `ids`. */ -function byId(ids: readonly string[]): SignerSelector { +function bySignerId(ids: readonly string[]): SignerSelector { return ({ id }) => ids.includes(id) } /** @@ -431,7 +431,7 @@ function verifiableOnChain(): SignerSelector { } export const select = { - byId, + bySignerId, byAlgorithm, byDid, verifiableOnChain, From 8d938417935e6e43ded965d6e5b77bdfe6847f6d Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Thu, 26 Oct 2023 19:04:06 +0200 Subject: [PATCH 15/16] chore: improve an error message --- packages/did/src/Did.chain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 9f6f8c442..3b03d0782 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -471,7 +471,7 @@ export function didSignatureToChain({ : lower if (!isValidVerificationMethodType(keyType)) { throw new SDKErrors.DidError( - `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` + `encodedDidSignature requires a signature algorithm in ${Signers.DID_PALLET_SUPPORTED_ALGORITHMS}. A key of type "${algorithm}" was used instead` ) } From 29d153468384486d301c8796423968dfe0f36f76 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 30 Oct 2023 13:25:04 +0100 Subject: [PATCH 16/16] feat: use newest suites release * allows removing reimplementation signer factories * allows stronger typing of algorithm identifiers --- packages/did/src/Did.chain.ts | 12 +- packages/did/src/DidDetails/FullDidDetails.ts | 6 +- packages/utils/package.json | 7 +- packages/utils/src/Signers.ts | 109 ++++-------------- yarn.lock | 56 ++++----- 5 files changed, 64 insertions(+), 126 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 3b03d0782..59258dce2 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -478,18 +478,18 @@ export function didSignatureToChain({ return { [keyType]: signature } as EncodedSignature } +export type DidPalletSigner = SignerInterface< + Signers.DidPalletSupportedAlgorithms, + DidUrl +> + export type AuthorizeCallInput = { did: Did txCounter: AnyNumber call: Extrinsic submitter: KiltAddress blockNumber?: AnyNumber - signer: SignerInterface< - | typeof Signers.ALGORITHMS.SR25519 - | typeof Signers.ALGORITHMS.ED25519 - | typeof Signers.ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, - DidUrl - > + signer: DidPalletSigner } /** diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index fe6b0fc81..108099494 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -12,7 +12,6 @@ import { BN } from '@polkadot/util' import type { DidDocument, Did, - DidUrl, KiltAddress, SignatureVerificationRelationship, SignerInterface, @@ -23,6 +22,7 @@ import { SDKErrors, Signers } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' import { + DidPalletSigner, documentFromChain, generateDidAuthenticatedTx, toChain, @@ -172,7 +172,7 @@ export async function authorizeTx( ) } - const signer = await Signers.selectSigner>( + const signer = await Signers.selectSigner( signers, verifiableOnChain(), byDid(didDocument, { verificationRelationship }) @@ -297,7 +297,7 @@ export async function authorizeBatch({ const { verificationRelationship } = group - const signer = await Signers.selectSigner>( + const signer = await Signers.selectSigner( signers, verifiableOnChain(), byDid(didDocument as DidDocument, { verificationRelationship }) diff --git a/packages/utils/package.json b/packages/utils/package.json index a95aeea23..457473926 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -34,9 +34,10 @@ "typescript": "^4.8.3" }, "dependencies": { - "@kiltprotocol/ecdsa-secp256k1-jcs-2023": "0.0.1-rc.2", - "@kiltprotocol/eddsa-jcs-2022": "0.0.1-rc.2", - "@kiltprotocol/sr25519-jcs-2023": "0.0.1-rc.2", + "@kiltprotocol/eddsa-jcs-2022": "0.1.0-rc.1", + "@kiltprotocol/es256k-jcs-2023": "0.1.0-rc.1", + "@kiltprotocol/jcs-data-integrity-proofs-common": "0.1.0-rc.1", + "@kiltprotocol/sr25519-jcs-2023": "0.1.0-rc.1", "@kiltprotocol/types": "workspace:*", "@polkadot/api": "^10.4.0", "@polkadot/keyring": "^12.0.0", diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index aac797d2b..d4e141f9d 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -7,23 +7,24 @@ import { decodePair } from '@polkadot/keyring/pair/decode' import { - cryptoWaitReady, encodeAddress, randomAsHex, secp256k1Sign, - sr25519Sign, } from '@polkadot/util-crypto' import type { Keypair } from '@polkadot/util-crypto/types' import { - createSigner as es256kSignerWrapped, + createSigner as es256kSigner, cryptosuite as es256kSuite, -} from '@kiltprotocol/ecdsa-secp256k1-jcs-2023' +} from '@kiltprotocol/es256k-jcs-2023' import { - createSigner as ed25519SignerWrapped, + createSigner as ed25519Signer, cryptosuite as ed25519Suite, } from '@kiltprotocol/eddsa-jcs-2022' -import { cryptosuite as sr25519Suite } from '@kiltprotocol/sr25519-jcs-2023' +import { + cryptosuite as sr25519Suite, + createSigner as sr25519Signer, +} from '@kiltprotocol/sr25519-jcs-2023' import type { SignerInterface, @@ -36,8 +37,8 @@ import type { import { DidError } from './SDKErrors.js' export const ALGORITHMS = Object.freeze({ - ECRECOVER_SECP256K1_BLAKE2B: 'Ecrecover-Secp256k1-Blake2b', // could also be called ES256K-R-Blake2b - ECRECOVER_SECP256K1_KECCAK: 'Ecrecover-Secp256k1-Keccak', // could also be called ES256K-R-Keccak + ECRECOVER_SECP256K1_BLAKE2B: 'Ecrecover-Secp256k1-Blake2b' as const, // could also be called ES256K-R-Blake2b + ECRECOVER_SECP256K1_KECCAK: 'Ecrecover-Secp256k1-Keccak' as const, // could also be called ES256K-R-Keccak ES256K: es256kSuite.requiredAlgorithm, SR25519: sr25519Suite.requiredAlgorithm, ED25519: ed25519Suite.requiredAlgorithm, @@ -49,6 +50,12 @@ export const DID_PALLET_SUPPORTED_ALGORITHMS = Object.freeze([ ALGORITHMS.SR25519, ]) +export type KnownAlgorithms = typeof ALGORITHMS[keyof typeof ALGORITHMS] +export type DidPalletSupportedAlgorithms = + typeof DID_PALLET_SUPPORTED_ALGORITHMS[number] + +export { ed25519Signer, es256kSigner } + /** * Signer that produces an ECDSA signature over a Blake2b-256 digest of the message using the secp256k1 curve. * The signature has a recovery bit appended to the end, allowing public key recovery. @@ -105,81 +112,6 @@ export async function ethereumEcdsaSigner({ } } -/** - * Signer that produces an ES256K signature over the message. - * - * @param input Holds all function arguments. - * @param input.id Sets the signer's id property. - * @param input.secretKey A 32 byte ECDSA secret key on the secp256k1 curve. - * @param input.publicKey The corresponding public key. May be omitted. - * @returns A signer interface capable of making ES256K signatures. - */ -export async function es256kSigner({ - secretKey, - id, -}: { - id: Id - secretKey: Uint8Array - publicKey?: Uint8Array -}): Promise> { - // only exists to map secretKey to seed - return es256kSignerWrapped({ seed: secretKey, keyUri: id }) as Promise< - SignerInterface - > -} - -/** - * Signer that produces an Ed25519 signature over the message. - * - * @param input Holds all function arguments. - * @param input.id Sets the signer's id property. - * @param input.secretKey A 32 byte Ed25519 secret key. Some key representations append the public key to the private key; to allow these, all bytes after the 32nd byte will be dropped. - * @param input.publicKey The corresponding public key. May be omitted. - * @returns A signer interface capable of making Ed25519 signatures. - */ -export async function ed25519Signer({ - secretKey, - id, -}: { - id: Id - secretKey: Uint8Array - publicKey?: Uint8Array -}): Promise> { - // polkadot ed25519 private keys are a concatenation of private and public key for some reason - return ed25519SignerWrapped({ - seed: secretKey.slice(0, 32), - keyUri: id, - }) as Promise> -} - -/** - * Signer that produces an Sr25519 signature over the message. - * - * @param input Holds all function arguments. - * @param input.id Sets the signer's id property. - * @param input.secretKey A 64 byte Sr25519 secret key. - * @param input.publicKey The corresponding 32 byte public key. - * @returns A signer interface capable of making Sr25519 signatures. - */ -export async function sr25519Signer({ - secretKey, - id, - publicKey, -}: { - publicKey: Uint8Array - secretKey: Uint8Array - id: Id -}): Promise> { - await cryptoWaitReady() - return { - id, - algorithm: ALGORITHMS.SR25519, - sign: async ({ data }: { data: Uint8Array }) => { - return sr25519Sign(data, { secretKey, publicKey }) - }, - } -} - function extractPk(pair: KeyringPair): Uint8Array { const pw = randomAsHex() const encoded = pair.encodePkcs8(pw) @@ -204,7 +136,10 @@ const signerFactory = { * @param input.algorithm An algorithm identifier from the {@link ALGORITHMS} map. * @returns A signer interface. */ -export async function signerFromKeypair({ +export async function signerFromKeypair< + Alg extends KnownAlgorithms, + Id extends string +>({ id, keypair, algorithm, @@ -248,7 +183,7 @@ export async function signerFromKeypair({ }) } -function algsForKeyType(keyType: string): string[] { +function algsForKeyType(keyType: string): KnownAlgorithms[] { switch (keyType.toLowerCase()) { case 'ed25519': return [ALGORITHMS.ED25519] @@ -283,13 +218,13 @@ export async function getSignersForKeypair({ id?: Id keypair: Keypair | KeyringPair type?: string -}): Promise>> { +}): Promise>> { if (!type) { throw new Error('type is required if keypair.type is not given') } const algorithms = algsForKeyType(type) return Promise.all( - algorithms.map>>(async (algorithm) => { + algorithms.map(async (algorithm) => { return signerFromKeypair({ keypair, id, algorithm }) }) ) diff --git a/yarn.lock b/yarn.lock index 6d27e7cf4..bccf8eb09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2037,34 +2037,35 @@ __metadata: languageName: unknown linkType: soft -"@kiltprotocol/ecdsa-secp256k1-jcs-2023@npm:0.0.1-rc.2": - version: 0.0.1-rc.2 - resolution: "@kiltprotocol/ecdsa-secp256k1-jcs-2023@npm:0.0.1-rc.2" +"@kiltprotocol/eddsa-jcs-2022@npm:0.1.0-rc.1": + version: 0.1.0-rc.1 + resolution: "@kiltprotocol/eddsa-jcs-2022@npm:0.1.0-rc.1" dependencies: - "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.0.1-rc.2 - "@noble/curves": ^1.1.0 + "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.1.0-rc.1 + "@noble/curves": ^1.0.0 "@scure/base": ^1.1.1 - checksum: ba083c296ddfb54f917e3bb6751e5e69089d11a22dfa395a8b2c912edb5282abf6837823541040c91e889e807672df5374a2523f9bab9908cb672cbbf3cf8f22 + checksum: c3127e4f98e37216752e52314aa3e173d32327819fc88ba69fdba7bfbb014ca61cf271eb01864274c1c25813edbf07975c8dc829708dc525f7425ea2a38ab7d9 languageName: node linkType: hard -"@kiltprotocol/eddsa-jcs-2022@npm:0.0.1-rc.2": - version: 0.0.1-rc.2 - resolution: "@kiltprotocol/eddsa-jcs-2022@npm:0.0.1-rc.2" +"@kiltprotocol/es256k-jcs-2023@npm:0.1.0-rc.1": + version: 0.1.0-rc.1 + resolution: "@kiltprotocol/es256k-jcs-2023@npm:0.1.0-rc.1" dependencies: - "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.0.1-rc.2 - "@noble/curves": ^1.1.0 + "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.1.0-rc.1 + "@noble/curves": ^1.0.0 "@scure/base": ^1.1.1 - checksum: dd96d116e4580e3d6aa611bbe6d0e3010c69032073ee27a69e1867bcd8fbc95e7054710c81655584575b36e9eedd37f8e530fd55010891934f5afba43fbd801d + checksum: 843437a6806c10728fedc6cfa45f408aa9278263d64fce7c5057515d06fed74bc9ea702d932cc24543daff66a1b9935389b0a1453ea45d59111b46b59b19ad56 languageName: node linkType: hard -"@kiltprotocol/jcs-data-integrity-proofs-common@npm:^0.0.1-rc.2": - version: 0.0.1-rc.2 - resolution: "@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.0.1-rc.2" +"@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.1.0-rc.1, @kiltprotocol/jcs-data-integrity-proofs-common@npm:^0.1.0-rc.1": + version: 0.1.0-rc.1 + resolution: "@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.1.0-rc.1" dependencies: canonicalize: ^2.0.0 - checksum: f342051b94d4eff1cfbe2ed2445432e91180d4fcdb5a354312f197fd292b29109b9bd2d9de1bcfec53a1ecd1a61155badb54c761a2a4de95e33205ebc9c3de2f + multibase: ^4.0.6 + checksum: fefc3c86bdb0a732f5851155cedf1743eedace4cf03fa5fbeb37f321c3cf87e9ac1704328e0d69d3cb80203e9afc07b6849907816c402bcd90b7c18b99177d40 languageName: node linkType: hard @@ -2102,14 +2103,14 @@ __metadata: languageName: unknown linkType: soft -"@kiltprotocol/sr25519-jcs-2023@npm:0.0.1-rc.2": - version: 0.0.1-rc.2 - resolution: "@kiltprotocol/sr25519-jcs-2023@npm:0.0.1-rc.2" +"@kiltprotocol/sr25519-jcs-2023@npm:0.1.0-rc.1": + version: 0.1.0-rc.1 + resolution: "@kiltprotocol/sr25519-jcs-2023@npm:0.1.0-rc.1" dependencies: - "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.0.1-rc.2 - "@polkadot/util-crypto": ^12.3.2 + "@kiltprotocol/jcs-data-integrity-proofs-common": ^0.1.0-rc.1 + "@polkadot/util-crypto": ^12.0.1 "@scure/base": ^1.1.1 - checksum: 573ba2dee062d0af9f18b3805a8156987c613af22f271df5c6f28b332e428efaebedc248e2a88aeffde43bfd224ab1df66928949ebce09d871fd517931ef1d52 + checksum: 8253c03b8c9daef61bc2155ee64894a568f4b591c941dc0bb668839475ed8d8fcf1048339710805e3046cb3ea603ed58c5e857887c4c67b602af098c2a92fb52 languageName: node linkType: hard @@ -2140,9 +2141,10 @@ __metadata: version: 0.0.0-use.local resolution: "@kiltprotocol/utils@workspace:packages/utils" dependencies: - "@kiltprotocol/ecdsa-secp256k1-jcs-2023": 0.0.1-rc.2 - "@kiltprotocol/eddsa-jcs-2022": 0.0.1-rc.2 - "@kiltprotocol/sr25519-jcs-2023": 0.0.1-rc.2 + "@kiltprotocol/eddsa-jcs-2022": 0.1.0-rc.1 + "@kiltprotocol/es256k-jcs-2023": 0.1.0-rc.1 + "@kiltprotocol/jcs-data-integrity-proofs-common": 0.1.0-rc.1 + "@kiltprotocol/sr25519-jcs-2023": 0.1.0-rc.1 "@kiltprotocol/types": "workspace:*" "@polkadot/api": ^10.4.0 "@polkadot/keyring": ^12.0.0 @@ -2185,7 +2187,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.0.0, @noble/curves@npm:^1.1.0": +"@noble/curves@npm:1.0.0, @noble/curves@npm:^1.0.0": version: 1.0.0 resolution: "@noble/curves@npm:1.0.0" dependencies: @@ -2517,7 +2519,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/util-crypto@npm:12.2.1, @polkadot/util-crypto@npm:^12.0.0, @polkadot/util-crypto@npm:^12.2.1, @polkadot/util-crypto@npm:^12.3.2": +"@polkadot/util-crypto@npm:12.2.1, @polkadot/util-crypto@npm:^12.0.0, @polkadot/util-crypto@npm:^12.0.1, @polkadot/util-crypto@npm:^12.2.1": version: 12.2.1 resolution: "@polkadot/util-crypto@npm:12.2.1" dependencies: