diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index f4942a116..1a1388542 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -43,11 +43,12 @@ 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' @@ -659,11 +660,6 @@ export type AttestationHandler = ( timestamp?: number }> -export interface DidSigner { - did: Did - signer: SignExtrinsicCallback -} - export type TxHandler = { account: KiltAddress signAndSubmit?: AttestationHandler @@ -671,7 +667,7 @@ export type TxHandler = { } export type IssueOpts = { - didSigner: DidSigner + signers: readonly SignerInterface[] transactionHandler: TxHandler } & Parameters[4] @@ -695,8 +691,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 +702,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..dc1d59fd9 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,38 @@ 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: readonly 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 SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: delegateDid.id, + verificationRelationship: 'authentication', + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + }, + }) } - return Did.didSignatureToChain( - verificationMethod, - delegateSignature.signature - ) + const signature = await signer.sign({ + data: this.generateHash(), + }) + + return Did.didSignatureToChain({ + algorithm: signer.algorithm, + signature, + }) } /** diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index d8ebc0f1e..59258dce2 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -19,19 +19,16 @@ import type { BN, Deposit, Did, + DidUrl, KiltAddress, Service, - SignatureVerificationRelationship, - SignExtrinsicCallback, - SignRequestData, - SignResponseData, + SignerInterface, SubmittableExtrinsic, UriFragment, - VerificationMethod, } 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 +40,6 @@ import type { import { isValidVerificationMethodType } from './DidDetails/DidDetails.js' import { - multibaseKeyToDidKey, keypairToMultibaseKey, getAddressFromVerificationMethod, getFullDid, @@ -296,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 @@ -331,14 +319,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. * @@ -352,14 +332,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') @@ -437,9 +418,30 @@ 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.bySignerId([ + 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, @@ -447,9 +449,47 @@ export async function getStoreTx( return api.tx.did.create(encoded, encodedSignature) } -export interface SigningOptions { - sign: SignExtrinsicCallback - verificationRelationship: SignatureVerificationRelationship +/** + * 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 signature algorithm in ${Signers.DID_PALLET_SUPPORTED_ALGORITHMS}. A key of type "${algorithm}" was used instead` + ) + } + + 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: DidPalletSigner } /** @@ -458,8 +498,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,13 +507,12 @@ export interface SigningOptions { */ export async function generateDidAuthenticatedTx({ did, - verificationRelationship, - sign, + signer, call, txCounter, submitter, blockNumber, -}: AuthorizeCallInput & SigningOptions): Promise { +}: AuthorizeCallInput): Promise { const api = ConfigService.get('api') const signableCall = api.registry.createType( @@ -487,38 +525,12 @@ 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. - * - * @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. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ -export function didSignatureToChain( - { publicKeyMultibase }: VerificationMethod, - signature: Uint8Array -): EncodedSignature { - const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) - 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 e5672ca8f..73e3ea40b 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -9,13 +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' @@ -24,7 +24,6 @@ import { makeSigningKeyTool } from '../../../tests/testUtils' import { isDidSignature, signatureFromJson, - signatureToJson, verifyDidSignature, } from './Did.signature' import { dereference, SupportedContentType } from './DidResolver/DidResolver' @@ -39,14 +38,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 +75,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 +91,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 +111,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 +126,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 +141,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 +152,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 +160,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 +176,15 @@ 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 +199,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 +222,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 +243,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 +274,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + signerUrl: authenticationSigner.id as DidUrl, expectedSigner, expectedVerificationRelationship: 'authentication', }) @@ -303,8 +285,8 @@ describe('light DID', () => { describe('full DID', () => { let keypair: KiltKeyringPair let did: DidDocument - let sign: SignCallback - beforeAll(() => { + let signer: SignerInterface + beforeAll(async () => { keypair = Crypto.makeKeypairFromSeed() did = { id: `did:kilt:${keypair.address}`, @@ -318,14 +300,10 @@ 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, + id: `${did.id}#0x12345`, + algorithm: 'Ed25519', }) }) @@ -353,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() @@ -370,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() @@ -392,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() @@ -413,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() @@ -430,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( @@ -455,7 +423,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedSigner, expectedVerificationRelationship: 'authentication', }) @@ -465,7 +433,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: signer.id, expectedSigner, allowUpgraded: true, expectedVerificationRelationship: 'authentication', 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/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 10e39ebdc..95f8b3ef1 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,14 @@ 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(), + resolveRepresentation: jest.fn(), + } +}) jest.mock('../Did.chain') jest .mocked(generateDidAuthenticatedTx) @@ -45,14 +53,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 +68,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 +86,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 +123,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 +151,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..108099494 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -10,22 +10,25 @@ 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 { + DidPalletSigner, documentFromChain, generateDidAuthenticatedTx, 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 +113,43 @@ 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 + /** * 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: readonly SignerInterface[], submitterAccount: KiltAddress, { txCounter, @@ -132,7 +157,9 @@ export async function authorizeTx( txCounter?: BN } = {} ): Promise { - if (parse(did).type === 'light') { + const didDocument = await conditionalLoadDocument(did) + + if (parse(didDocument.id).type === 'light') { throw new SDKErrors.DidError( `An extrinsic can only be authorized with a full DID, not with "${did}"` ) @@ -145,12 +172,27 @@ export async function authorizeTx( ) } + const signer = await Signers.selectSigner( + signers, + verifiableOnChain(), + byDid(didDocument, { verificationRelationship }) + ) + if (typeof signer === 'undefined') { + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: didDocument.id, + verificationRelationship, + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + }, + availableSigners: signers, + }) + } + return generateDidAuthenticatedTx({ - did, - verificationRelationship, - sign, + did: didDocument.id, + signer, call: extrinsic, - txCounter: txCounter || (await getNextNonce(did)), + txCounter: txCounter || (await getNextNonce(didDocument.id)), submitter: submitterAccount, }) } @@ -202,9 +244,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 +256,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: readonly SignerInterface[] submitter: KiltAddress }): Promise { if (extrinsics.length === 0) { @@ -230,20 +272,23 @@ export async function authorizeBatch({ ) } - if (parse(did).type === 'light') { + // resolve DID document beforehand to avoid resolving in loop + const didDocument = await conditionalLoadDocument(did) + + if (parse(didDocument.id).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(didDocument.id)) const promises = groups.map(async (group, batchIndex) => { const list = group.extrinsics @@ -252,10 +297,25 @@ export async function authorizeBatch({ const { verificationRelationship } = group + const signer = await Signers.selectSigner( + signers, + verifiableOnChain(), + byDid(didDocument as DidDocument, { verificationRelationship }) + ) + if (typeof signer === 'undefined') { + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: (didDocument as DidDocument).id, + verificationRelationship, + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + }, + availableSigners: signers, + }) + } + return generateDidAuthenticatedTx({ - did, - verificationRelationship, - sign, + did: didDocument.id, + 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..19b36b49d 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: readonly 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..490cca510 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 - selectedAttributes?: string[] + signers: readonly SignerInterface[] + selectedAttributes?: readonly string[] challenge?: string + didDocument?: DidDocument }): Promise { // filter attributes that are not in public attributes const excludedClaimProperties = selectedAttributes @@ -541,16 +548,38 @@ export async function createPresentation({ excludedClaimProperties ) - const signature = await signCallback({ + 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}` + ) + } + const signer = await Signers.selectSigner( + signers, + verifiableOnChain(), + byDid(didDoc, { verificationRelationship: 'authentication' }) + ) + if (!signer) { + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: { + did: didDoc.id, + algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS, + verificationRelationship: 'authentication', + }, + }) + } + + 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/Signers.ts b/packages/types/src/Signers.ts new file mode 100644 index 000000000..dbe15db31 --- /dev/null +++ b/packages/types/src/Signers.ts @@ -0,0 +1,15 @@ +/** + * 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< + Alg extends string = string, + Id extends string = string +> = { + algorithm: Alg + id: Id + 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/package.json b/packages/utils/package.json index 3759f261b..457473926 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -34,6 +34,10 @@ "typescript": "^4.8.3" }, "dependencies": { + "@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/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index cf9bec169..6844d45a1 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?: readonly 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 new file mode 100644 index 000000000..d4e141f9d --- /dev/null +++ b/packages/utils/src/Signers.ts @@ -0,0 +1,373 @@ +/** + * 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 { decodePair } from '@polkadot/keyring/pair/decode' +import { + encodeAddress, + randomAsHex, + secp256k1Sign, +} from '@polkadot/util-crypto' +import type { Keypair } from '@polkadot/util-crypto/types' + +import { + createSigner as es256kSigner, + cryptosuite as es256kSuite, +} from '@kiltprotocol/es256k-jcs-2023' +import { + createSigner as ed25519Signer, + cryptosuite as ed25519Suite, +} from '@kiltprotocol/eddsa-jcs-2022' +import { + cryptosuite as sr25519Suite, + createSigner as sr25519Signer, +} from '@kiltprotocol/sr25519-jcs-2023' + +import type { + SignerInterface, + DidDocument, + DidUrl, + KeyringPair, + UriFragment, +} from '@kiltprotocol/types' + +import { DidError } from './SDKErrors.js' + +export const ALGORITHMS = Object.freeze({ + 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, +}) + +export const DID_PALLET_SUPPORTED_ALGORITHMS = Object.freeze([ + ALGORITHMS.ED25519, + ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, + 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. + * + * @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 ECDSA signatures with recovery bit added. + */ +export async function polkadotEcdsaSigner({ + secretKey, + id, +}: { + id: Id + secretKey: Uint8Array + publicKey?: Uint8Array +}): Promise< + SignerInterface +> { + return { + id, + algorithm: ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, + sign: async ({ data }) => { + return secp256k1Sign(data, { secretKey }, '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 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 ECDSA signatures with recovery bit added. + */ +export async function ethereumEcdsaSigner({ + secretKey, + id, +}: { + id: Id + secretKey: Uint8Array + publicKey?: Uint8Array +}): Promise> { + return { + id, + algorithm: ALGORITHMS.ECRECOVER_SECP256K1_KECCAK, + sign: async ({ data }) => { + return secp256k1Sign(data, { secretKey }, 'keccak') + }, + } +} + +function extractPk(pair: KeyringPair): Uint8Array { + const pw = randomAsHex() + const encoded = pair.encodePkcs8(pw) + const { secretKey } = decodePair(pw, encoded) + return secretKey +} + +const signerFactory = { + [ALGORITHMS.ED25519]: ed25519Signer, + [ALGORITHMS.SR25519]: sr25519Signer, + [ALGORITHMS.ES256K]: es256kSigner, + [ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B]: polkadotEcdsaSigner, + [ALGORITHMS.ECRECOVER_SECP256K1_KECCAK]: ethereumEcdsaSigner, +} + +/** + * Creates a signer interface based on an existing keypair and an algorithm descriptor. + * + * @param input Holds all function arguments. + * @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< + Alg extends KnownAlgorithms, + Id extends string +>({ + id, + keypair, + algorithm, +}: { + keypair: Keypair | KeyringPair + algorithm: Alg + id?: Id +}): Promise> { + const makeSigner = signerFactory[algorithm] as (x: { + secretKey: Uint8Array + publicKey: Uint8Array + id: Id + }) => Promise> + if (typeof makeSigner !== 'function') { + throw new Error(`unknown algorithm ${algorithm}`) + } + + if (!('secretKey' in keypair) && 'encodePkcs8' in keypair) { + const signerId = id ?? (keypair.address as Id) + return { + id: signerId, + 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({ + secretKey, + publicKey: keypair.publicKey, + id: signerId, + }) + return sign(signData) + }, + } + } + + const { secretKey, publicKey } = keypair + return makeSigner({ + secretKey, + publicKey, + id: id ?? (encodeAddress(publicKey, 38) as Id), + }) +} + +function algsForKeyType(keyType: string): KnownAlgorithms[] { + switch (keyType.toLowerCase()) { + case 'ed25519': + return [ALGORITHMS.ED25519] + case 'sr25519': + return [ALGORITHMS.SR25519] + case 'ecdsa': + case 'secpk256k1': + return [ + ALGORITHMS.ES256K, + ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B, + ALGORITHMS.ECRECOVER_SECP256K1_KECCAK, + ] + default: + return [] + } +} + +/** + * 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.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({ + id, + keypair, + type = (keypair as KeyringPair).type, +}: { + id?: Id + keypair: Keypair | KeyringPair + type?: 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, id, algorithm }) + }) + ) +} + +export interface SignerSelector { + (signer: SignerInterface): boolean // TODO: allow async +} + +/** + * 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< + 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): signer is SelectedSigners => + selectors.every((selector) => selector(signer)) + ) +} + +/** + * 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< + 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): signer is SelectedSigner => + selectors.every((selector) => selector(signer)) + ) +} + +/** + * 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 bySignerId(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, + { + controller, + verificationRelationship, + }: { verificationRelationship?: string; controller?: string } = {} +): SignerSelector { + 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 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.` + ) + } + 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 (id.startsWith('#')) { + return `${didDocument.id}${id as UriFragment}` + } + return `${didDocument.id}#${id}` + } + let eligibleIds = eligibleVMs.map(({ id }) => absoluteId(id)) + if (typeof verificationRelationship === 'string') { + if ( + !Array.isArray(didDocument[verificationRelationship]) || + didDocument[verificationRelationship].length === 0 + ) { + throw new DidError( + `DID ${didDocument.id} not fit for signing: No verification methods available for the requested verification relationship ("${verificationRelationship}").` + ) + } + 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 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( + { controller, verificationRelationship } + )}.` + ) + } + return ({ id }) => { + return eligibleIds.includes(id as DidUrl) + } +} + +function verifiableOnChain(): SignerSelector { + return byAlgorithm(DID_PALLET_SUPPORTED_ALGORITHMS) +} + +export const select = { + bySignerId, + 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..d57a63899 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -23,10 +23,12 @@ import { ConfigService } from '@kiltprotocol/config' import * as Did from '@kiltprotocol/did' import type { DidDocument, + DidUrl, HexString, ICType, KiltAddress, KiltKeyringPair, + SignerInterface, SubmittableExtrinsic, } from '@kiltprotocol/types' import { Crypto } from '@kiltprotocol/utils' @@ -446,17 +448,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<'Sr25519', DidUrl> = { + sign: async () => new Uint8Array(32), + algorithm: 'Sr25519', + id: `${issuer}#1`, } const transactionHandler: KiltAttestationProofV1.TxHandler = { account: attester, @@ -484,7 +480,8 @@ describe('issuance', () => { let newCred: Partial = await issuanceSuite.anchorCredential( { ...toBeSigned }, - didSigner, + issuer, + [signer], transactionHandler ) newCred = await vcjs.issue({ @@ -542,7 +539,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..4516780e9 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: readonly 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..63e8faf12 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -10,10 +10,10 @@ import type { NewDidEncryptionKey } from '@kiltprotocol/did' import type { DidDocument, - KeyringPair, + DidUrl, KiltEncryptionKeypair, KiltKeyringPair, - SignCallback, + SignerInterface, } from '@kiltprotocol/types' const { kilt } = window @@ -33,56 +33,43 @@ 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 { - return async function sign({ data }) { - const signature = keypair.sign(data, { withType: false }) - return { - signature, - verificationMethod: { - publicKeyMultibase: Did.keypairToMultibaseKey(keypair), - }, - } - } -} - -function makeSigningKeypair( +async function makeSigningKeypair( seed: string, type: KiltKeyringPair['type'] = 'sr25519' -): { +): Promise<{ keypair: KiltKeyringPair - getSignCallback: (didDocument: DidDocument) => SignCallback - storeDidCallback: StoreDidCallback -} { + getSigners: ( + didDocument: DidDocument + ) => Promise>> + storeDidSigners: SignerInterface[] +}> { const keypair = Crypto.makeKeypairFromUri(seed, type) - const getSignCallback = makeSignCallback(keypair) - const storeDidCallback = makeStoreDidCallback(keypair) + + const getSigners: ( + didDocument: DidDocument + ) => Promise>> = async ( + didDocument + ) => { + return ( + await Promise.all( + didDocument.verificationMethod?.map(({ id }) => + kilt.Utils.Signers.getSignersForKeypair({ + keypair, + id: `${didDocument.id}${id}`, + }) + ) ?? [] + ) + ).flat() + } + const storeDidSigners = await kilt.Utils.Signers.getSignersForKeypair({ + keypair, + id: keypair.address, + }) return { keypair, - getSignCallback, - storeDidCallback, + getSigners, + storeDidSigners, } } @@ -103,7 +90,10 @@ async function createFullDidFromKeypair( encryptionKey: NewDidEncryptionKey ) { const api = ConfigService.get('api') - const sign = makeStoreDidCallback(keypair) + const signers = await kilt.Utils.Signers.getSignersForKeypair({ + keypair, + id: keypair.address, + }) const storeTx = await Did.getStoreTx( { @@ -113,7 +103,7 @@ async function createFullDidFromKeypair( keyAgreement: [encryptionKey], }, payer.address, - sign + signers ) await Blockchain.signAndSubmitTx(storeTx, payer) @@ -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, storeDidSigners } = await makeSigningKeypair( '//Foo', 'ed25519' ) @@ -190,7 +180,7 @@ async function runAll() { const didStoreTx = await Did.getStoreTx( { authentication: [keypair] }, payer.address, - storeDidCallback + storeDidSigners ) await Blockchain.signAndSubmitTx(didStoreTx, payer) @@ -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..11edacd7e 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: readonly 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..c8cb8b48d 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) @@ -359,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, @@ -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..b1955060e 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,21 +51,19 @@ 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 () => { 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) @@ -111,7 +107,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 +149,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 +173,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 +189,7 @@ describe('write and didDeleteTx', () => { submittable = await Did.authorizeTx( fullDid.id, call, - signCallback, + signers, paymentAccount.address ) @@ -213,7 +220,7 @@ describe('write and didDeleteTx', () => { const submittable = await Did.authorizeTx( fullDid.id, call, - signCallback, + signers, paymentAccount.address ) @@ -230,14 +237,12 @@ describe('write and didDeleteTx', () => { }) it('creates and updates DID, and then reclaims the deposit back', async () => { - const { keypair, getSignCallback, storeDidCallback } = 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) @@ -247,7 +252,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 +260,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 +285,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 +305,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 +331,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 { storeDidSigner, authentication } = await makeSigningKeyTool( + 'ed25519' + ) const { keyAgreement } = makeEncryptionKeyTool( '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ) @@ -338,7 +345,7 @@ describe('DID migration', () => { const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(storeTx, paymentAccount) @@ -387,7 +394,7 @@ describe('DID migration', () => { }) it('migrates light DID with sr25519 auth key', async () => { - const { authentication, storeDidCallback } = makeSigningKeyTool() + const { authentication, storeDidSigner } = await makeSigningKeyTool() const lightDid = Did.createLightDidDocument({ authentication, }) @@ -395,7 +402,7 @@ describe('DID migration', () => { const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(storeTx, paymentAccount) @@ -438,7 +445,9 @@ describe('DID migration', () => { }) it('migrates light DID with ed25519 auth key, encryption key, and services', async () => { - const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') + const { storeDidSigner, authentication } = await makeSigningKeyTool( + 'ed25519' + ) const { keyAgreement } = makeEncryptionKeyTool( '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' ) @@ -458,7 +467,7 @@ describe('DID migration', () => { const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(storeTx, paymentAccount) @@ -530,10 +539,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, storeDidSigner, authentication } = + await makeSigningKeyTool('ed25519') + const createTx = await Did.getStoreTx( { authentication, @@ -541,7 +552,7 @@ describe('DID authorization', () => { capabilityDelegation: authentication, }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) const didLinkedInfo = await api.call.did.query( @@ -552,6 +563,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 +572,7 @@ describe('DID authorization', () => { const tx = await Did.authorizeTx( did.id, call, - getSignCallback(did), + signers, paymentAccount.address ) await submitTx(tx, paymentAccount) @@ -577,7 +589,7 @@ describe('DID authorization', () => { const tx = await Did.authorizeTx( did.id, deleteCall, - getSignCallback(did), + signers, paymentAccount.address ) await submitTx(tx, paymentAccount) @@ -585,9 +597,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 +614,7 @@ describe('DID authorization', () => { describe('DID management batching', () => { describe('FullDidCreationBuilder', () => { it('Build a complete full DID', async () => { - const { storeDidCallback, authentication } = makeSigningKeyTool() + const { storeDidSigner, authentication } = await makeSigningKeyTool() const extrinsic = await Did.getStoreTx( { authentication, @@ -651,7 +663,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( @@ -745,7 +757,7 @@ describe('DID management batching', () => { }) it('Build a minimal full DID with an Ecdsa key', async () => { - const { keypair, storeDidCallback } = makeSigningKeyTool('ecdsa') + const { keypair, storeDidSigner } = await makeSigningKeyTool('ecdsa') const didAuthKey: Did.NewDidVerificationKey = { publicKey: keypair.publicKey, type: 'ecdsa', @@ -754,7 +766,7 @@ describe('DID management batching', () => { const extrinsic = await Did.getStoreTx( { authentication: [didAuthKey] }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(extrinsic, paymentAccount) @@ -787,8 +799,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, storeDidSigner, authentication } = + await makeSigningKeyTool() const createTx = await Did.getStoreTx( { @@ -829,7 +841,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) @@ -870,7 +882,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,16 +916,16 @@ describe('DID management batching', () => { }, 40_000) it('Correctly handles rotation of the authentication key', async () => { - const { authentication, getSignCallback, storeDidCallback } = - makeSigningKeyTool() + const { authentication, getSigners, storeDidSigner } = + await makeSigningKeyTool() const { authentication: [newAuthKey], - } = makeSigningKeyTool('ed25519') + } = await makeSigningKeyTool('ed25519') const createTx = await Did.getStoreTx( { authentication }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) @@ -948,7 +960,7 @@ describe('DID management batching', () => { }) ), ], - sign: getSignCallback(initialFullDid), + signers: await getSigners(initialFullDid), submitter: paymentAccount.address, }) @@ -995,8 +1007,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, storeDidSigner } = + await makeSigningKeyTool() const tx = await Did.getStoreTx( { authentication, @@ -1009,7 +1021,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) // Create the full DIgetStoreTx await submitTx(tx, paymentAccount) @@ -1038,7 +1050,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 +1092,8 @@ describe('DID management batching', () => { }, 60_000) it('batchAll fails if any extrinsics fails', async () => { - const { authentication, getSignCallback, storeDidCallback } = - makeSigningKeyTool() + const { authentication, getSigners, storeDidSigner } = + await makeSigningKeyTool() const createTx = await Did.getStoreTx( { authentication, @@ -1094,7 +1106,7 @@ describe('DID management batching', () => { ], }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) await submitTx(createTx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( @@ -1122,7 +1134,7 @@ describe('DID management batching', () => { }) ), ], - sign: getSignCallback(fullDid), + signers: await getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1157,7 +1169,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 +1192,7 @@ describe('DID extrinsics batching', () => { delegationRevocationTx, delegationStoreTx, ], - sign: key.getSignCallback(fullDid), + signers: await key.getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1210,7 +1222,7 @@ describe('DID extrinsics batching', () => { delegationRevocationTx, delegationStoreTx, ], - sign: key.getSignCallback(fullDid), + signers: await key.getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1229,7 +1241,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 +1252,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 +1300,7 @@ describe('DID extrinsics batching', () => { ctype2Creation, delegationHierarchyRemoval, ], - sign: key.getSignCallback(fullDid), + signers: await key.getSigners(fullDid), submitter: paymentAccount.address, }) @@ -1315,13 +1327,14 @@ describe('DID extrinsics batching', () => { describe('Runtime constraints', () => { let testAuthKey: Did.NewDidVerificationKey - const { keypair, storeDidCallback } = makeSigningKeyTool('ed25519') - + let storeDidSigner: SignerInterface beforeAll(async () => { + const tool = await makeSigningKeyTool('ed25519') testAuthKey = { - publicKey: keypair.publicKey, + publicKey: tool.keypair.publicKey, type: 'ed25519', } + storeDidSigner = tool.storeDidSigner }) describe('DID creation', () => { it('should not be possible to create a DID with too many encryption keys', async () => { @@ -1338,7 +1351,7 @@ describe('Runtime constraints', () => { keyAgreement: newKeyAgreementKeys, }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) // One more than the maximum newKeyAgreementKeys.push({ @@ -1353,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"` @@ -1375,7 +1388,7 @@ describe('Runtime constraints', () => { service: newServiceEndpoints, }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) // One more than the maximum newServiceEndpoints.push({ @@ -1391,7 +1404,7 @@ describe('Runtime constraints', () => { }, paymentAccount.address, - storeDidCallback + [storeDidSigner] ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Cannot store more than 25 services per DID"` 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..e3f4d62dd 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -10,12 +10,13 @@ import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' import type { DecryptCallback, DidDocument, + DidUrl, EncryptCallback, KeyringPair, KiltAddress, KiltEncryptionKeypair, KiltKeyringPair, - SignCallback, + SignerInterface, SubmittableExtrinsic, UriFragment, VerificationMethod, @@ -25,7 +26,6 @@ import type { BaseNewDidKey, ChainDidKey, DidVerificationMethodType, - GetStoreTxSignCallback, LightDidSupportedVerificationKeyType, NewLightDidVerificationKey, NewDidVerificationKey, @@ -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'] /** @@ -163,24 +129,26 @@ 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, + id: keypair.address, + }) + const signer = await Signers.selectSigner( + signers, + Signers.select.verifiableOnChain() + ) + return signer! } export interface KeyTool { keypair: KiltKeyringPair - getSignCallback: KeyToolSignCallback - storeDidCallback: StoreDidCallback + getSigners: ( + doc: DidDocument + ) => Promise>> + storeDidSigner: SignerInterface authentication: [NewLightDidVerificationKey] } @@ -190,17 +158,33 @@ 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 storeDidCallback = makeStoreDidCallback(keypair) + const getSigners: ( + didDocument: DidDocument + ) => Promise>> = async ( + didDocument + ) => { + return ( + await Promise.all( + didDocument.verificationMethod?.map(({ id }) => + Signers.getSignersForKeypair({ + keypair, + id: `${didDocument.id}${id}`, + }) + ) ?? [] + ) + ).flat() + } + + const storeDidSigner = await makeStoreDidSigner(keypair) return { keypair, - getSignCallback, - storeDidCallback, + getSigners, + storeDidSigner, authentication: [keypair as NewLightDidVerificationKey], } } @@ -412,14 +396,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, @@ -480,14 +465,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 @@ -502,14 +487,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 } @@ -518,6 +503,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]) } diff --git a/yarn.lock b/yarn.lock index 1ac7b6ba2..bccf8eb09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2037,6 +2037,38 @@ __metadata: languageName: unknown linkType: soft +"@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.1.0-rc.1 + "@noble/curves": ^1.0.0 + "@scure/base": ^1.1.1 + checksum: c3127e4f98e37216752e52314aa3e173d32327819fc88ba69fdba7bfbb014ca61cf271eb01864274c1c25813edbf07975c8dc829708dc525f7425ea2a38ab7d9 + languageName: node + linkType: hard + +"@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.1.0-rc.1 + "@noble/curves": ^1.0.0 + "@scure/base": ^1.1.1 + checksum: 843437a6806c10728fedc6cfa45f408aa9278263d64fce7c5057515d06fed74bc9ea702d932cc24543daff66a1b9935389b0a1453ea45d59111b46b59b19ad56 + languageName: node + linkType: hard + +"@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 + multibase: ^4.0.6 + checksum: fefc3c86bdb0a732f5851155cedf1743eedace4cf03fa5fbeb37f321c3cf87e9ac1704328e0d69d3cb80203e9afc07b6849907816c402bcd90b7c18b99177d40 + 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 +2103,17 @@ __metadata: languageName: unknown linkType: soft +"@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.1.0-rc.1 + "@polkadot/util-crypto": ^12.0.1 + "@scure/base": ^1.1.1 + checksum: 8253c03b8c9daef61bc2155ee64894a568f4b591c941dc0bb668839475ed8d8fcf1048339710805e3046cb3ea603ed58c5e857887c4c67b602af098c2a92fb52 + 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 +2141,10 @@ __metadata: version: 0.0.0-use.local resolution: "@kiltprotocol/utils@workspace:packages/utils" dependencies: + "@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 @@ -2140,7 +2187,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.0.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: @@ -2472,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.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: @@ -2661,7 +2708,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 +4085,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"