diff --git a/jest.config.js b/jest.config.js index 42bfbd960..8a5901281 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,8 @@ module.exports = { testEnvironment: 'node', clearMocks: true, runner: 'groups', - testTimeout: 10000, + // Parachain block time is 12s + testTimeout: 15000, setupFilesAfterEnv: ['../testingTools/setup.js'], transformIgnorePatterns:['/node_modules/(?!@polkadot|@babel/runtime/helpers/esm/)'], coverageThreshold: { diff --git a/package.json b/package.json index b66738c98..3565390ce 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,6 @@ "typedoc-plugin-external-module-name": "^4.0.6", "typescript": "^4.2.2" }, - "version": "0.22.2-2", + "version": "0.24.0-0", "packageManager": "yarn@2.4.2" } diff --git a/packages/chain-helpers/package.json b/packages/chain-helpers/package.json index 4f55f4c54..4c701b0ec 100644 --- a/packages/chain-helpers/package.json +++ b/packages/chain-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/chain-helpers", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/packages/config/package.json b/packages/config/package.json index 2105291df..958a1a2ed 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/config", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/packages/core/package.json b/packages/core/package.json index 75d1c0dc7..0ad5b651b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/core", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/packages/core/src/__integrationtests__/Attestation.spec.ts b/packages/core/src/__integrationtests__/Attestation.spec.ts index f16fe5621..cf7fa665d 100644 --- a/packages/core/src/__integrationtests__/Attestation.spec.ts +++ b/packages/core/src/__integrationtests__/Attestation.spec.ts @@ -14,7 +14,7 @@ import { BlockchainUtils, ExtrinsicErrors } from '@kiltprotocol/chain-helpers' import { createOnChainDidFromSeed, DemoKeystore, - DidDetails, + FullDidDetails, } from '@kiltprotocol/did' import { Crypto } from '@kiltprotocol/utils' import { randomAsHex } from '@polkadot/util-crypto' @@ -39,9 +39,9 @@ import '../../../../testingTools/jestErrorCodeMatcher' let tokenHolder: KeyringPair let signer: DemoKeystore -let attester: DidDetails -let anotherAttester: DidDetails -let claimer: DidDetails +let attester: FullDidDetails +let anotherAttester: FullDidDetails +let claimer: FullDidDetails beforeAll(async () => { await init({ address: WS_ADDRESS }) diff --git a/packages/core/src/__integrationtests__/Ctypes.spec.ts b/packages/core/src/__integrationtests__/Ctypes.spec.ts index 06f92d497..4b63e0fe9 100644 --- a/packages/core/src/__integrationtests__/Ctypes.spec.ts +++ b/packages/core/src/__integrationtests__/Ctypes.spec.ts @@ -13,7 +13,7 @@ import type { ICType } from '@kiltprotocol/types' import { BlockchainUtils, ExtrinsicErrors } from '@kiltprotocol/chain-helpers' import { KeyringPair } from '@polkadot/keyring/types' import { - DidDetails, + FullDidDetails, DemoKeystore, createOnChainDidFromSeed, } from '@kiltprotocol/did' @@ -31,7 +31,7 @@ beforeAll(async () => { }) describe('When there is an CtypeCreator and a verifier', () => { - let ctypeCreator: DidDetails + let ctypeCreator: FullDidDetails let paymentAccount: KeyringPair let ctypeCounter = 0 const keystore = new DemoKeystore() diff --git a/packages/core/src/__integrationtests__/Delegation.spec.ts b/packages/core/src/__integrationtests__/Delegation.spec.ts index 3f7a45b8e..184e2b155 100644 --- a/packages/core/src/__integrationtests__/Delegation.spec.ts +++ b/packages/core/src/__integrationtests__/Delegation.spec.ts @@ -16,7 +16,7 @@ import type { KeyringPair } from '@polkadot/keyring/types' import { createOnChainDidFromSeed, DemoKeystore, - DidDetails, + FullDidDetails, } from '@kiltprotocol/did' import { randomAsHex } from '@polkadot/util-crypto' import Attestation from '../attestation/Attestation' @@ -30,12 +30,12 @@ import { getAttestationHashes } from '../delegation/DelegationNode.chain' let paymentAccount: KeyringPair let signer: DemoKeystore -let root: DidDetails -let claimer: DidDetails -let attester: DidDetails +let root: FullDidDetails +let claimer: FullDidDetails +let attester: FullDidDetails async function writeHierarchy( - delegator: DidDetails, + delegator: FullDidDetails, ctypeHash: ICType['hash'] ): Promise { const rootNode = DelegationNode.newRoot({ @@ -60,8 +60,8 @@ async function writeHierarchy( async function addDelegation( hierarchyId: IDelegationNode['id'], parentId: DelegationNode['id'], - delegator: DidDetails, - delegee: DidDetails, + delegator: FullDidDetails, + delegee: FullDidDetails, permissions: Permission[] = [Permission.ATTEST, Permission.DELEGATE] ): Promise { const delegationNode = DelegationNode.newNode({ diff --git a/packages/core/src/__integrationtests__/Did.spec.ts b/packages/core/src/__integrationtests__/Did.spec.ts index 46285ed3f..817e850f0 100644 --- a/packages/core/src/__integrationtests__/Did.spec.ts +++ b/packages/core/src/__integrationtests__/Did.spec.ts @@ -72,7 +72,7 @@ describe('write and didDeleteTx', () => { await expect(DidChain.queryById(didIdentifier)).resolves.toMatchObject< Partial >({ - did: DidUtils.getKiltDidFromIdentifier(didIdentifier), + did: DidUtils.getKiltDidFromIdentifier(didIdentifier, 'full'), }) }, 30_000) @@ -80,7 +80,7 @@ describe('write and didDeleteTx', () => { await expect(DidChain.queryById(didIdentifier)).resolves.toMatchObject< Partial >({ - did: DidUtils.getKiltDidFromIdentifier(didIdentifier), + did: DidUtils.getKiltDidFromIdentifier(didIdentifier, 'full'), }) const call = await DidChain.getDeleteDidExtrinsic() @@ -132,7 +132,7 @@ it('creates and updates DID', async () => { await expect(DidChain.queryById(didIdentifier)).resolves.toMatchObject< Partial >({ - did: DidUtils.getKiltDidFromIdentifier(didIdentifier), + did: DidUtils.getKiltDidFromIdentifier(didIdentifier, 'full'), endpointData: { urls: ['https://example.com'], contentType: 'application/json', @@ -164,7 +164,7 @@ it('creates and updates DID', async () => { await expect(DidChain.queryById(didIdentifier)).resolves.toMatchObject< Partial >({ - did: DidUtils.getKiltDidFromIdentifier(didIdentifier), + did: DidUtils.getKiltDidFromIdentifier(didIdentifier, 'full'), endpointData: { urls: ['ftp://example.com/abc'], contentType: 'application/ld+json', @@ -181,7 +181,7 @@ describe('DID authorization', () => { const { publicKey, alg } = await keystore.generateKeypair({ alg: SigningAlgorithms.Ed25519, }) - didIdentifier = encodeAddress(publicKey) + didIdentifier = encodeAddress(publicKey, 38) key = { publicKey, type: alg } const tx = await DidChain.generateCreateTx({ didIdentifier, @@ -202,13 +202,13 @@ describe('DID authorization', () => { await expect(DidChain.queryById(didIdentifier)).resolves.toMatchObject< Partial >({ - did: DidUtils.getKiltDidFromIdentifier(didIdentifier), + did: DidUtils.getKiltDidFromIdentifier(didIdentifier, 'full'), }) }, 30_000) beforeEach(async () => { lastTxIndex = await DidChain.queryLastTxIndex( - DidUtils.getKiltDidFromIdentifier(didIdentifier) + DidUtils.getKiltDidFromIdentifier(didIdentifier, 'full') ) }) diff --git a/packages/core/src/__integrationtests__/ErrorHandler.spec.ts b/packages/core/src/__integrationtests__/ErrorHandler.spec.ts index 8bcc59ce9..13c5f0037 100644 --- a/packages/core/src/__integrationtests__/ErrorHandler.spec.ts +++ b/packages/core/src/__integrationtests__/ErrorHandler.spec.ts @@ -15,7 +15,7 @@ import { KeyringPair } from '@polkadot/keyring/types' import { createOnChainDidFromSeed, DemoKeystore, - DidDetails, + FullDidDetails, } from '@kiltprotocol/did' import { randomAsHex } from '@polkadot/util-crypto' import { Attestation } from '..' @@ -26,7 +26,7 @@ import { addressFromRandom, devAlice, WS_ADDRESS } from './utils' import '../../../../testingTools/jestErrorCodeMatcher' let paymentAccount: KeyringPair -let someDid: DidDetails +let someDid: FullDidDetails const keystore = new DemoKeystore() beforeAll(async () => { diff --git a/packages/core/src/attestation/Attestation.chain.ts b/packages/core/src/attestation/Attestation.chain.ts index 6b8ad02dd..5e09a2b62 100644 --- a/packages/core/src/attestation/Attestation.chain.ts +++ b/packages/core/src/attestation/Attestation.chain.ts @@ -62,7 +62,8 @@ function decode( claimHash, cTypeHash: chainAttestation.ctypeHash.toString(), owner: DidUtils.getKiltDidFromIdentifier( - chainAttestation.attester.toString() + chainAttestation.attester.toString(), + 'full' ), delegationId: chainAttestation.delegationId.unwrapOr(null)?.toString() || null, diff --git a/packages/core/src/attestedclaim/AttestedClaim.spec.ts b/packages/core/src/attestedclaim/AttestedClaim.spec.ts index a3eb63480..1b1e218ac 100644 --- a/packages/core/src/attestedclaim/AttestedClaim.spec.ts +++ b/packages/core/src/attestedclaim/AttestedClaim.spec.ts @@ -18,7 +18,11 @@ import type { IDidDetails, IDidResolver, } from '@kiltprotocol/types' -import { DemoKeystore, createLocalDemoDidFromSeed } from '@kiltprotocol/did' +import { + DemoKeystore, + createLocalDemoDidFromSeed, + createLightDidFromSeed, +} from '@kiltprotocol/did' import { UUID } from '@kiltprotocol/utils' import Attestation from '../attestation/Attestation' import Claim from '../claim/Claim' @@ -120,7 +124,7 @@ describe('RequestForAttestation', () => { ] }) - it('verify attested claims', async () => { + it('verify attested claims signed by a full DID', async () => { const attestedClaim = await buildAttestedClaim( identityCharlie, identityAlice, @@ -141,6 +145,28 @@ describe('RequestForAttestation', () => { AttestedClaim.verify(attestedClaim, { claimerDid: identityCharlie }) ).resolves.toBe(true) }) + it('verify attested claims signed by a light DID', async () => { + const identityDave = await createLightDidFromSeed(keystore, '//Dave') + const attestedClaim = await buildAttestedClaim( + identityDave, + identityAlice, + { + a: 'a', + b: 'b', + c: 'c', + }, + [legitimation], + keystore + ) + + ;(query as jest.Mock).mockResolvedValue(attestedClaim.attestation) + + // check proof on complete data + expect(AttestedClaim.verifyData(attestedClaim)).toBeTruthy() + await expect( + AttestedClaim.verify(attestedClaim, { claimerDid: identityDave }) + ).resolves.toBe(true) + }) it('compresses and decompresses the attested claims object', () => { expect(AttestedClaimUtils.compress(legitimation)).toEqual( @@ -269,7 +295,7 @@ describe('create presentation', () => { expect(cred).toBeDefined() }) - it('should create presentation and exclude specific attributes', async () => { + it('should create presentation and exclude specific attributes using a full DID', async () => { const mockResolver: IDidResolver = { resolveDoc: async (did: string) => { if (did.startsWith(claimer.did)) return claimer @@ -293,6 +319,47 @@ describe('create presentation', () => { ).resolves.toBe(true) expect(att.request.claimerSignature?.challenge).toEqual(challenge) }) + it('should create presentation and exclude specific attributes using a light DID', async () => { + claimer = await createLightDidFromSeed(keystore, '//Attester') + const rawCType: ICType['schema'] = { + $id: 'kilt:ctype:0x1', + $schema: 'http://kilt-protocol.org/draft-01/ctype#', + title: 'credential', + properties: { + name: { type: 'string' }, + }, + type: 'object', + } + ctype = CType.fromSchema(rawCType, attester.did) + + // cannot be used since the variable needs to be established in the outer scope + reqForAtt = RequestForAttestation.fromClaim( + Claim.fromCTypeAndClaimContents( + ctype, + { + name: 'Peter', + age: 12, + }, + claimer.did + ) + ) + + attestation = Attestation.fromRequestAndDid(reqForAtt, attester.did) + ;(query as jest.Mock).mockResolvedValue(attestation) + + const cred = AttestedClaim.fromRequestAndAttestation(reqForAtt, attestation) + + const challenge = UUID.generate() + const att = await cred.createPresentation({ + selectedAttributes: ['name'], + signer: keystore, + claimerDid: claimer, + challenge, + }) + expect(att.getAttributes()).toEqual(new Set(['name'])) + await expect(AttestedClaim.verify(att)).resolves.toBe(true) + expect(att.request.claimerSignature?.challenge).toEqual(challenge) + }) it('should get attribute keys', async () => { const cred = AttestedClaim.fromRequestAndAttestation(reqForAtt, attestation) diff --git a/packages/core/src/ctype/CType.chain.ts b/packages/core/src/ctype/CType.chain.ts index 5ce35e5c7..7875b2a4e 100644 --- a/packages/core/src/ctype/CType.chain.ts +++ b/packages/core/src/ctype/CType.chain.ts @@ -42,7 +42,7 @@ export async function store(ctype: ICType): Promise { export function decode(encoded: Option): IDidDetails['did'] | null { DecoderUtils.assertCodecIsType(encoded, ['Option']) return encoded.isSome - ? DidUtils.getKiltDidFromIdentifier(encoded.unwrap().toString()) + ? DidUtils.getKiltDidFromIdentifier(encoded.unwrap().toString(), 'full') : null } diff --git a/packages/core/src/ctype/CType.spec.ts b/packages/core/src/ctype/CType.spec.ts index 415f3f992..b1187cc04 100644 --- a/packages/core/src/ctype/CType.spec.ts +++ b/packages/core/src/ctype/CType.spec.ts @@ -142,7 +142,7 @@ describe('CType', () => { `) expect(() => CType.fromCType(faultyAddressTypeCType) - ).toThrowErrorMatchingInlineSnapshot(`"Not a KILT did: 4262626426"`) + ).toThrowErrorMatchingInlineSnapshot(`"Not a valid KILT did: 4262626426"`) expect(() => CType.fromCType(wrongSchemaIdCType) ).toThrowErrorMatchingInlineSnapshot( diff --git a/packages/core/src/delegation/DelegationDecoder.ts b/packages/core/src/delegation/DelegationDecoder.ts index 77c561fca..8a5b4d03e 100644 --- a/packages/core/src/delegation/DelegationDecoder.ts +++ b/packages/core/src/delegation/DelegationDecoder.ts @@ -116,7 +116,8 @@ export function decodeDelegationNode( : undefined, childrenIds: [...delegationNode.children.keys()].map((id) => id.toHex()), account: DidUtils.getKiltDidFromIdentifier( - delegationNode.details.owner.toString() + delegationNode.details.owner.toString(), + 'full' ), permissions: decodePermissions( delegationNode.details.permissions.toNumber() diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index e9e1590ad..3d152652e 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -33,7 +33,7 @@ import { } from '@kiltprotocol/types' import { Crypto, SDKErrors, UUID } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' -import { DidTypes, DidDetailsUtils } from '@kiltprotocol/did' +import { DidTypes, DidUtils } from '@kiltprotocol/did' import type { DelegationHierarchyDetailsRecord } from './DelegationDecoder' import { query as queryAttestation } from '../attestation/Attestation.chain' import { @@ -275,7 +275,7 @@ export default class DelegationNode implements IDelegationNode { delegeeDid: IDidDetails, signer: KeystoreSigner ): Promise { - const { alg, signature } = await DidDetailsUtils.signWithDid( + const { alg, signature } = await DidUtils.signWithDid( Crypto.coToUInt8(this.generateHash()), delegeeDid, signer, diff --git a/packages/core/src/requestforattestation/RequestForAttestation.ts b/packages/core/src/requestforattestation/RequestForAttestation.ts index 435da397b..9d33d951b 100644 --- a/packages/core/src/requestforattestation/RequestForAttestation.ts +++ b/packages/core/src/requestforattestation/RequestForAttestation.ts @@ -32,7 +32,7 @@ import type { } from '@kiltprotocol/types' import { KeyRelationship } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { DefaultResolver, DidUtils, DidDetailsUtils } from '@kiltprotocol/did' +import { DefaultResolver, DidUtils } from '@kiltprotocol/did' import ClaimUtils from '../claim/Claim.utils' import AttestedClaim from '../attestedclaim/AttestedClaim' import RequestForAttestationUtils from './RequestForAttestation.utils' @@ -320,7 +320,7 @@ export default class RequestForAttestation implements IRequestForAttestation { did: IDidDetails, challenge?: string ): Promise { - const { signature, keyId } = await DidDetailsUtils.signWithDid( + const { signature, keyId } = await DidUtils.signWithDid( makeSigningData(this, challenge), did, signer, @@ -334,7 +334,7 @@ export default class RequestForAttestation implements IRequestForAttestation { key: IDidKeyDetails, challenge?: string ): Promise { - const { signature } = await DidDetailsUtils.signWithKey( + const { signature } = await DidUtils.signWithKey( makeSigningData(this, challenge), key, signer diff --git a/packages/kilt-did/package.json b/packages/kilt-did/package.json index 282f2e1ed..c4d36fae2 100644 --- a/packages/kilt-did/package.json +++ b/packages/kilt-did/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/did", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", @@ -28,9 +28,11 @@ "@kiltprotocol/chain-helpers": "workspace:*", "@kiltprotocol/types": "workspace:*", "@kiltprotocol/utils": "workspace:*", + "@polkadot/api": "^5.4.1", "@polkadot/keyring": "^7.1.2", "@polkadot/types": "^5.4.1", "@polkadot/util": "^7.1.2", - "@polkadot/util-crypto": "^7.1.2" + "@polkadot/util-crypto": "^7.1.2", + "cbor": "^6.0.0" } } diff --git a/packages/kilt-did/src/DemoKeystore/DemoKeystore.ts b/packages/kilt-did/src/DemoKeystore/DemoKeystore.ts index fcb065262..0e3e4e144 100644 --- a/packages/kilt-did/src/DemoKeystore/DemoKeystore.ts +++ b/packages/kilt-did/src/DemoKeystore/DemoKeystore.ts @@ -31,9 +31,10 @@ import { BlockchainUtils } from '@kiltprotocol/chain-helpers' import { KeypairType } from '@polkadot/util-crypto/types' import { u8aEq } from '@polkadot/util' import { getKiltDidFromIdentifier } from '../Did.utils' -import { DidDetails, DidDetailsUtils } from '../DidDetails' +import { FullDidDetails, LightDidDetails } from '../DidDetails' import { DefaultResolver, DidUtils } from '..' import { PublicKeyRoleAssignment } from '../types' +import { newFullDidDetailsfromKeys } from '../DidDetails/FullDidDetails.utils' export enum SigningAlgorithms { Ed25519 = 'ed25519', @@ -285,9 +286,10 @@ export async function createLocalDemoDidFromSeed( keystore: DemoKeystore, mnemonicOrHexSeed: string, signingKeyType = SigningAlgorithms.Ed25519 -): Promise { +): Promise { const did = getKiltDidFromIdentifier( - encodeAddress(blake2AsU8a(mnemonicOrHexSeed, 32 * 8), 38) + encodeAddress(blake2AsU8a(mnemonicOrHexSeed, 32 * 8), 38), + 'full' ) const generateKeypairForDid = async ( @@ -311,7 +313,7 @@ export async function createLocalDemoDidFromSeed( } } - return DidDetailsUtils.newDidDetailsfromKeys({ + return newFullDidDetailsfromKeys({ [KeyRelationship.authentication]: await generateKeypairForDid( '', signingKeyType, @@ -335,12 +337,30 @@ export async function createLocalDemoDidFromSeed( }) } +export async function createLightDidFromSeed( + keystore: DemoKeystore, + mnemonicOrHexSeed: string, + signingKeyType = SigningAlgorithms.Sr25519 +): Promise { + const authenticationPublicKey = await keystore.generateKeypair({ + alg: signingKeyType, + seed: mnemonicOrHexSeed, + }) + + return new LightDidDetails({ + authenticationKey: { + publicKey: authenticationPublicKey.publicKey, + type: authenticationPublicKey.alg, + }, + }) +} + export async function createOnChainDidFromSeed( paymentAccount: KeyringPair, keystore: DemoKeystore, mnemonicOrHexSeed: string, signingKeyType = SigningAlgorithms.Ed25519 -): Promise { +): Promise { const makeKey = (seed: string, alg: string) => keystore .generateKeypair({ @@ -378,7 +398,7 @@ export async function createOnChainDidFromSeed( }) const queried = await DefaultResolver.resolveDoc(did) if (queried) { - return queried as DidDetails + return queried as FullDidDetails } throw Error(`failed to write Did${did}`) } diff --git a/packages/kilt-did/src/Did.chain.ts b/packages/kilt-did/src/Did.chain.ts index ce9a5ee20..6cd24baca 100644 --- a/packages/kilt-did/src/Did.chain.ts +++ b/packages/kilt-did/src/Did.chain.ts @@ -77,7 +77,10 @@ function decodeEndpointUrl(url: Url): string { return (url.value as UrlEncoding).payload.toString() } -function decodeDidChainRecord(didDetail: IDidChainRecordCodec, did: string) { +function decodeDidChainRecord( + didDetail: IDidChainRecordCodec, + did: string +): IDidChainRecordJSON { const publicKeys: IDidKeyDetails[] = Array.from( didDetail.publicKeys.entries() ).map(([keyId, keyDetails]) => { @@ -125,7 +128,7 @@ export async function queryById( if (result.isSome) { return decodeDidChainRecord( result.unwrap(), - getKiltDidFromIdentifier(didIdentifier) + getKiltDidFromIdentifier(didIdentifier, 'full') ) } return null @@ -156,7 +159,8 @@ export async function queryKey( } export async function queryLastTxIndex(did: string): Promise { - const encoded = await queryEncoded(getIdentifierFromKiltDid(did)) + const identifier = getIdentifierFromKiltDid(did) + const encoded = await queryEncoded(identifier) if (encoded.isNone) return new BN(0) return encoded.unwrap().lastTxCounter.toBn() } diff --git a/packages/kilt-did/src/Did.utils.ts b/packages/kilt-did/src/Did.utils.ts index b9c82e31f..714a1e633 100644 --- a/packages/kilt-did/src/Did.utils.ts +++ b/packages/kilt-did/src/Did.utils.ts @@ -33,12 +33,23 @@ import type { INewPublicKey, PublicKeyRoleAssignment, EndpointData, + IDidParsingResult, } from './types' import { generateCreateTx } from './Did.chain' -import { signWithDid } from './DidDetails/utils' export const KILT_DID_PREFIX = 'did:kilt:' -export const KILT_DID_REGEX = /^did:kilt:(?[1-9a-km-zA-HJ-NP-Z]{48})(?#.+)?$/ + +// Matches the following full DIDs +// - did:kilt: +// - did:kilt:# +export const FULL_KILT_DID_REGEX = /^did:kilt:(?[1-9a-km-zA-HJ-NP-Z]{48})(?#[^#\n]+)?$/ + +// Matches the following light DIDs +// - did:kilt:light:00 +// - did:kilt:light:01: +// - did:kilt:light:10# +// - did:kilt:light:99:# +export const LIGHT_KILT_DID_REGEX = /^did:kilt:light:(?[0-9]{2})(?[1-9a-km-zA-HJ-NP-Z]{48,49})(?:.+?)?(?#[^#\n]+)?$/ export enum CHAIN_SUPPORTED_SIGNATURE_KEY_TYPES { ed25519 = 'ed25519', @@ -66,37 +77,100 @@ export function getSignatureAlgForKeyType(keyType: string): string { return SignatureAlgForKeyType[keyType] || keyType } -export function getKiltDidFromIdentifier(identifier: string): string { +export enum LIGHT_DID_SUPPORTED_SIGNING_KEY_TYPES { + ed25519 = 'ed25519', + sr25519 = 'sr25519', + ecdsa = 'ecdsa', +} + +const EncodingForSigningKeyType = { + [LIGHT_DID_SUPPORTED_SIGNING_KEY_TYPES.sr25519]: '00', + [LIGHT_DID_SUPPORTED_SIGNING_KEY_TYPES.ed25519]: '01', + [LIGHT_DID_SUPPORTED_SIGNING_KEY_TYPES.ecdsa]: '02', +} + +const SigningKeyTypeFromEncoding = { + '00': LIGHT_DID_SUPPORTED_SIGNING_KEY_TYPES.sr25519, + '01': LIGHT_DID_SUPPORTED_SIGNING_KEY_TYPES.ed25519, + '02': LIGHT_DID_SUPPORTED_SIGNING_KEY_TYPES.ecdsa, +} + +export function getEncodingForSigningKeyType(keyType: string): string { + return EncodingForSigningKeyType[keyType] || null +} + +export function getSigningKeyTypeFromEncoding(encoding: string): string { + return SigningKeyTypeFromEncoding[encoding]?.toString() || null +} + +function getLightDidFromIdentifier(identifier: string, didVersion = 1): string { + const versionString = didVersion === 1 ? '' : `:v${didVersion}` + return KILT_DID_PREFIX.concat(`light${versionString}:${identifier}`) +} + +function getFullDidFromIdentifier(identifier: string, didVersion = 1): string { + const versionString = didVersion === 1 ? '' : `v${didVersion}:` + return KILT_DID_PREFIX.concat(`${versionString}${identifier}`) +} + +export function getKiltDidFromIdentifier( + identifier: string, + didType: 'full' | 'light', + didVersion = 1 +): string { if (identifier.startsWith(KILT_DID_PREFIX)) { - return identifier + if ( + FULL_KILT_DID_REGEX.exec(identifier) || + LIGHT_KILT_DID_REGEX.exec(identifier) + ) { + return identifier + } + throw SDKErrors.ERROR_INVALID_DID_FORMAT } - return KILT_DID_PREFIX + identifier -} -export function getIdentifierFromKiltDid(did: string): string { - if (!did.startsWith(KILT_DID_PREFIX)) { - throw SDKErrors.ERROR_INVALID_DID_PREFIX(did) + switch (didType) { + case 'full': + return getFullDidFromIdentifier(identifier, didVersion) + case 'light': + return getLightDidFromIdentifier(identifier, didVersion) + default: + throw SDKErrors.ERROR_UNSUPPORTED_DID(didType) } - return did.substr(KILT_DID_PREFIX.length) } -export function getIdentifierFromDid(did: string): string { - const secondColonAt = did.indexOf(':', did.indexOf(':') + 1) - const identifier = did.substring(secondColonAt + 1) - if (!identifier) { - throw SDKErrors.ERROR_INVALID_DID_PREFIX(did) +export function parseDidUrl(didUrl: string): IDidParsingResult { + let matches = FULL_KILT_DID_REGEX.exec(didUrl)?.groups + if (matches && matches.identifier) { + const version = matches.version ? parseInt(matches.version, 10) : 1 + return { + did: getKiltDidFromIdentifier(matches.identifier, 'full', version), + version, + type: 'full', + identifier: matches.identifier, + fragment: matches.fragment?.substring(1), + } } - return identifier -} -export function parseDidUrl(didUrl: string) { - const { identifier, fragment } = didUrl.match(KILT_DID_REGEX)?.groups || {} - if (!identifier) throw SDKErrors.ERROR_INVALID_DID_PREFIX(didUrl) - return { - did: getKiltDidFromIdentifier(identifier), - identifier, - fragment: fragment?.substr(1), + // If it fails to parse full DID, try with light DID + matches = LIGHT_KILT_DID_REGEX.exec(didUrl)?.groups + if (matches && matches.identifier && matches.auth_key_type) { + const version = matches.version ? parseInt(matches.version, 10) : 1 + const lightDidIdentifier = matches.auth_key_type.concat(matches.identifier) + return { + did: getKiltDidFromIdentifier(lightDidIdentifier, 'light', version), + version, + type: 'light', + identifier: matches.auth_key_type.concat(matches.identifier), + fragment: matches.fragment?.substring(1), + encodedDetails: matches.encoded_details?.substring(1), + } } + + throw SDKErrors.ERROR_INVALID_DID_FORMAT(didUrl) +} + +export function getIdentifierFromKiltDid(did: string): string { + return parseDidUrl(did).identifier } export function validateKiltDid( @@ -106,14 +180,25 @@ export function validateKiltDid( if (typeof input !== 'string') { throw TypeError(`DID string expected, got ${typeof input}`) } - const { identifier, did } = parseDidUrl(input) - if (!allowFragment && did !== input) { - throw new Error( - `Expected DID of format kilt:did:, got ${input}` - ) + const { identifier, type, fragment } = parseDidUrl(input) + if (!allowFragment && fragment) { + throw SDKErrors.ERROR_INVALID_DID_FORMAT(input) } - if (!checkAddress(identifier, 38)[0]) { - throw SDKErrors.ERROR_ADDRESS_INVALID(identifier, 'DID identifier') + + switch (type) { + case 'full': + if (!checkAddress(identifier, 38)[0]) { + throw SDKErrors.ERROR_ADDRESS_INVALID(identifier, 'DID identifier') + } + break + case 'light': + // Identifier includes the first two characters for the key type encoding + if (!checkAddress(identifier.substring(2), 38)[0]) { + throw SDKErrors.ERROR_ADDRESS_INVALID(identifier, 'DID identifier') + } + break + default: + throw SDKErrors.ERROR_UNSUPPORTED_DID(input) } return true } @@ -156,7 +241,7 @@ export function encodeEndpointUrl(url: string): UrlEnum { }) if (!matched) throw new Error( - 'only endpoint urls starting with http/https, ftp, and ipfs are accepted' + 'Only endpoint urls starting with http/https, ftp, and ipfs are accepted' ) return typedUrl as UrlEnum } @@ -317,20 +402,6 @@ export async function verifyDidSignatureAsync({ } } -export async function getDidAuthenticationSignature( - toSign: Uint8Array | string, - did: IDidDetails, - signer: KeystoreSigner -): Promise { - const { keyId, signature } = await signWithDid( - toSign, - did, - signer, - KeyRelationship.authentication - ) - return { keyId, signature: Crypto.u8aToHex(signature) } -} - export async function writeDidfromPublicKeys( signer: KeystoreSigner, publicKeys: PublicKeyRoleAssignment, @@ -348,7 +419,7 @@ export async function writeDidfromPublicKeys( signingPublicKey: authenticationKey.publicKey, endpointData, }) - return { submittable, did: getKiltDidFromIdentifier(didIdentifier) } + return { submittable, did: getKiltDidFromIdentifier(didIdentifier, 'full') } } export function writeDidfromIdentity( @@ -367,3 +438,52 @@ export function writeDidfromIdentity( [KeyRelationship.keyAgreement]: { ...identity.boxKeyPair, type: 'x25519' }, }) } + +export async function signWithKey( + toSign: Uint8Array | string, + key: IDidKeyDetails, + signer: KeystoreSigner +): Promise<{ keyId: string; alg: string; signature: Uint8Array }> { + const alg = getSignatureAlgForKeyType(key.type) + const { data: signature } = await signer.sign({ + publicKey: Crypto.coToUInt8(key.publicKeyHex), + alg, + data: Crypto.coToUInt8(toSign), + }) + return { keyId: key.id, signature, alg } +} + +export async function signWithDid( + toSign: Uint8Array | string, + did: IDidDetails, + signer: KeystoreSigner, + whichKey: KeyRelationship | IDidKeyDetails['id'] +): Promise<{ keyId: string; alg: string; signature: Uint8Array }> { + let key: IDidKeyDetails | undefined + if (Object.values(KeyRelationship).includes(whichKey as KeyRelationship)) { + // eslint-disable-next-line prefer-destructuring + key = did.getKeys(KeyRelationship.authentication)[0] + } else { + key = did.getKey(whichKey) + } + if (!key) { + throw Error( + `failed to find key on FullDidDetails (${did.did}): ${whichKey}` + ) + } + return signWithKey(toSign, key, signer) +} + +export async function getDidAuthenticationSignature( + toSign: Uint8Array | string, + did: IDidDetails, + signer: KeystoreSigner +): Promise { + const { keyId, signature } = await signWithDid( + toSign, + did, + signer, + KeyRelationship.authentication + ) + return { keyId, signature: Crypto.u8aToHex(signature) } +} diff --git a/packages/kilt-did/src/DidDetails/DidDetails.ts b/packages/kilt-did/src/DidDetails/DidDetails.ts index 69cd243ca..4d50d52bd 100644 --- a/packages/kilt-did/src/DidDetails/DidDetails.ts +++ b/packages/kilt-did/src/DidDetails/DidDetails.ts @@ -5,117 +5,53 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { Extrinsic } from '@polkadot/types/interfaces' -import { BlockchainApiConnection } from '@kiltprotocol/chain-helpers' +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable import/prefer-default-export */ + import type { - IDidKeyDetails, - KeystoreSigner, - SubmittableExtrinsic, - ApiOrMetadata, - CallMeta, IDidDetails, - ServiceDetails, + IServiceDetails, + IDidKeyDetails, } from '@kiltprotocol/types' import { KeyRelationship } from '@kiltprotocol/types' +import type { MapKeyToRelationship } from '../types' -import type { BN } from '@polkadot/util' -import { generateDidAuthenticatedTx, queryLastTxIndex } from '../Did.chain' -import { getKeysForCall, getKeysForExtrinsic } from './utils' -import { getIdentifierFromDid, getSignatureAlgForKeyType } from '../Did.utils' - -export type MapKeyToRelationship = Partial< - Record> -> - -export interface DidDetailsCreationOpts { - did: string - keys: IDidKeyDetails[] - keyRelationships: MapKeyToRelationship - lastTxIndex: BN - services?: ServiceDetails[] -} - -function errorCheck({ - did, - keys, - keyRelationships, -}: Required): void { - if (!did) { - throw Error('did is required for DidDetails') - } - const keyIds = new Set(keys.map((key) => key.id)) - if (!keyRelationships[KeyRelationship.authentication]?.length) { - throw Error( - `At least one ${KeyRelationship.authentication} key is required on DidDetails` - ) - } - const allowedKeyRelationships: string[] = [ - ...Object.values(KeyRelationship), - 'none', - ] - Object.keys(keyRelationships).forEach((kr) => { - if (!allowedKeyRelationships.includes(kr)) { - throw Error( - `key relationship ${kr} is not recognized. Allowed: ${KeyRelationship}` - ) - } - }) - const keyReferences = new Set( - Array.prototype.concat(...Object.values(keyRelationships)) - ) - keyReferences.forEach((id) => { - if (!keyIds.has(id)) throw new Error(`No key with id ${id} in "keys"`) - }) -} - -export class DidDetails implements IDidDetails { - public readonly did: string - public readonly identifier: string - protected services: ServiceDetails[] - protected keys: Map +/** + * An abstract instance for some details associated with a KILT DID. + */ +export abstract class DidDetails implements IDidDetails { + // The complete DID URI, such as did:kilt: for full DIDs and did:kilt:light:v1: + protected didUri: string + // The identifier of the DID, meaning either the KILT address for full DIDs or the KILT address + the encoded authentication key type for light DIDs. + protected id: string + // The set of service endpoints associated with the DID. + protected services: IServiceDetails[] = [] + // A map from key ID to key details, which allows for efficient retrieval of a key information given its ID. + protected keys: Map = new Map() + // A map from key relationship type (authentication, assertion method, etc.) to key ID, which can then be used to retrieve the key details if needed. protected keyRelationships: MapKeyToRelationship & { none?: Array - } + } = {} - private lastTxIndex: BN + constructor(didUri: string, id: string, services: IServiceDetails[]) { + this.didUri = didUri + this.id = id + this.services = services + } - constructor({ - did, - keys, - keyRelationships = {}, - lastTxIndex, - services = [], - }: DidDetailsCreationOpts) { - errorCheck({ - did, - keys, - keyRelationships, - services, - lastTxIndex, - }) + public get did(): string { + return this.didUri + } - this.did = did - this.keys = new Map(keys.map((key) => [key.id, key])) - this.lastTxIndex = lastTxIndex - this.services = services - this.identifier = getIdentifierFromDid(this.did) - this.keyRelationships = keyRelationships - this.keyRelationships.none = [] - const keysWithRelationship = new Set( - Array.prototype.concat(...Object.values(keyRelationships)) - ) - this.keys.forEach((_, id) => { - if (!keysWithRelationship.has(id)) { - this.keyRelationships.none?.push(id) - } - }) + public get identifier(): string { + return this.id } public getKey(id: IDidKeyDetails['id']): IDidKeyDetails | undefined { return this.keys.get(id) } - public getService(id: ServiceDetails['id']): ServiceDetails | undefined { + public getService(id: IServiceDetails['id']): IServiceDetails | undefined { return this.services.find((s) => s.id === id) } @@ -135,54 +71,10 @@ export class DidDetails implements IDidDetails { return [...this.keys.keys()] } - public getServices(type?: string): ServiceDetails[] { + public getServices(type?: string): IServiceDetails[] { if (type) { return this.services.filter((service) => service.type === type) } return this.services } - - public getNextTxIndex(increment = true): BN { - const nextIndex = this.lastTxIndex.addn(1) - if (increment) this.lastTxIndex = nextIndex - return nextIndex - } - - public getKeysForCall(call: CallMeta): IDidKeyDetails[] { - return getKeysForCall(this, call) - } - - public getKeysForExtrinsic( - apiOrMetadata: ApiOrMetadata, - extrinsic: Extrinsic - ): IDidKeyDetails[] { - return getKeysForExtrinsic(apiOrMetadata, this, extrinsic) - } - - public async authorizeExtrinsic( - extrinsic: Extrinsic, - signer: KeystoreSigner, - incrementTxIndex = true - ): Promise { - const { api } = await BlockchainApiConnection.getConnectionOrConnect() - const [signingKey] = this.getKeysForExtrinsic(api, extrinsic) - if (!signingKey) { - throw new Error( - `The details for did ${this.did} do not contain the required keys for this operation` - ) - } - return generateDidAuthenticatedTx({ - didIdentifier: this.identifier, - signingPublicKey: signingKey.publicKeyHex, - alg: getSignatureAlgForKeyType(signingKey.type), - signer, - call: extrinsic, - txCounter: this.getNextTxIndex(incrementTxIndex), - }) - } - - public async refreshTxIndex(): Promise { - this.lastTxIndex = await queryLastTxIndex(this.did) - return this - } } diff --git a/packages/kilt-did/src/DidDetails/DidDetails.utils.spec.ts b/packages/kilt-did/src/DidDetails/DidDetails.utils.spec.ts deleted file mode 100644 index f8d3028d4..000000000 --- a/packages/kilt-did/src/DidDetails/DidDetails.utils.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright 2018-2021 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 { mapCallToKeyRelationship } from './utils' - -it('gets the right key relationship for each pallet', () => { - // CTYPE - expect( - mapCallToKeyRelationship({ section: 'ctype', method: 'add' }) - ).toMatchInlineSnapshot(`"assertionMethod"`) - // DELEGATION - expect( - mapCallToKeyRelationship({ section: 'delegation', method: 'addDelegation' }) - ).toMatchInlineSnapshot(`"capabilityDelegation"`) - expect( - mapCallToKeyRelationship({ - section: 'delegation', - method: 'revokeDelegation', - }) - ).toMatchInlineSnapshot(`"capabilityDelegation"`) - // ATTESTATION - expect( - mapCallToKeyRelationship({ section: 'attestation', method: 'add' }) - ).toMatchInlineSnapshot(`"assertionMethod"`) - expect( - mapCallToKeyRelationship({ section: 'attestation', method: 'revoke' }) - ).toMatchInlineSnapshot(`"assertionMethod"`) - - // DID - expect( - mapCallToKeyRelationship({ - section: 'did', - method: 'submitDidCreateOperation', - }) - ).toMatchInlineSnapshot(`"paymentAccount"`) - expect( - mapCallToKeyRelationship({ - section: 'did', - method: 'submitDidUpdateOperation', - }) - ).toMatchInlineSnapshot(`"paymentAccount"`) - expect( - mapCallToKeyRelationship({ section: 'did', method: 'submitDidCall' }) - ).toMatchInlineSnapshot(`"paymentAccount"`) - // BALANCES - expect( - mapCallToKeyRelationship({ section: 'balances', method: 'transfer' }) - ).toMatchInlineSnapshot(`"paymentAccount"`) -}) diff --git a/packages/kilt-did/src/DidDetails/DidDetails.utils.ts b/packages/kilt-did/src/DidDetails/DidDetails.utils.ts new file mode 100644 index 000000000..c6fd19520 --- /dev/null +++ b/packages/kilt-did/src/DidDetails/DidDetails.utils.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2018-2021 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 type { + IDidDetails, + IDidKeyDetails, + KeystoreSigner, + SubmittableExtrinsic, +} from '@kiltprotocol/types' +import { KeyRelationship } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import type { TypeRegistry } from '@polkadot/types' +import type { PublicKeyRoleAssignment } from '../types' +import { generateCreateTx } from '../Did.chain' +import { + computeKeyId, + encodeDidPublicKey, + getKiltDidFromIdentifier, + getSignatureAlgForKeyType, + getIdentifierFromKiltDid, +} from '../Did.utils' + +/** + * Write on the KILT blockchain a new (full) DID with the provided details. + * + * @param didDetails The details of the new DID to write on chain. + * @param signer The signer (a KILT account) to be used to sign the resulting operation. + * @returns The signed extrinsic that can be submitted to the KILT blockchain to create the new DID. + */ +export async function writeNewDidFromDidDetails( + didDetails: IDidDetails, + signer: KeystoreSigner +): Promise { + const [signingKey] = didDetails.getKeys(KeyRelationship.authentication) + const [assertionMethod] = didDetails.getKeys(KeyRelationship.assertionMethod) + const [delegation] = didDetails.getKeys(KeyRelationship.capabilityDelegation) + const [keyAgreement] = didDetails.getKeys(KeyRelationship.keyAgreement) + + const keys: PublicKeyRoleAssignment = { + [KeyRelationship.assertionMethod]: { + ...assertionMethod, + publicKey: Crypto.coToUInt8(assertionMethod.publicKeyHex), + }, + [KeyRelationship.capabilityDelegation]: { + ...delegation, + publicKey: Crypto.coToUInt8(delegation.publicKeyHex), + }, + [KeyRelationship.keyAgreement]: { + ...keyAgreement, + publicKey: Crypto.coToUInt8(keyAgreement.publicKeyHex), + }, + } + return generateCreateTx({ + signer, + signingPublicKey: signingKey.publicKeyHex, + alg: getSignatureAlgForKeyType(signingKey.type), + didIdentifier: getIdentifierFromKiltDid(didDetails.did), + keys, + }) +} + +/** + * A tool to predict public key details if a given key would be added to an on-chain DID. + * Especially handy for predicting the key id or for deriving which DID may be claimed with a + * given authentication key. + * + * @param typeRegistry A TypeRegistry instance to which @kiltprotocol/types have been registered. + * @param publicKey The public key in hex or U8a encoding. + * @param type The [[CHAIN_SUPPORTED_KEY_TYPES]] variant indicating the key type. + * @param controller Optionally, set the the DID to which this key would be added. + * If left blank, the controller DID is inferred from the public key, mimicing the link between a new + * DID and its authentication key. + * @returns The [[IDidKeyDetails]] including key id, controller, type, and the public key hex encoded. + */ +export function deriveDidPublicKey( + typeRegistry: TypeRegistry, + publicKey: string | Uint8Array, + type: T, + controller?: string +): IDidKeyDetails { + const publicKeyHex = + typeof publicKey === 'string' ? publicKey : Crypto.u8aToHex(publicKey) + const publicKeyU8a = + publicKey instanceof Uint8Array ? publicKey : Crypto.coToUInt8(publicKey) + const keyIdentifier = computeKeyId( + encodeDidPublicKey(typeRegistry, { publicKey: publicKeyU8a, type }) + ) + const did = + controller || + getKiltDidFromIdentifier(Crypto.encodeAddress(publicKeyU8a, 38), 'full') + return { + id: `${did}#${keyIdentifier}`, + controller: did, + type, + publicKeyHex, + } +} diff --git a/packages/kilt-did/src/DidDetails/DidDetails.spec.ts b/packages/kilt-did/src/DidDetails/FullDidDetails.spec.ts similarity index 57% rename from packages/kilt-did/src/DidDetails/DidDetails.spec.ts rename to packages/kilt-did/src/DidDetails/FullDidDetails.spec.ts index ebbef86fa..61b4273b5 100644 --- a/packages/kilt-did/src/DidDetails/DidDetails.spec.ts +++ b/packages/kilt-did/src/DidDetails/FullDidDetails.spec.ts @@ -11,38 +11,44 @@ import { KeyRelationship } from '@kiltprotocol/types' import { BN } from '@polkadot/util' -import { DidDetails, DidDetailsCreationOpts } from './DidDetails' +import { mapCallToKeyRelationship } from './FullDidDetails.utils' +import { FullDidDetails, FullDidDetailsCreationOpts } from './FullDidDetails' describe('functional tests', () => { - const did = 'did:kilt:test' + const identifier = '4rp4rcDHP71YrBNvDhcH5iRoM3YzVoQVnCZvQPwPom9bjo2e' + const did = `did:kilt:${identifier}` const keys = [ { id: `${did}#1`, controller: did, includedAt: 100, type: 'ed25519', - publicKeyHex: '0xed25519', + publicKeyHex: + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }, { id: `${did}#2`, controller: did, includedAt: 250, type: 'x25519', - publicKeyHex: '0x255191', + publicKeyHex: + '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', }, { id: `${did}#3`, controller: did, includedAt: 250, type: 'x25519', - publicKeyHex: '0x255192', + publicKeyHex: + '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', }, { id: `${did}#4`, controller: did, includedAt: 200, type: 'sr25519', - publicKeyHex: '0xbeef', + publicKeyHex: + '0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', }, ] const services = [ @@ -57,7 +63,7 @@ describe('functional tests', () => { serviceEndpoint: '123344', }, ] - const didDetails: DidDetailsCreationOpts = { + const didDetails: FullDidDetailsCreationOpts = { did, keys, keyRelationships: { @@ -69,38 +75,38 @@ describe('functional tests', () => { services, } - it('creates DidDetails', () => { - const dd = new DidDetails(didDetails) + it('creates FullDidDetails', () => { + const dd = new FullDidDetails(didDetails) expect(dd.did).toEqual(did) - expect(dd.identifier).toMatchInlineSnapshot(`"test"`) + expect(dd.identifier).toEqual(identifier) expect(dd.getKeys()).toMatchInlineSnapshot(` Array [ Object { - "controller": "did:kilt:test", - "id": "did:kilt:test#1", + "controller": "${did}", + "id": "${did}#1", "includedAt": 100, - "publicKeyHex": "0xed25519", + "publicKeyHex": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "type": "ed25519", }, Object { - "controller": "did:kilt:test", - "id": "did:kilt:test#2", + "controller": "${did}", + "id": "${did}#2", "includedAt": 250, - "publicKeyHex": "0x255191", + "publicKeyHex": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "type": "x25519", }, Object { - "controller": "did:kilt:test", - "id": "did:kilt:test#3", + "controller": "${did}", + "id": "${did}#3", "includedAt": 250, - "publicKeyHex": "0x255192", + "publicKeyHex": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "type": "x25519", }, Object { - "controller": "did:kilt:test", - "id": "did:kilt:test#4", + "controller": "${did}", + "id": "${did}#4", "includedAt": 200, - "publicKeyHex": "0xbeef", + "publicKeyHex": "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", "type": "sr25519", }, ] @@ -108,12 +114,12 @@ describe('functional tests', () => { expect(dd.getServices()).toMatchInlineSnapshot(` Array [ Object { - "id": "did:kilt:test#service1", + "id": "${did}#service1", "serviceEndpoint": "example.com", "type": "messaging", }, Object { - "id": "did:kilt:test#service2", + "id": "${did}#service2", "serviceEndpoint": "123344", "type": "telephone", }, @@ -122,7 +128,7 @@ describe('functional tests', () => { }) it('gets keys via role', () => { - let dd = new DidDetails(didDetails) + let dd = new FullDidDetails(didDetails) expect(dd.getKeyIds(KeyRelationship.authentication)).toEqual([keys[0].id]) expect(dd.getKeys(KeyRelationship.authentication)).toEqual([keys[0]]) expect(dd.getKeyIds(KeyRelationship.keyAgreement)).toEqual( @@ -133,7 +139,7 @@ describe('functional tests', () => { ).toEqual(didDetails.keyRelationships[KeyRelationship.keyAgreement]) expect(dd.getKeyIds(KeyRelationship.assertionMethod)).toEqual([keys[3].id]) - dd = new DidDetails({ + dd = new FullDidDetails({ ...didDetails, keyRelationships: { [KeyRelationship.authentication]: [keys[3].id] }, }) @@ -144,7 +150,7 @@ describe('functional tests', () => { }) it('gets service via type', () => { - const dd = new DidDetails(didDetails) + const dd = new FullDidDetails(didDetails) expect(dd.getServices('messaging').map((s) => s.type)).toEqual([ 'messaging', ]) @@ -154,14 +160,14 @@ describe('functional tests', () => { }) it('returns the next nonce', () => { - let dd = new DidDetails(didDetails) + let dd = new FullDidDetails(didDetails) expect(dd.getNextTxIndex().toString()).toEqual( didDetails.lastTxIndex.addn(1).toString() ) expect(dd.getNextTxIndex().toString()).toEqual( didDetails.lastTxIndex.addn(2).toString() ) - dd = new DidDetails(didDetails) + dd = new FullDidDetails(didDetails) expect(dd.getNextTxIndex(false).toString()).toEqual( didDetails.lastTxIndex.addn(1).toString() ) @@ -171,7 +177,7 @@ describe('functional tests', () => { }) it('gets the correct keys for each pallet', () => { - const dd = new DidDetails({ + const dd = new FullDidDetails({ ...didDetails, keyRelationships: { [KeyRelationship.authentication]: [keys[0].id], @@ -185,7 +191,7 @@ describe('functional tests', () => { .map((key) => key.id) ).toMatchInlineSnapshot(` Array [ - "did:kilt:test#4", + "${did}#4", ] `) expect( @@ -194,7 +200,7 @@ describe('functional tests', () => { .map((key) => key.id) ).toMatchInlineSnapshot(` Array [ - "did:kilt:test#2", + "${did}#2", ] `) expect( @@ -203,8 +209,58 @@ describe('functional tests', () => { .map((key) => key.id) ).toMatchInlineSnapshot(` Array [ - "did:kilt:test#4", + "${did}#4", ] `) }) }) + +describe('Key mapping tests', () => { + it('gets the right key relationship for each pallet', () => { + // CTYPE + expect( + mapCallToKeyRelationship({ section: 'ctype', method: 'add' }) + ).toMatchInlineSnapshot(`"assertionMethod"`) + // DELEGATION + expect( + mapCallToKeyRelationship({ + section: 'delegation', + method: 'addDelegation', + }) + ).toMatchInlineSnapshot(`"capabilityDelegation"`) + expect( + mapCallToKeyRelationship({ + section: 'delegation', + method: 'revokeDelegation', + }) + ).toMatchInlineSnapshot(`"capabilityDelegation"`) + // ATTESTATION + expect( + mapCallToKeyRelationship({ section: 'attestation', method: 'add' }) + ).toMatchInlineSnapshot(`"assertionMethod"`) + expect( + mapCallToKeyRelationship({ section: 'attestation', method: 'revoke' }) + ).toMatchInlineSnapshot(`"assertionMethod"`) + + // DID + expect( + mapCallToKeyRelationship({ + section: 'did', + method: 'create', + }) + ).toMatchInlineSnapshot(`"paymentAccount"`) + expect( + mapCallToKeyRelationship({ + section: 'did', + method: 'update', + }) + ).toMatchInlineSnapshot(`"authentication"`) + expect( + mapCallToKeyRelationship({ section: 'did', method: 'submitDidCall' }) + ).toMatchInlineSnapshot(`"paymentAccount"`) + // BALANCES + expect( + mapCallToKeyRelationship({ section: 'balances', method: 'transfer' }) + ).toMatchInlineSnapshot(`"paymentAccount"`) + }) +}) diff --git a/packages/kilt-did/src/DidDetails/FullDidDetails.ts b/packages/kilt-did/src/DidDetails/FullDidDetails.ts new file mode 100644 index 000000000..6a9a9e4da --- /dev/null +++ b/packages/kilt-did/src/DidDetails/FullDidDetails.ts @@ -0,0 +1,189 @@ +/** + * Copyright 2018-2021 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 type { Extrinsic } from '@polkadot/types/interfaces' +import { BlockchainApiConnection } from '@kiltprotocol/chain-helpers' +import type { + IDidKeyDetails, + KeystoreSigner, + SubmittableExtrinsic, + ApiOrMetadata, + CallMeta, + IServiceDetails, +} from '@kiltprotocol/types' +import { KeyRelationship } from '@kiltprotocol/types' +import { BN } from '@polkadot/util' +import { MapKeyToRelationship } from '../types' +import { generateDidAuthenticatedTx, queryLastTxIndex } from '../Did.chain' +import { getKeysForCall, getKeysForExtrinsic } from './FullDidDetails.utils' +import { + getSignatureAlgForKeyType, + getIdentifierFromKiltDid, + parseDidUrl, +} from '../Did.utils' +import { DidDetails } from './DidDetails' + +export interface FullDidDetailsCreationOpts { + // The full DID URI, following the scheme did:kilt: + did: string + keys: IDidKeyDetails[] + keyRelationships: MapKeyToRelationship + lastTxIndex: BN + services?: IServiceDetails[] +} + +function errorCheck({ + did, + keys, + keyRelationships, +}: Required): void { + if (!did) { + throw Error('did is required for FullDidDetails') + } + const { type } = parseDidUrl(did) + if (type !== 'full') { + throw Error('Only a full DID URI is allowed.') + } + const keyIds = new Set(keys.map((key) => key.id)) + if (keyRelationships[KeyRelationship.authentication]?.length !== 1) { + throw Error( + `One and only one ${KeyRelationship.authentication} key is required on FullDidDetails` + ) + } + const allowedKeyRelationships: string[] = [ + ...Object.values(KeyRelationship), + 'none', + ] + Object.keys(keyRelationships).forEach((kr) => { + if (!allowedKeyRelationships.includes(kr)) { + throw Error( + `key relationship ${kr} is not recognized. Allowed: ${KeyRelationship}` + ) + } + }) + const keyReferences = new Set( + Array.prototype.concat(...Object.values(keyRelationships)) + ) + keyReferences.forEach((id) => { + if (!keyIds.has(id)) throw new Error(`No key with id ${id} in "keys"`) + }) +} + +export class FullDidDetails extends DidDetails { + /// The latest version for KILT full DIDs. + public static readonly FULL_DID_LATEST_VERSION = 1 + + private lastTxIndex: BN + + constructor({ + did, + keys, + keyRelationships = {}, + lastTxIndex, + services = [], + }: FullDidDetailsCreationOpts) { + errorCheck({ + did, + keys, + keyRelationships, + services, + lastTxIndex, + }) + + const id = getIdentifierFromKiltDid(did) + super(did, id, services) + + this.keys = new Map(keys.map((key) => [key.id, key])) + this.lastTxIndex = lastTxIndex + this.keyRelationships = keyRelationships + this.keyRelationships.none = [] + const keysWithRelationship = new Set( + Array.prototype.concat(...Object.values(keyRelationships)) + ) + this.keys.forEach((_, keyId) => { + if (!keysWithRelationship.has(keyId)) { + this.keyRelationships.none?.push(keyId) + } + }) + } + + /** + * Gets the next nonce/transaction index required for DID authorized blockchain transactions. + * + * @param increment Flag indicating whether the retrieved tx index should be increased. + * @returns A [[BN]] indicating the next transaction index. + */ + public getNextTxIndex(increment = true): BN { + const nextIndex = this.lastTxIndex.addn(1) + if (increment) this.lastTxIndex = nextIndex + return nextIndex + } + + /** + * Returns all the DID keys that could be used to authorize the submission of the provided call. + * + * @param call The call to submit. + * @returns The set of keys that could be used to sign the call. + */ + public getKeysForCall(call: CallMeta): IDidKeyDetails[] { + return getKeysForCall(this, call) + } + + /** + * Returns all the DID keys that could be used to authorize the submission of the provided extrinsic. + * + * @param apiOrMetadata The node runtime information to use to retrieve the required information. + * @param extrinsic The extrinsic to submit. + * @returns The set of keys that could be used to sign the extrinsic. + */ + public getKeysForExtrinsic( + apiOrMetadata: ApiOrMetadata, + extrinsic: Extrinsic + ): IDidKeyDetails[] { + return getKeysForExtrinsic(apiOrMetadata, this, extrinsic) + } + + /** + * Signs and returns the provided unsigned extrinsic with the right DID key, if present. Otherwise, it will return an error. + * + * @param extrinsic The unsigned extrinsic to sign. + * @param signer The keystore to be used to sign the encoded extrinsic. + * @param incrementTxIndex Flag indicating whether the DID nonce should be increased before submitting the operation or not. + * @returns The DID-signed submittable extrinsic. + */ + public async authorizeExtrinsic( + extrinsic: Extrinsic, + signer: KeystoreSigner, + incrementTxIndex = true + ): Promise { + const { api } = await BlockchainApiConnection.getConnectionOrConnect() + const [signingKey] = this.getKeysForExtrinsic(api, extrinsic) + if (!signingKey) { + throw new Error( + `The details for did ${this.did} do not contain the required keys for this operation` + ) + } + return generateDidAuthenticatedTx({ + didIdentifier: this.identifier, + signingPublicKey: signingKey.publicKeyHex, + alg: getSignatureAlgForKeyType(signingKey.type), + signer, + call: extrinsic, + txCounter: this.getNextTxIndex(incrementTxIndex), + }) + } + + /** + * Retrieve from the chain the last used nonce for the DID. + * + * @returns The last used nonce. + */ + public async refreshTxIndex(): Promise { + this.lastTxIndex = await queryLastTxIndex(this.did) + return this + } +} diff --git a/packages/kilt-did/src/DidDetails/utils.ts b/packages/kilt-did/src/DidDetails/FullDidDetails.utils.ts similarity index 50% rename from packages/kilt-did/src/DidDetails/utils.ts rename to packages/kilt-did/src/DidDetails/FullDidDetails.utils.ts index aa25fdb23..319aa3bd6 100644 --- a/packages/kilt-did/src/DidDetails/utils.ts +++ b/packages/kilt-did/src/DidDetails/FullDidDetails.utils.ts @@ -8,27 +8,17 @@ import type { ApiPromise } from '@polkadot/api' import type { Extrinsic } from '@polkadot/types/interfaces' import { TypeRegistry } from '@polkadot/types' +import { BN } from '@polkadot/util' import type { ApiOrMetadata, CallMeta, IDidDetails, IDidKeyDetails, - KeystoreSigner, - SubmittableExtrinsic, VerificationKeyRelationship, } from '@kiltprotocol/types' import { KeyRelationship } from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' -import { BN } from '@polkadot/util' -import { DidDetails, MapKeyToRelationship } from './DidDetails' -import { PublicKeyRoleAssignment } from '../types' -import { DidChain, DidUtils } from '..' -import { - computeKeyId, - encodeDidPublicKey, - getKiltDidFromIdentifier, - getSignatureAlgForKeyType, -} from '../Did.utils' +import type { MapKeyToRelationship } from '../types' +import { FullDidDetails } from './FullDidDetails' interface MethodMapping { default: V @@ -123,11 +113,11 @@ export function getKeyIdsForExtrinsic( return getKeyIdsForCall(didDetails, callMeta) } -export function newDidDetailsfromKeys( +export function newFullDidDetailsfromKeys( keys: Partial> & { [KeyRelationship.authentication]: IDidKeyDetails } -): DidDetails { +): FullDidDetails { const did = keys[KeyRelationship.authentication].controller const allKeys: IDidKeyDetails[] = [] const keyRelationships: MapKeyToRelationship = {} @@ -137,111 +127,10 @@ export function newDidDetailsfromKeys( allKeys.push(thisKey) } }) - return new DidDetails({ + return new FullDidDetails({ did, keys: allKeys, keyRelationships, lastTxIndex: new BN(0), }) } - -export async function writeNewDidFromDidDetails( - didDetails: IDidDetails, - signer: KeystoreSigner -): Promise { - const [signingKey] = didDetails.getKeys(KeyRelationship.authentication) - const [assertionMethod] = didDetails.getKeys(KeyRelationship.assertionMethod) - const [delegation] = didDetails.getKeys(KeyRelationship.capabilityDelegation) - const [keyAgreement] = didDetails.getKeys(KeyRelationship.keyAgreement) - - const keys: PublicKeyRoleAssignment = { - [KeyRelationship.assertionMethod]: { - ...assertionMethod, - publicKey: Crypto.coToUInt8(assertionMethod.publicKeyHex), - }, - [KeyRelationship.capabilityDelegation]: { - ...delegation, - publicKey: Crypto.coToUInt8(delegation.publicKeyHex), - }, - [KeyRelationship.keyAgreement]: { - ...keyAgreement, - publicKey: Crypto.coToUInt8(keyAgreement.publicKeyHex), - }, - } - return DidChain.generateCreateTx({ - signer, - signingPublicKey: signingKey.publicKeyHex, - alg: getSignatureAlgForKeyType(signingKey.type), - didIdentifier: DidUtils.getIdentifierFromKiltDid(didDetails.did), - keys, - }) -} - -export async function signWithKey( - toSign: Uint8Array | string, - key: IDidKeyDetails, - signer: KeystoreSigner -): Promise<{ keyId: string; alg: string; signature: Uint8Array }> { - const alg = getSignatureAlgForKeyType(key.type) - const { data: signature } = await signer.sign({ - publicKey: Crypto.coToUInt8(key.publicKeyHex), - alg, - data: Crypto.coToUInt8(toSign), - }) - return { keyId: key.id, signature, alg } -} - -export async function signWithDid( - toSign: Uint8Array | string, - did: IDidDetails, - signer: KeystoreSigner, - whichKey: KeyRelationship | IDidKeyDetails['id'] -): Promise<{ keyId: string; alg: string; signature: Uint8Array }> { - let key: IDidKeyDetails | undefined - if (Object.values(KeyRelationship).includes(whichKey as KeyRelationship)) { - ;[key] = did.getKeys(KeyRelationship.authentication) - } else { - key = did.getKey(whichKey) - } - if (!key) { - throw Error(`failed to find key on DidDetails (${did.did}): ${whichKey}`) - } - return signWithKey(toSign, key, signer) -} - -/** - * A tool to predict public key details if a given key would be added to an on-chain DID. - * Especially handy for predicting the key id or for deriving which DID may be claimed with a - * given authentication key. - * - * @param typeRegistry A TypeRegistry instance to which @kiltprotocol/types have been registered. - * @param publicKey The public key in hex or U8a encoding. - * @param type The [[CHAIN_SUPPORTED_KEY_TYPES]] variant indicating the key type. - * @param controller Optionally, set the the DID to which this key would be added. - * If left blank, the controller DID is inferred from the public key, mimicing the link between a new - * DID and its authentication key. - * @returns The [[IDidKeyDetails]] including key id, controller, type, and the public key hex encoded. - */ -export function deriveDidPublicKey( - typeRegistry: TypeRegistry, - publicKey: string | Uint8Array, - type: T, - controller?: string -): IDidKeyDetails { - const publicKeyHex = - typeof publicKey === 'string' ? publicKey : Crypto.u8aToHex(publicKey) - const publicKeyU8a = - publicKey instanceof Uint8Array ? publicKey : Crypto.coToUInt8(publicKey) - const keyIdentifier = computeKeyId( - encodeDidPublicKey(typeRegistry, { publicKey: publicKeyU8a, type }) - ) - const did = - controller || - getKiltDidFromIdentifier(Crypto.encodeAddress(publicKeyU8a, 38)) - return { - id: `${did}#${keyIdentifier}`, - controller: did, - type, - publicKeyHex, - } -} diff --git a/packages/kilt-did/src/DidDetails/LightDidDetails.spec.ts b/packages/kilt-did/src/DidDetails/LightDidDetails.spec.ts new file mode 100644 index 000000000..196f400f6 --- /dev/null +++ b/packages/kilt-did/src/DidDetails/LightDidDetails.spec.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2018-2021 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. + */ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +/** + * @group unit/did + */ + +import { Crypto } from '@kiltprotocol/utils' +import type { IServiceDetails } from '@kiltprotocol/types' +import { hexToU8a } from '@polkadot/util' +import { LightDidDetails, LightDidDetailsCreationOpts } from './LightDidDetails' +import type { INewPublicKey } from '../types' + +describe('Light DID v1 tests', () => { + const authPublicKey = hexToU8a( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + ) + const encPublicKey = hexToU8a( + '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + ) + const address = Crypto.encodeAddress(authPublicKey, 38) + const authenticationDidKeyDetails: INewPublicKey = { + publicKey: authPublicKey, + type: 'ed25519', + } + let encryptionDidKeyDetails: INewPublicKey | undefined + let services: IServiceDetails[] | undefined + + it('creates LightDidDetails from authentication key, encryption key, and service endpoints', () => { + encryptionDidKeyDetails = { + publicKey: encPublicKey, + type: 'x25519', + } + services = [ + { + id: `service1`, + type: 'messaging', + serviceEndpoint: 'example.com', + }, + { + id: `service2`, + type: 'telephone', + serviceEndpoint: '123344', + }, + ] + + const didCreationDetails: LightDidDetailsCreationOpts = { + authenticationKey: authenticationDidKeyDetails, + encryptionKey: encryptionDidKeyDetails, + services, + } + + const did = new LightDidDetails(didCreationDetails) + expect(did.did).toEqual( + `did:kilt:light:01${address}:omFlomlwdWJsaWNLZXlYILu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7ZHR5cGVmeDI1NTE5YXOCo2JpZGhzZXJ2aWNlMWR0eXBlaW1lc3NhZ2luZ29zZXJ2aWNlRW5kcG9pbnRrZXhhbXBsZS5jb22jYmlkaHNlcnZpY2UyZHR5cGVpdGVsZXBob25lb3NlcnZpY2VFbmRwb2ludGYxMjMzNDQ=` + ) + }) + + it('creates LightDidDetails from authentication key and encryption key only', () => { + encryptionDidKeyDetails = { + publicKey: encPublicKey, + type: 'x25519', + } + + const didCreationDetails: LightDidDetailsCreationOpts = { + authenticationKey: authenticationDidKeyDetails, + encryptionKey: encryptionDidKeyDetails, + } + + const did = new LightDidDetails(didCreationDetails) + expect(did.did).toEqual( + `did:kilt:light:01${address}:oWFlomlwdWJsaWNLZXlYILu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7ZHR5cGVmeDI1NTE5` + ) + }) + + it('creates LightDidDetails from authentication key and service endpoints only', () => { + services = [ + { + id: `service1`, + type: 'messaging', + serviceEndpoint: 'example.com', + }, + { + id: `service2`, + type: 'telephone', + serviceEndpoint: '123344', + }, + ] + + const didCreationDetails: LightDidDetailsCreationOpts = { + authenticationKey: authenticationDidKeyDetails, + services, + } + + const did = new LightDidDetails(didCreationDetails) + expect(did.did).toEqual( + `did:kilt:light:01${address}:oWFzgqNiaWRoc2VydmljZTFkdHlwZWltZXNzYWdpbmdvc2VydmljZUVuZHBvaW50a2V4YW1wbGUuY29to2JpZGhzZXJ2aWNlMmR0eXBlaXRlbGVwaG9uZW9zZXJ2aWNlRW5kcG9pbnRmMTIzMzQ0` + ) + }) + + it('creates LightDidDetails from authentication key only', () => { + const didCreationDetails: LightDidDetailsCreationOpts = { + authenticationKey: authenticationDidKeyDetails, + } + + const did = new LightDidDetails(didCreationDetails) + // no concat of : and encoded details + expect(did.did).toEqual(`did:kilt:light:01${address}`) + }) +}) diff --git a/packages/kilt-did/src/DidDetails/LightDidDetails.ts b/packages/kilt-did/src/DidDetails/LightDidDetails.ts new file mode 100644 index 000000000..7fdafc180 --- /dev/null +++ b/packages/kilt-did/src/DidDetails/LightDidDetails.ts @@ -0,0 +1,136 @@ +/** + * Copyright 2018-2021 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. + */ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import type { IDidKeyDetails, IServiceDetails } from '@kiltprotocol/types' +import { KeyRelationship } from '@kiltprotocol/types' +import { SDKErrors, Crypto } from '@kiltprotocol/utils' +import { encodeAddress } from '@polkadot/util-crypto' +import { + getEncodingForSigningKeyType, + getKiltDidFromIdentifier, +} from '../Did.utils' +import type { INewPublicKey } from '../types' +import { DidDetails } from './DidDetails' +import { serializeAndEncodeAdditionalLightDidDetails } from './LightDidDetails.utils' + +export interface LightDidDetailsCreationOpts { + authenticationKey: INewPublicKey + encryptionKey?: INewPublicKey + // For light DIDs, the service IDs do not have to include the whole DID URI, but just the service ID. The complete URI is generated internally in the constructor. + services?: IServiceDetails[] +} + +export class LightDidDetails extends DidDetails { + /// The latest version for KILT light DIDs. + public static readonly LIGHT_DID_LATEST_VERSION = 1 + + constructor({ + authenticationKey, + encryptionKey = undefined, + services = [], + }: LightDidDetailsCreationOpts) { + const encodedDetails = serializeAndEncodeAdditionalLightDidDetails({ + encryptionKey, + services, + }) + const authenticationKeyTypeEncoding = getEncodingForSigningKeyType( + authenticationKey.type + ) + if (!authenticationKeyTypeEncoding) { + throw SDKErrors.ERROR_UNSUPPORTED_KEY + } + + // A KILT light DID identifier becomes + const id = authenticationKeyTypeEncoding.concat( + encodeAddress(authenticationKey.publicKey, 38) + ) + let did = getKiltDidFromIdentifier( + id, + 'light', + LightDidDetails.LIGHT_DID_LATEST_VERSION + ) + if (encodedDetails) { + did = did.concat(':', encodedDetails) + } + + super( + did, + id, + services.map((service) => { + return { ...service, id: `${did}#${service.id}` } + }) + ) + + // Authentication key always has the #authentication ID. + this.keys = new Map([ + [ + `${this.did}#authentication`, + { + controller: this.did, + id: `${this.did}#authentication`, + publicKeyHex: Crypto.u8aToHex(authenticationKey.publicKey), + type: authenticationKey.type, + }, + ], + ]) + this.keyRelationships = { + authentication: [`${this.didUri}#authentication`], + } + + // Encryption key always has the #encryption ID. + if (encryptionKey) { + this.keys.set(`${this.didUri}#encryption`, { + controller: this.did, + id: `${this.did}#encryption`, + publicKeyHex: Crypto.u8aToHex(encryptionKey.publicKey), + type: encryptionKey.type, + }) + this.keyRelationships.keyAgreement = [`${this.didUri}#encryption`] + } + } + + public get did(): string { + return this.didUri + } + + public get identifier(): string { + return this.id + } + + public getKey(id: IDidKeyDetails['id']): IDidKeyDetails | undefined { + return this.keys.get(id) + } + + public getService(id: IServiceDetails['id']): IServiceDetails | undefined { + return this.services.find((s) => s.id === id) + } + + public getKeys(relationship?: KeyRelationship | 'none'): IDidKeyDetails[] { + if (relationship) { + return this.getKeyIds(relationship).map((id) => this.getKey(id)!) + } + return [...this.keys.values()] + } + + public getKeyIds( + relationship?: KeyRelationship | 'none' + ): Array { + if (relationship) { + return this.keyRelationships[relationship] || [] + } + return [...this.keys.keys()] + } + + public getServices(type?: string): IServiceDetails[] { + if (type) { + return this.services.filter((service) => service.type === type) + } + return this.services + } +} diff --git a/packages/kilt-did/src/DidDetails/LightDidDetails.utils.ts b/packages/kilt-did/src/DidDetails/LightDidDetails.utils.ts new file mode 100644 index 000000000..2ac7bd702 --- /dev/null +++ b/packages/kilt-did/src/DidDetails/LightDidDetails.utils.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2018-2021 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 { encode as cborEncode, decode as cborDecode } from 'cbor' +import type { LightDidDetailsCreationOpts } from './LightDidDetails' + +const ENCRYPTION_KEY_MAP_KEY = 'e' +const SERVICES_MAP_KEY = 's' + +/** + * Serialize the optional encryption key and service endpoints of an off-chain DID using the CBOR serialization algorithm and encoding the result in Base64 format. + * + * @param root0 + * @param root0.encryptionKey + * @param root0.services + * @returns The Base64-encoded and CBOR-serialized off-chain DID optional details. + */ +export function serializeAndEncodeAdditionalLightDidDetails({ + encryptionKey, + services, +}: Pick): + | string + | null { + const objectToSerialize: Map = new Map() + if (encryptionKey) { + objectToSerialize.set(ENCRYPTION_KEY_MAP_KEY, encryptionKey) + } + if (services && services.length) { + objectToSerialize.set(SERVICES_MAP_KEY, services) + } + + if (!objectToSerialize.size) { + return null + } + + return cborEncode(objectToSerialize).toString('base64') +} + +// Version # not used for now +export function decodeAndDeserializeAdditionalLightDidDetails( + rawInput: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + version = 1 +): Pick { + const decodedPayload: Map = cborDecode(rawInput, { + encoding: 'base64', + }) + + return { + encryptionKey: decodedPayload[ENCRYPTION_KEY_MAP_KEY], + services: decodedPayload[SERVICES_MAP_KEY], + } +} diff --git a/packages/kilt-did/src/DidDetails/index.ts b/packages/kilt-did/src/DidDetails/index.ts index 4d35abc99..ecaf4f3a9 100644 --- a/packages/kilt-did/src/DidDetails/index.ts +++ b/packages/kilt-did/src/DidDetails/index.ts @@ -6,4 +6,8 @@ */ export * from './DidDetails' -export * as DidDetailsUtils from './utils' +export * from './DidDetails.utils' +export * from './FullDidDetails' +export * from './FullDidDetails.utils' +export * from './LightDidDetails' +export * from './LightDidDetails.utils' diff --git a/packages/kilt-did/src/DidResolver/DefaultResolver.ts b/packages/kilt-did/src/DidResolver/DefaultResolver.ts index ca7b38aae..c81d1c54e 100644 --- a/packages/kilt-did/src/DidResolver/DefaultResolver.ts +++ b/packages/kilt-did/src/DidResolver/DefaultResolver.ts @@ -8,18 +8,38 @@ import type { IDidResolver, IDidKeyDetails, + IDidDetails, ResolverOpts, - ServiceDetails, + IServiceDetails, } from '@kiltprotocol/types' import { KeyRelationship } from '@kiltprotocol/types' -import { DidDetails, DidDetailsCreationOpts } from '../DidDetails/DidDetails' +import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { LightDidDetails } from '../DidDetails/LightDidDetails' +import { FullDidDetails } from '../DidDetails/FullDidDetails' +import type { LightDidDetailsCreationOpts } from '../DidDetails/LightDidDetails' +import { decodeAndDeserializeAdditionalLightDidDetails } from '../DidDetails/LightDidDetails.utils' import { queryById, queryKey } from '../Did.chain' -import { getKiltDidFromIdentifier, parseDidUrl } from '../Did.utils' +import { + getKiltDidFromIdentifier, + getSigningKeyTypeFromEncoding, + parseDidUrl, +} from '../Did.utils' +import type { IDidParsingResult } from '../types' -async function detailsFromIdentifier( +/** + * Retrieves all the details associated with a DID from the KILT blockchain. + * + * @param identifier The full DID identifier. + * @param serviceResolver The optional service resolver to resolve the DID service endpoints. + * @param serviceResolver.servicesResolver + * @param version The DID version number. + * @returns The full DID details queried from the KILT blockchain. + */ +async function queryFullDetailsFromIdentifier( identifier: string, - { servicesResolver }: ResolverOpts -): Promise { + { servicesResolver }: ResolverOpts, + version = FullDidDetails.FULL_DID_LATEST_VERSION +): Promise { const didRec = await queryById(identifier) if (!didRec) return null const { @@ -32,7 +52,7 @@ async function detailsFromIdentifier( lastTxCounter, } = didRec - const keyRelationships: DidDetailsCreationOpts['keyRelationships'] = { + const keyRelationships: FullDidDetails['keyRelationships'] = { [KeyRelationship.authentication]: [authenticationKey], [KeyRelationship.keyAgreement]: keyAgreementKeys, } @@ -44,44 +64,162 @@ async function detailsFromIdentifier( capabilityDelegationKey, ] } - const didDetails: DidDetailsCreationOpts = { - did: getKiltDidFromIdentifier(identifier), + + let services: IServiceDetails[] = [] + if (servicesResolver && endpointData) { + const { contentHash, contentType, urls } = endpointData + services = await servicesResolver(contentHash, urls, contentType) + } + + return new FullDidDetails({ + did: getKiltDidFromIdentifier(identifier, 'full', version), keys: publicKeys, keyRelationships, lastTxIndex: lastTxCounter.toBn(), + services, + }) +} + +function buildLightDetailsFromMatch({ + identifier, + version, + encodedDetails, +}: Pick< + IDidParsingResult, + 'identifier' | 'version' | 'encodedDetails' +>): LightDidDetails { + // In light DIDs the key type encoding (first two chars) is part of the identifier. + // We are sure the URI follows the expected structure as it has been checked in `parseDidUrl`. + const keyTypeEncoding = identifier.substring(0, 2) + const keyType = getSigningKeyTypeFromEncoding(keyTypeEncoding) + if (!keyType) { + throw Error() } - if (servicesResolver && endpointData) { - const { contentHash, contentType, urls } = endpointData - didDetails.services = await servicesResolver(contentHash, urls, contentType) + const kiltIdentifier = identifier.substring(2) + const lightDidCreationOptions: LightDidDetailsCreationOpts = { + authenticationKey: { + publicKey: Crypto.decodeAddress(kiltIdentifier, true, 38), + type: keyType, + }, } - return new DidDetails(didDetails) + + if (encodedDetails) { + const decodedDetails = decodeAndDeserializeAdditionalLightDidDetails( + encodedDetails, + version + ) + lightDidCreationOptions.encryptionKey = decodedDetails.encryptionKey + lightDidCreationOptions.services = decodedDetails.services + } + + return new LightDidDetails(lightDidCreationOptions) } -export async function resolveKey( - didUri: string -): Promise { - const { did, fragment } = parseDidUrl(didUri) - return queryKey(did, fragment) +/** + * Resolve a DID URI (including a key ID or a service endpoint ID). + * + * @param didUri The DID URI to resolve. + * @param opts The options to provide to the service resolver. + * @returns The DID, key, or service endpoint details depending on the input URI. If not resource can be resolved, null is returned. + */ +export async function resolve( + didUri: string, + opts: ResolverOpts = {} +): Promise { + const { identifier, type, version, fragment, encodedDetails } = parseDidUrl( + didUri + ) + + switch (type) { + case 'full': { + const details = await queryFullDetailsFromIdentifier( + identifier, + opts, + version + ) + // If the URI is a subject DID, return the retrieved details. + if (!fragment) { + return details + } + + // Otherwise, return either the key or the service endpoints referenced by the URI. + return details?.getKey(didUri) || details?.getService(didUri) || null + } + case 'light': { + let details: LightDidDetails + try { + details = buildLightDetailsFromMatch({ + identifier, + version, + encodedDetails, + }) + } catch { + throw SDKErrors.ERROR_INVALID_DID_FORMAT(didUri) + } + // If the URI is a subject DID, return the retrieved details. + if (!fragment) { + return details + } + + return details?.getKey(didUri) || details?.getService(didUri) || null + } + default: + throw SDKErrors.ERROR_UNSUPPORTED_DID(didUri) + } } +/** + * Resolve a DID URI (including a key or a service endpoint reference) to the details of the DID subject. + * + * @param did The subject', the key, or the service endpoint identifier. + * @param opts The options to provide to the service resolver. + * @returns The details associated with the DID subject. + */ export async function resolveDoc( did: string, opts: ResolverOpts = {} -): Promise { - const { identifier } = parseDidUrl(did) - return detailsFromIdentifier(identifier, opts) +): Promise { + const { fragment } = parseDidUrl(did) + + let didToResolve = did + if (fragment) { + // eslint-disable-next-line prefer-destructuring + didToResolve = didToResolve.split('#')[0] + } + + return resolve(didToResolve, opts) as Promise } -export async function resolve( - didUri: string, - opts: ResolverOpts = {} -): Promise { - const { fragment, identifier } = parseDidUrl(didUri) - const details = await detailsFromIdentifier(identifier, opts) - if (!fragment || !details) { - return details +/** + * Resolve a DID key URI to the key details. + * + * @param didUri The DID key URI. + * @returns The details associated with the key. + */ +export async function resolveKey( + didUri: string +): Promise { + const { did, fragment, type } = parseDidUrl(didUri) + + // A fragment IS expected to resolve a key + if (!fragment) { + throw SDKErrors.ERROR_INVALID_DID_FORMAT + } + + switch (type) { + case 'full': + return queryKey(did, fragment) + case 'light': { + const resolvedDetails = await resolveDoc(didUri) + if (!resolvedDetails) { + throw SDKErrors.ERROR_INVALID_DID_FORMAT(didUri) + } + // The fragment includes the '#' symbol which we do not need + return resolvedDetails.getKey(fragment.substring(1)) || null + } + default: + throw SDKErrors.ERROR_UNSUPPORTED_DID(didUri) } - return details?.getKey(didUri) || details?.getService(didUri) || null } export const DefaultResolver: IDidResolver = { resolveDoc, resolveKey, resolve } diff --git a/packages/kilt-did/src/DidResolver/Resolver.spec.ts b/packages/kilt-did/src/DidResolver/Resolver.spec.ts index b12dde283..03c76a25a 100644 --- a/packages/kilt-did/src/DidResolver/Resolver.spec.ts +++ b/packages/kilt-did/src/DidResolver/Resolver.spec.ts @@ -5,6 +5,8 @@ * found in the LICENSE file in the root directory of this source tree. */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + /** * @group unit/did */ @@ -12,6 +14,12 @@ import { TypeRegistry } from '@kiltprotocol/chain-helpers' import { KeyRelationship, ServicesResolver } from '@kiltprotocol/types' import { Crypto } from '@kiltprotocol/utils' +import type { IDidDetails, IServiceDetails } from '@kiltprotocol/types' +import { Keyring } from '@polkadot/api' +import type { KeyringPair } from '@polkadot/keyring/types' +import { hexToU8a, u8aToHex } from '@polkadot/util' +import { LightDidDetails } from '../DidDetails' +import type { INewPublicKey } from '../types' import { IDidChainRecordJSON } from '../types' import { DefaultResolver } from './DefaultResolver' @@ -26,14 +34,16 @@ jest.mock('../Did.chain', () => { id: `${did}#auth`, type: 'ed25519', controller: did, - publicKeyHex: '0x123', + publicKeyHex: + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', includedAt: 200, }, { id: `${did}#x25519`, type: 'x25519', controller: did, - publicKeyHex: '0x25519', + publicKeyHex: + '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', includedAt: 250, }, ], @@ -55,48 +65,52 @@ jest.mock('../Did.chain', () => { }) const identifier = '4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' -const did = `did:kilt:${identifier}` +const fullDid = `did:kilt:${identifier}` it('resolves stuff', async () => { - await expect(DefaultResolver.resolveDoc(did)).resolves.toMatchObject({ - did, + await expect(DefaultResolver.resolveDoc(fullDid)).resolves.toMatchObject({ + did: fullDid, identifier, }) }) it('has the right keys', async () => { - const didRecord = await DefaultResolver.resolveDoc(did) - expect(didRecord?.getKeyIds()).toStrictEqual([`${did}#auth`, `${did}#x25519`]) + const didRecord = await DefaultResolver.resolveDoc(fullDid) + expect(didRecord?.getKeyIds()).toStrictEqual([ + `${fullDid}#auth`, + `${fullDid}#x25519`, + ]) expect(didRecord?.getKeyIds(KeyRelationship.authentication)).toStrictEqual([ - `${did}#auth`, + `${fullDid}#auth`, ]) expect(didRecord?.getKeys(KeyRelationship.keyAgreement)).toStrictEqual([ { - id: `${did}#x25519`, - controller: did, + id: `${fullDid}#x25519`, + controller: fullDid, includedAt: 250, type: 'x25519', - publicKeyHex: '0x25519', + publicKeyHex: + '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', }, ]) }) it('adds services when service resolver is present', async () => { const service = { - id: `${did}#messaging`, + id: `${fullDid}#messaging`, type: 'DidComm messaging', - serviceEndpoint: `example.com/didcomm/${did}`, + serviceEndpoint: `example.com/didcomm/${fullDid}`, } const servicesResolver: ServicesResolver = jest.fn(async () => [service]) await expect( - DefaultResolver.resolveDoc(did).then((didDetails) => + DefaultResolver.resolveDoc(fullDid).then((didDetails) => didDetails?.getServices('DidComm messaging') ) ).resolves.toMatchObject([]) await expect( - DefaultResolver.resolveDoc(did, { + DefaultResolver.resolveDoc(fullDid, { servicesResolver, }).then((didDetails) => didDetails?.getServices('DidComm messaging')) ).resolves.toMatchObject([service]) @@ -107,3 +121,176 @@ it('adds services when service resolver is present', async () => { 'application/json' ) }) + +const mnemonic = 'testMnemonic' + +describe('Light DID tests', () => { + const keyring: Keyring = new Keyring({ ss58Format: 38 }) + let keypair: KeyringPair + let publicAuthKey: INewPublicKey + let encryptionKey: INewPublicKey + let services: IServiceDetails[] + + it('Correctly resolves a light DID created with only an ed25519 authentication key', async () => { + keypair = keyring.addFromMnemonic(mnemonic, undefined, 'ed25519') + publicAuthKey = { + publicKey: keypair.publicKey, + type: 'ed25519', + } + const lightDID = new LightDidDetails({ + authenticationKey: publicAuthKey, + }) + const resolutionResult = (await DefaultResolver.resolve( + lightDID.did + )) as IDidDetails + const derivedAuthenticationPublicKey = resolutionResult.getKey( + `${lightDID.did}#authentication` + ) + expect(derivedAuthenticationPublicKey).toBeDefined() + expect(derivedAuthenticationPublicKey!.publicKeyHex).toEqual( + u8aToHex(publicAuthKey.publicKey) + ) + }) + + it('Correctly resolves a light DID created with only an sr25519 authentication key', async () => { + keypair = keyring.addFromMnemonic(mnemonic, undefined, 'sr25519') + publicAuthKey = { + publicKey: keypair.publicKey, + type: 'sr25519', + } + const lightDID = new LightDidDetails({ + authenticationKey: publicAuthKey, + }) + const resolutionResult = (await DefaultResolver.resolve( + lightDID.did + )) as IDidDetails + const derivedAuthenticationPublicKey = resolutionResult.getKey( + `${lightDID.did}#authentication` + ) + expect(derivedAuthenticationPublicKey).toBeDefined() + expect(derivedAuthenticationPublicKey!.publicKeyHex).toEqual( + u8aToHex(publicAuthKey.publicKey) + ) + }) + + it('Correctly resolves a light DID created with only an ecdsa authentication key', async () => { + keypair = keyring.addFromMnemonic(mnemonic, undefined, 'ecdsa') + publicAuthKey = { + publicKey: keypair.publicKey, + type: 'ecdsa', + } + const lightDID = new LightDidDetails({ + authenticationKey: publicAuthKey, + }) + const resolutionResult = (await DefaultResolver.resolve( + lightDID.did + )) as IDidDetails + const derivedAuthenticationPublicKey = resolutionResult.getKey( + `${lightDID.did}#authentication` + ) + expect(derivedAuthenticationPublicKey).toBeDefined() + expect(derivedAuthenticationPublicKey!.publicKeyHex).toEqual( + u8aToHex(publicAuthKey.publicKey) + ) + }) + + it('Correctly resolves a light DID created with only an authentication and an encryption key', async () => { + keypair = keyring.addFromMnemonic(mnemonic, undefined, 'ed25519') + publicAuthKey = { + publicKey: keypair.publicKey, + type: 'ecdsa', + } + encryptionKey = { + publicKey: hexToU8a( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + ), + type: 'x25519', + } + const lightDID = new LightDidDetails({ + authenticationKey: publicAuthKey, + encryptionKey, + }) + const resolutionResult = (await DefaultResolver.resolve( + lightDID.did + )) as IDidDetails + + const derivedAuthenticationPublicKey = resolutionResult.getKey( + `${lightDID.did}#authentication` + ) + expect(derivedAuthenticationPublicKey).toBeDefined() + expect(derivedAuthenticationPublicKey!.publicKeyHex).toEqual( + u8aToHex(publicAuthKey.publicKey) + ) + const derivedEncryptionPublicKey = resolutionResult.getKey( + `${lightDID.did}#encryption` + ) + expect(derivedEncryptionPublicKey).toBeDefined() + expect(derivedEncryptionPublicKey!.publicKeyHex).toEqual( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + ) + }) + + it('Correctly resolves a light DID created with an authentication, an encryption key, and service endpoints', async () => { + keypair = keyring.addFromMnemonic(mnemonic, undefined, 'ed25519') + publicAuthKey = { + publicKey: keypair.publicKey, + type: 'ecdsa', + } + encryptionKey = { + publicKey: hexToU8a( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + ), + type: 'x25519', + } + services = [ + { + id: `service1`, + type: 'messaging', + serviceEndpoint: 'example.com', + }, + { + id: `service2`, + type: 'telephone', + serviceEndpoint: '123344', + }, + ] + const lightDID = new LightDidDetails({ + authenticationKey: publicAuthKey, + encryptionKey, + services, + }) + const resolutionResult = (await DefaultResolver.resolve( + lightDID.did + )) as IDidDetails + + const derivedAuthenticationPublicKey = resolutionResult.getKey( + `${lightDID.did}#authentication` + ) + expect(derivedAuthenticationPublicKey).toBeDefined() + expect(derivedAuthenticationPublicKey!.publicKeyHex).toEqual( + u8aToHex(publicAuthKey.publicKey) + ) + const derivedEncryptionPublicKey = resolutionResult.getKey( + `${lightDID.did}#encryption` + ) + expect(derivedEncryptionPublicKey).toBeDefined() + expect(derivedEncryptionPublicKey!.publicKeyHex).toEqual( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + ) + const derivedServices = resolutionResult.getServices() + expect(derivedServices).toMatchInlineSnapshot(` + Array [ + Object { + "id": "${lightDID.did}#service1", + "serviceEndpoint": "example.com", + "type": "messaging", + }, + Object { + "id": "${lightDID.did}#service2", + "serviceEndpoint": "123344", + "type": "telephone", + }, + ] + `) + }) +}) diff --git a/packages/kilt-did/src/types.ts b/packages/kilt-did/src/types.ts index 80515aef1..cb48669ba 100644 --- a/packages/kilt-did/src/types.ts +++ b/packages/kilt-did/src/types.ts @@ -8,6 +8,7 @@ import type { IIdentity, IDidKeyDetails, + IDidDetails, KeyRelationship, } from '@kiltprotocol/types' import type { AnyNumber } from '@polkadot/types/types' @@ -31,6 +32,19 @@ import type { /* SDK TYPES */ +export type IDidParsingResult = { + did: IDidDetails['did'] + version: number + type: 'light' | 'full' + identifier: string + fragment?: string + encodedDetails?: string +} + +export type MapKeyToRelationship = Partial< + Record> +> + export interface INewPublicKey { publicKey: Uint8Array type: T diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 9867798fb..0de1115d8 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/messaging", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/packages/messaging/src/Message.utils.spec.ts b/packages/messaging/src/Message.utils.spec.ts index 1195c65ea..040d00382 100644 --- a/packages/messaging/src/Message.utils.spec.ts +++ b/packages/messaging/src/Message.utils.spec.ts @@ -977,15 +977,15 @@ describe('Messaging Utilities', () => { messageRequestTerms.receiver = 'did:kilt:thisisnotareceiveraddress' expect(() => MessageUtils.errorCheckMessage(messageRequestTerms) - ).toThrowErrorWithCode(SDKErrors.ERROR_INVALID_DID_PREFIX('')) + ).toThrowErrorWithCode(SDKErrors.ERROR_INVALID_DID_FORMAT('')) messageSubmitTerms.sender = 'this is not a sender did' expect(() => MessageUtils.errorCheckMessage(messageSubmitTerms) - ).toThrowErrorWithCode(SDKErrors.ERROR_INVALID_DID_PREFIX('')) + ).toThrowErrorWithCode(SDKErrors.ERROR_INVALID_DID_FORMAT('')) messageRejectTerms.sender = 'this is not a sender address' expect(() => MessageUtils.errorCheckMessage(messageRejectTerms) - ).toThrowErrorWithCode(SDKErrors.ERROR_INVALID_DID_PREFIX('')) + ).toThrowErrorWithCode(SDKErrors.ERROR_INVALID_DID_FORMAT('')) }) it('error check should throw errors on faulty bodies', () => { requestTermsBody.content.cTypeHash = 'this is not a ctype hash' diff --git a/packages/sdk-js/package.json b/packages/sdk-js/package.json index a34c977ea..56d86f95a 100644 --- a/packages/sdk-js/package.json +++ b/packages/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/sdk-js", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/packages/types/package.json b/packages/types/package.json index 3939a3ed3..fe3441f27 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/types", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/packages/types/src/DidDetails.ts b/packages/types/src/DidDetails.ts index 2387ae17b..d6491533d 100644 --- a/packages/types/src/DidDetails.ts +++ b/packages/types/src/DidDetails.ts @@ -7,16 +7,15 @@ import type { ApiPromise } from '@polkadot/api' import type { Metadata } from '@polkadot/types' -import type { BN } from '@polkadot/util' /** * DID keys are purpose-bound. Their role or purpose is indicated by the verification or key relationship type. */ export enum KeyRelationship { - authentication = 'Authentication', - capabilityDelegation = 'CapabilityDelegation', - assertionMethod = 'AssertionMethod', - keyAgreement = 'KeyAgreement', + authentication = 'authentication', + capabilityDelegation = 'capabilityDelegation', + assertionMethod = 'assertionMethod', + keyAgreement = 'keyAgreement', } /** @@ -63,9 +62,9 @@ export interface IDidKeyDetails { /** * A service record associated with a DID record. */ -export interface ServiceDetails { +export interface IServiceDetails { /** - * Service id, consisting of did:kilt:#. + * Service id. */ id: string /** @@ -118,13 +117,7 @@ export interface IDidDetails { * @param type A type string to filter out services with a specific type. * @returns An array of all or selected [[ServiceDetails]], depending on the `type` parameter. */ - getServices(type?: string): ServiceDetails[] - /** - * Gets the next nonce/transaction index required for DID authorized blockchain transactions. - * - * @returns A [[BN]] indicating the next transaction index. - */ - getNextTxIndex(): BN + getServices(type?: string): IServiceDetails[] } export type ApiOrMetadata = ApiPromise | Metadata diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index acce6ba34..9d63044fe 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { IDidDetails, IDidKeyDetails, ServiceDetails } from './DidDetails' +import type { IDidDetails, IDidKeyDetails, IServiceDetails } from './DidDetails' /** * A URL resolver for additional service endpoints data associated with a DID. It takes the hash of @@ -22,7 +22,7 @@ export type ServicesResolver = ( resourceHash: string, endpoints: string[], contentType: string -) => Promise +) => Promise export interface ResolverOpts { servicesResolver?: ServicesResolver @@ -45,7 +45,7 @@ export interface IDidResolver { resolve: ( didUri: string, opts?: ResolverOpts - ) => Promise + ) => Promise /** * Resolves a DID (or DID URI), returning the full contents of the DID document. * diff --git a/packages/utils/package.json b/packages/utils/package.json index 8a83054d6..05a7e3d4f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/utils", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/packages/utils/src/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index 2554e39d5..4d3ecc352 100644 --- a/packages/utils/src/SDKErrors.ts +++ b/packages/utils/src/SDKErrors.ts @@ -65,9 +65,10 @@ export enum ErrorCode { ERROR_PE_MISMATCH = 20015, ERROR_DID_IDENTIFIER_MISMATCH = 20016, ERROR_HIERARCHY_QUERY = 20017, - ERROR_INVALID_DID_PREFIX = 20018, + ERROR_INVALID_DID_FORMAT = 20018, ERROR_MESSAGE_BODY_MALFORMED = 20019, ERROR_NODE_QUERY = 20020, + ERROR_UNSUPPORTED_DID = 20021, // Data is invalid ERROR_ADDRESS_INVALID = 30001, @@ -83,6 +84,7 @@ export enum ErrorCode { ERROR_NESTED_CLAIM_UNVERIFIABLE = 30011, ERROR_INVALID_PROOF_FOR_STATEMENT = 30012, ERROR_CTYPE_PROPERTIES_NOT_MATCHING = 30013, + ERROR_UNSUPPORTED_KEY = 30014, // Compression / Decompressions ERROR_DECOMPRESSION_ARRAY = 40001, @@ -175,6 +177,15 @@ export const ERROR_CTYPE_PROPERTIES_NOT_MATCHING: () => SDKError = () => { ) } +export const ERROR_UNSUPPORTED_KEY: (keyType: string) => SDKError = ( + keyType: string +) => { + return new SDKError( + ErrorCode.ERROR_UNSUPPORTED_KEY, + `The provided key type "${keyType}" is currently not supported.` + ) +} + export const ERROR_CLAIM_HASH_NOT_PROVIDED: () => SDKError = () => { return new SDKError( ErrorCode.ERROR_CLAIM_HASH_NOT_PROVIDED, @@ -422,12 +433,20 @@ export const ERROR_NODE_QUERY: (nodeId: string) => SDKError = ( `Could not find node with id ${nodeId}` ) } -export const ERROR_INVALID_DID_PREFIX: (identifier: string) => SDKError = ( +export const ERROR_INVALID_DID_FORMAT: (identifier: string) => SDKError = ( identifier: string ) => { return new SDKError( - ErrorCode.ERROR_INVALID_DID_PREFIX, - `Not a KILT did: ${identifier}` + ErrorCode.ERROR_INVALID_DID_FORMAT, + `Not a valid KILT did: ${identifier}` + ) +} +export const ERROR_UNSUPPORTED_DID: (input: string) => SDKError = ( + input: string +) => { + return new SDKError( + ErrorCode.ERROR_UNSUPPORTED_DID, + `The DID ${input} is not supported.` ) } diff --git a/packages/vc-export/package.json b/packages/vc-export/package.json index 5be4e9d4f..52a52dfef 100644 --- a/packages/vc-export/package.json +++ b/packages/vc-export/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/vc-export", - "version": "0.22.2-2", + "version": "0.24.0-0", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/yarn.lock b/yarn.lock index ff4e3d7dd..550dd0424 100644 --- a/yarn.lock +++ b/yarn.lock @@ -865,10 +865,12 @@ __metadata: "@kiltprotocol/chain-helpers": "workspace:*" "@kiltprotocol/types": "workspace:*" "@kiltprotocol/utils": "workspace:*" + "@polkadot/api": ^5.4.1 "@polkadot/keyring": ^7.1.2 "@polkadot/types": ^5.4.1 "@polkadot/util": ^7.1.2 "@polkadot/util-crypto": ^7.1.2 + cbor: ^6.0.0 rimraf: ^3.0.2 typescript: ^4.2.2 languageName: unknown @@ -2363,6 +2365,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.1": + version: 9.0.1 + resolution: "bignumber.js@npm:9.0.1" + checksum: 605e9639c413f344c37b23e919254f60a5017cc5ccd925e2f8fb79b36aa3d54f356df9c726f38465263236455f685d60dcf38dbe32cb0b7e4d2a32c94b035476 + languageName: node + linkType: hard + "blakejs@npm:^1.1.1": version: 1.1.1 resolution: "blakejs@npm:1.1.1" @@ -2679,6 +2688,16 @@ __metadata: languageName: node linkType: hard +"cbor@npm:^6.0.0": + version: 6.0.1 + resolution: "cbor@npm:6.0.1" + dependencies: + bignumber.js: ^9.0.1 + nofilter: ^1.0.4 + checksum: 57bad020415bedaa7d7b1c04c94592696f183c8fbf3675baac258754da373495fe03078afacf95c352403631154031cf383d1e5899b77105d5bd219d400050d6 + languageName: node + linkType: hard + "chalk@npm:4.1.0, chalk@npm:^4.0.0": version: 4.1.0 resolution: "chalk@npm:4.1.0" @@ -6707,6 +6726,13 @@ fsevents@^2.1.2: languageName: node linkType: hard +"nofilter@npm:^1.0.4": + version: 1.0.4 + resolution: "nofilter@npm:1.0.4" + checksum: af46b9255190250595702f75623d3de4bc5e07822d16d2b3b2e47dc892825273efedc326710e884689e4a74bfc9863c61ca8d6b4115c7bcac40a4e787ae5a8c9 + languageName: node + linkType: hard + "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0"