diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index 91f8c5960..10b1a53ac 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -17,57 +17,71 @@ import type { KiltSupportDeposit, } from '@kiltprotocol/augment-api' -import type { +import { BN, CryptoCallbacksV2, Deposit, DidDocumentV2, + encryptionKeyTypesMap, KiltAddress, + NewDidEncryptionKey, + NewDidVerificationKey, SubmittableExtrinsic, + verificationKeyTypesMap, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { - EncryptionKeyType, - NewServiceEndpoint, - NewVerificationMethod, - VerificationKeyType, + DidEncryptionKeyType, + NewService, + DidVerificationKeyType, verificationKeyTypes, } from './DidDetailsv2/DidDetailsV2.js' import { - decodeMultikeyVerificationMethod, - getAddressByVerificationMethod, + multibaseKeyToDidKey, + keypairToMultibaseKey, + getAddressFromVerificationMethod, getFullDidUri, parse, } from './Did2.utils.js' export type ChainDidIdentifier = KiltAddress -export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails export type EncodedVerificationKey = | { sr25519: Uint8Array } | { ed25519: Uint8Array } | { ecdsa: Uint8Array } - export type EncodedEncryptionKey = { x25519: Uint8Array } -export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey +export type EncodedDidKey = EncodedVerificationKey | EncodedEncryptionKey export type EncodedSignature = EncodedVerificationKey +/** + * @param did + */ export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { return parse(did).address } +/** + * @param id + */ export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { return id.replace(/^#/, '') } +/** + * @param encoded + */ export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) } +/** + * @param deposit + */ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { return { owner: Crypto.encodeAddress(deposit.owner, ss58Format), @@ -82,19 +96,17 @@ export type ChainDidBaseKey = { type: string } export type ChainDidVerificationKey = ChainDidBaseKey & { - type: VerificationKeyType + type: DidVerificationKeyType } export type ChainDidEncryptionKey = ChainDidBaseKey & { - type: EncryptionKeyType + type: DidEncryptionKeyType } export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey - export type ChainDidService = { id: string serviceTypes: string[] urls: string[] } - export type ChainDidDetails = { authentication: [ChainDidVerificationKey] assertionMethod?: [ChainDidVerificationKey] @@ -107,9 +119,9 @@ export type ChainDidDetails = { deposit: Deposit } -function didPublicKeyDetailsFromChain( +function publicKeyFromChain( keyId: Hash, - keyDetails: ChainDidPublicKeyDetails + keyDetails: DidDidDetailsDidPublicKeyDetails ): ChainDidKey { const key = keyDetails.key.isPublicEncryptionKey ? keyDetails.key.asPublicEncryptionKey @@ -123,6 +135,9 @@ function didPublicKeyDetailsFromChain( } } +/** + * @param encoded + */ export function documentFromChain( encoded: Option ): ChainDidDetails { @@ -137,9 +152,7 @@ export function documentFromChain( } = encoded.unwrap() const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => - didPublicKeyDetailsFromChain(keyId, keyDetails) - ) + .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) .reduce((res, key) => { res[fragmentIdToChain(key.id)] = key return res @@ -175,17 +188,22 @@ export function documentFromChain( return didRecord } -export function verificationMethodToChain( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > -): EncodedKey { - const { keyType, publicKey } = - decodeMultikeyVerificationMethod(verificationMethod) - return { - [keyType]: publicKey, - } as EncodedKey +export function publicKeyToChain( + key: NewDidVerificationKey +): EncodedVerificationKey +export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey + +/** + * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. + * + * @param key Object describing data associated with a public key. + * @returns Data restructured to allow SCALE encoding by polkadot api. + */ +export function publicKeyToChain( + key: NewDidVerificationKey | NewDidEncryptionKey +): EncodedDidKey { + // TypeScript can't infer type here, so we have to add a type assertion. + return { [key.type]: key.publicKey } as EncodedDidKey } function isUri(str: string): boolean { @@ -207,9 +225,12 @@ function isUriFragment(str: string): boolean { } } -export function validateNewService(endpoint: NewServiceEndpoint): void { +/** + * @param endpoint + */ +export function validateNewService(endpoint: NewService): void { const { id, serviceEndpoint } = endpoint - if (id.startsWith('did:kilt')) { + if ((id as string).startsWith('did:kilt')) { throw new SDKErrors.DidError( `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` ) @@ -228,7 +249,10 @@ export function validateNewService(endpoint: NewServiceEndpoint): void { }) } -export function serviceToChain(service: NewServiceEndpoint): ChainDidService { +/** + * @param service + */ +export function serviceToChain(service: NewService): ChainDidService { validateNewService(service) const { id, type, serviceEndpoint } = service return { @@ -238,9 +262,12 @@ export function serviceToChain(service: NewServiceEndpoint): ChainDidService { } } +/** + * @param encoded + */ export function serviceFromChain( encoded: Option -): NewServiceEndpoint { +): NewService { const { id, serviceTypes, urls } = encoded.unwrap() return { id: `#${id.toUtf8()}`, @@ -258,19 +285,24 @@ export type AuthorizeCallInput = { } interface GetStoreTxInput { - authentication: [NewVerificationMethod] - assertionMethod?: [NewVerificationMethod] - capabilityDelegation?: [NewVerificationMethod] - keyAgreement?: NewVerificationMethod[] + authentication: [NewDidVerificationKey] + assertionMethod?: [NewDidVerificationKey] + capabilityDelegation?: [NewDidVerificationKey] + keyAgreement?: NewDidEncryptionKey[] - service?: NewServiceEndpoint[] + service?: NewService[] } export type GetStoreTxSignCallback = ( signData: Omit ) => Promise -export async function getStoreTx( +/** + * @param input + * @param submitter + * @param sign + */ +export async function getStoreTxFromInput( input: GetStoreTxInput, submitter: KiltAddress, sign: GetStoreTxSignCallback @@ -292,14 +324,14 @@ export async function getStoreTx( } // For now, it only takes the first attestation key, if present. - if (assertionMethod && assertionMethod.length > 1) { + if (assertionMethod !== undefined && assertionMethod.length > 1) { throw new SDKErrors.DidError( `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` ) } // For now, it only takes the first delegation key, if present. - if (capabilityDelegation && capabilityDelegation.length > 1) { + if (capabilityDelegation !== undefined && capabilityDelegation.length > 1) { throw new SDKErrors.DidError( `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` ) @@ -321,19 +353,21 @@ export async function getStoreTx( } const [authenticationKey] = authentication - const did = getAddressByVerificationMethod(authenticationKey) + const did = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authenticationKey), + }) const newAttestationKey = assertionMethod && assertionMethod.length > 0 && - getAddressByVerificationMethod(assertionMethod[0]) + publicKeyToChain(assertionMethod[0]) const newDelegationKey = capabilityDelegation && capabilityDelegation.length > 0 && - getAddressByVerificationMethod(capabilityDelegation[0]) + publicKeyToChain(capabilityDelegation[0]) - const newKeyAgreementKeys = keyAgreement.map(getAddressByVerificationMethod) + const newKeyAgreementKeys = keyAgreement.map(publicKeyToChain) const newServiceDetails = service.map(serviceToChain) const apiInput = { @@ -349,22 +383,157 @@ export async function getStoreTx( .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) .toU8a() - const { signature, verificationMethod } = await sign({ + const { signature, verificationMethodPublicKey } = await sign({ data: encoded, verificationMethodRelationship: 'authentication', }) - const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) + const { keyType } = multibaseKeyToDidKey(verificationMethodPublicKey) const encodedSignature = { [keyType]: signature, } as EncodedSignature return api.tx.did.create(encoded, encodedSignature) } +/** + * @param input + * @param submitter + * @param sign + */ +export async function getStoreTxFromDidDocument( + input: DidDocumentV2.DidDocument, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const { + authentication, + assertionMethod, + keyAgreement, + capabilityDelegation, + service, + verificationMethod, + } = input + + const authKey = (() => { + const authVerificationMethod = verificationMethod.find( + (vm) => vm.id === authentication[0] + ) + if (authVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + authVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const keyAgreementKey = (() => { + if (keyAgreement === undefined) { + return undefined + } + const keyAgreementVerificationMethod = verificationMethod.find( + (vm) => vm.id === keyAgreement?.[0] + ) + if (keyAgreementVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + keyAgreementVerificationMethod.publicKeyMultibase + ) + if (encryptionKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidEncryptionKey + })() + + const assertionMethodKey = (() => { + if (assertionMethod === undefined) { + return undefined + } + const assertionMethodVerificationMethod = verificationMethod.find( + (vm) => vm.id === assertionMethod?.[0] + ) + if (assertionMethodVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + assertionMethodVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const capabilityDelegationKey = (() => { + if (capabilityDelegation === undefined) { + return undefined + } + const capabilityDelegationVerificationMethod = verificationMethod.find( + (vm) => vm.id === capabilityDelegation?.[0] + ) + if (capabilityDelegationVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + capabilityDelegationVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const storeTxInput: GetStoreTxInput = { + authentication: [authKey], + assertionMethod: assertionMethodKey ? [assertionMethodKey] : undefined, + capabilityDelegation: capabilityDelegationKey + ? [capabilityDelegationKey] + : undefined, + keyAgreement: keyAgreementKey ? [keyAgreementKey] : undefined, + service, + } + + return getStoreTxFromInput(storeTxInput, submitter, sign) +} + export interface SigningOptions { sign: CryptoCallbacksV2.SignExtrinsicCallback verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship } +/** + * @param root0 + * @param root0.did + * @param root0.verificationMethodRelationship + * @param root0.sign + * @param root0.call + * @param root0.txCounter + * @param root0.submitter + * @param root0.blockNumber + */ export async function generateDidAuthenticatedTx({ did, verificationMethodRelationship, @@ -386,23 +555,28 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const { verificationMethod, signature } = await sign({ + const { signature, verificationMethodPublicKey } = await sign({ data: signableCall.toU8a(), verificationMethodRelationship, did, }) - const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) + const { keyType } = multibaseKeyToDidKey(verificationMethodPublicKey) const encodedSignature = { [keyType]: signature, } as EncodedSignature return api.tx.did.submitDidCall(signableCall, encodedSignature) } +/** + * @param root0 + * @param root0.publicKeyMultibase + * @param signature + */ export function didSignatureToChain( - signature: Uint8Array, - verificationMethod: DidDocumentV2.VerificationMethod + { publicKeyMultibase }: DidDocumentV2.VerificationMethod, + signature: Uint8Array ): EncodedSignature { - const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) + const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) if (!verificationKeyTypes.includes(keyType)) { throw new SDKErrors.DidError( `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index 9c5dda845..fc84c212a 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -10,7 +10,11 @@ import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' -import type { DidDocumentV2, KiltAddress } from '@kiltprotocol/types' +import type { + DidDocumentV2, + KeyringPair, + KiltAddress, +} from '@kiltprotocol/types' import type { DidKeyType } from './DidDetailsv2/DidDetailsV2.js' @@ -121,18 +125,13 @@ const multicodecReversePrefixes: Record = { /** * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. * - * @param verificationMethod The verification method. + * @param publicKeyMultibase The verification method's public key multibase. * @returns The decoded public key and [DidKeyType]. */ -export function decodeMultikeyVerificationMethod( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > +export function multibaseKeyToDidKey( + publicKeyMultibase: DidDocumentV2.VerificationMethod['publicKeyMultibase'] ): DecodedVerificationMethod { - const decodedMulticodecPublicKey = multibaseDecode( - verificationMethod.publicKeyMultibase - ) + const decodedMulticodecPublicKey = multibaseDecode(publicKeyMultibase) const [keyTypeFlag, publicKey] = [ decodedMulticodecPublicKey.subarray(0, 1)[0], decodedMulticodecPublicKey.subarray(1), @@ -148,7 +147,32 @@ export function decodeMultikeyVerificationMethod( throw new Error('Invalid encoding of the verification method.') } -export function encodeVerificationMethodToMultiKey( +export function keypairToMultibaseKey({ + type, + publicKey, +}: Pick< + KeyringPair, + 'publicKey' | 'type' +>): DidDocumentV2.VerificationMethod['publicKeyMultibase'] { + const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] + if (multiCodecPublicKeyPrefix === undefined) { + // TODO: Proper error + throw new Error(`Invalid key type provided: ${type}.`) + } + const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] + if (publicKey.length !== expectedPublicKeySize) { + // TODO: Proper error + throw new Error( + `Provided public key does not match the expected length: ${expectedPublicKeySize}.` + ) + } + const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] + return Buffer.from( + multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) + ).toString() as `z${string}` +} + +export function didKeyToVerificationMethod( controller: DidDocumentV2.VerificationMethod['controller'], id: DidDocumentV2.VerificationMethod['id'], { keyType, publicKey }: DecodedVerificationMethod @@ -207,7 +231,7 @@ export function validateUri( const { address, fragment } = parse(input as DidDocumentV2.DidUri) if ( - fragment && + fragment !== undefined && (expectType === 'Did' || // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. (typeof expectType === 'boolean' && expectType === false)) @@ -217,7 +241,7 @@ export function validateUri( ) } - if (!fragment && expectType === 'ResourceUri') { + if (fragment === undefined && expectType === 'ResourceUri') { throw new SDKErrors.DidError( 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' ) @@ -227,14 +251,10 @@ export function validateUri( } // TODO: Fix JSDoc -export function getAddressByVerificationMethod( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > -): KiltAddress { - const { keyType: type, publicKey } = - decodeMultikeyVerificationMethod(verificationMethod) +export function getAddressFromVerificationMethod({ + publicKeyMultibase, +}: Pick): KiltAddress { + const { keyType: type, publicKey } = multibaseKeyToDidKey(publicKeyMultibase) if (type === 'ed25519' || type === 'sr25519') { return encodeAddress(publicKey, ss58Format) } @@ -275,6 +295,6 @@ export function getFullDidUriFromVerificationMethod( 'publicKeyMultibase' > ): DidDocumentV2.DidUri { - const address = getAddressByVerificationMethod(verificationMethod) + const address = getAddressFromVerificationMethod(verificationMethod) return getFullDidUri(address) } diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 30a752f6b..98deecc07 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -25,6 +25,7 @@ import { SDKErrors, ss58Format, cbor } from '@kiltprotocol/utils' import { getAddressByKey, parse } from '../Did.utils.js' import { resourceIdToChain, validateService } from '../Did.chain.js' +import { NewService } from '../DidDetailsv2/DidDetailsV2.js' const authenticationKeyId = '#authentication' const encryptionKeyId = '#encryption' @@ -65,7 +66,7 @@ export type CreateDocumentInput = { * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ - service?: DidServiceEndpoint[] + service?: NewService[] } function validateCreateDocumentInput({ diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 3b4513178..333a7853e 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -5,43 +5,47 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' +import type { DidDocumentV2 } from '@kiltprotocol/types' /** * Possible types for a DID verification key. */ const verificationKeyTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const export const verificationKeyTypes = verificationKeyTypesC as unknown as string[] -export type VerificationKeyType = typeof verificationKeyTypesC[number] +export type DidVerificationKeyType = typeof verificationKeyTypesC[number] // `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 -/** - * Currently, a light DID does not support the use of an ECDSA key as its authentication key. - */ -export type LightDidSupportedVerificationKeyType = Extract< - VerificationKeyType, - 'ed25519' | 'sr25519' -> - -/** - * Subset of key relationships which pertain to key agreement/encryption keys. - */ -export type EncryptionKeyRelationship = Extract - /** * Possible types for a DID encryption key. */ const encryptionKeyTypesC = ['x25519'] as const export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] -export type EncryptionKeyType = typeof encryptionKeyTypesC[number] +export type DidEncryptionKeyType = typeof encryptionKeyTypesC[number] -export type DidKeyType = VerificationKeyType | EncryptionKeyType +export type DidKeyType = DidVerificationKeyType | DidEncryptionKeyType export type NewVerificationMethod = Omit< DidDocumentV2.VerificationMethod, 'controller' -> & { - id: DidDocumentV2.UriFragment +> +export type NewService = DidDocumentV2.Service + +/** + * Type of a new key material to add under a DID. + */ +export type BaseNewDidKey = { + publicKey: Uint8Array + type: string +} + +/** + * Type of a new verification key to add under a DID. + */ +export type NewDidVerificationKey = BaseNewDidKey & { + type: DidVerificationKeyType } -export type NewServiceEndpoint = DidDocumentV2.Service +/** + * Type of a new encryption key to add under a DID. + */ +export type NewDidEncryptionKey = BaseNewDidKey & { type: DidEncryptionKeyType } diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts index 4b6502ebc..d998be6c1 100644 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts @@ -165,7 +165,7 @@ function groupExtrinsicsByKeyRelationship( const [first, ...rest] = extrinsics.map((extrinsic) => { const verificationMethodRelationship = getVerificationMethodRelationshipForTx(extrinsic) - if (!verificationMethodRelationship) { + if (verificationMethodRelationship === undefined) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index a7f6820ca..140e3a220 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -5,7 +5,12 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidDocumentV2, encryptionKeyTypes } from '@kiltprotocol/types' +import { + DidDocumentV2, + encryptionKeyTypes, + NewDidEncryptionKey, + NewDidVerificationKey, +} from '@kiltprotocol/types' import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { base58Decode, @@ -13,25 +18,28 @@ import { decodeAddress, } from '@polkadot/util-crypto' import { - decodeMultikeyVerificationMethod, - encodeVerificationMethodToMultiKey, - getAddressByVerificationMethod, + keypairToMultibaseKey, + didKeyToVerificationMethod, + getAddressFromVerificationMethod, parse, } from '../Did2.utils.js' import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' -import type { - NewVerificationMethod, - NewServiceEndpoint, - DidKeyType, -} from './DidDetailsV2.js' +import type { NewService, DidVerificationKeyType } from './DidDetailsV2.js' /** * Currently, a light DID does not support the use of an ECDSA key as its authentication key. */ export type LightDidSupportedVerificationKeyType = Extract< - DidKeyType, + DidVerificationKeyType, 'ed25519' | 'sr25519' > +/** + * A new public key specified when creating a new light DID. + */ +export type NewLightDidVerificationKey = NewDidVerificationKey & { + type: LightDidSupportedVerificationKeyType +} + type LightDidEncoding = '00' | '01' const authenticationKeyId = '#authentication' @@ -55,61 +63,54 @@ const lightDidEncodingToVerificationKeyType: Record< export type CreateDocumentInput = { /** - * The DID authentication key. This is mandatory and will be used as the first authentication key + * The DID authentication verification method. This is mandatory and will be used as the first authentication verification method * of the full DID upon migration. */ - authentication: [NewVerificationMethod] + authentication: [NewDidVerificationKey] /** - * The optional DID encryption key. If present, it will be used as the first key agreement key + * The optional DID encryption verification method. If present, it will be used as the first key agreement verification method * of the full DID upon migration. */ - keyAgreement?: [NewVerificationMethod] + keyAgreement?: [NewDidEncryptionKey] /** * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ - service?: NewServiceEndpoint[] + service?: NewService[] } function validateCreateDocumentInput({ authentication, keyAgreement, - service: services, + service, }: CreateDocumentInput): void { // Check authentication key type - const { keyType: authenticationKeyType } = decodeMultikeyVerificationMethod( - authentication[0] - ) - const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[ - authenticationKeyType - ] as LightDidEncoding + const authenticationKeyTypeEncoding = + verificationKeyTypeToLightDidEncoding[authentication[0].type] - if (!authenticationKeyTypeEncoding) { + if (authenticationKeyTypeEncoding !== undefined) { throw new SDKErrors.UnsupportedKeyError(authentication[0].type) } - - if (keyAgreement?.[0] !== undefined) { - const { keyType: keyAgreementKeyType } = decodeMultikeyVerificationMethod( - keyAgreement[0] + if ( + keyAgreement?.[0].type && + !encryptionKeyTypes.includes(keyAgreement[0].type) + ) { + throw new SDKErrors.DidError( + `Encryption key type "${keyAgreement[0].type}" is not supported` ) - if (!encryptionKeyTypes.includes(keyAgreementKeyType)) { - throw new SDKErrors.DidError( - `Encryption key type "${keyAgreementKeyType}" is not supported` - ) - } } // Checks that for all service IDs have regular strings as their ID and not a full DID. // Plus, we forbid a service ID to be `authentication` or `encryption` as that would create confusion // when upgrading to a full DID. - services?.forEach((service) => { + service?.forEach((s) => { // A service ID cannot have a reserved ID that is used for key IDs. - if (service.id === '#authentication' || service.id === '#encryption') { + if (s.id === '#authentication' || s.id === '#encryption') { throw new SDKErrors.DidError( - `Cannot specify a service ID with the name "${service.id}" as it is a reserved keyword` + `Cannot specify a service ID with the name "${s.id}" as it is a reserved keyword` ) } - validateNewService(service) + validateNewService(s) }) } @@ -117,9 +118,9 @@ const KEY_AGREEMENT_MAP_KEY = 'e' const SERVICES_MAP_KEY = 's' interface SerializableStructure { - [KEY_AGREEMENT_MAP_KEY]?: NewVerificationMethod + [KEY_AGREEMENT_MAP_KEY]?: NewDidEncryptionKey [SERVICES_MAP_KEY]?: Array< - Partial> & { + Partial> & { id: string } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their service endpoints. > @@ -197,11 +198,11 @@ export function createLightDidDocument({ service, }) // Validity is checked in validateCreateDocumentInput - const { keyType: authenticationKeyType, publicKey: authenticationPublicKey } = - decodeMultikeyVerificationMethod(authentication[0]) const authenticationKeyTypeEncoding = - verificationKeyTypeToLightDidEncoding[authenticationKeyType] - const address = getAddressByVerificationMethod(authentication[0]) + verificationKeyTypeToLightDidEncoding[authentication[0].type] + const address = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authentication[0]), + }) const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' const uri = @@ -211,22 +212,20 @@ export function createLightDidDocument({ id: uri, authentication: [authenticationKeyId], verificationMethod: [ - encodeVerificationMethodToMultiKey(uri, authenticationKeyId, { - keyType: authenticationKeyType, - publicKey: authenticationPublicKey, + didKeyToVerificationMethod(uri, authenticationKeyId, { + keyType: authentication[0].type, + publicKey: authentication[0].publicKey, }), ], service, } if (keyAgreement !== undefined) { - const { keyType: keyAgreementKeyType, publicKey: keyAgreementPublicKey } = - decodeMultikeyVerificationMethod(keyAgreement[0]) did.keyAgreement = [encryptionKeyId] did.verificationMethod.push( - encodeVerificationMethodToMultiKey(uri, encryptionKeyId, { - keyType: keyAgreementKeyType, - publicKey: keyAgreementPublicKey, + didKeyToVerificationMethod(uri, encryptionKeyId, { + keyType: keyAgreement[0].type, + publicKey: keyAgreement[0].publicKey, }) ) } @@ -252,7 +251,7 @@ export function parseDocumentFromLightDid( `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` ) } - if (fragment && failIfFragmentPresent) { + if (fragment !== undefined && failIfFragmentPresent) { throw new SDKErrors.DidError( `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` ) @@ -267,11 +266,8 @@ export function parseDocumentFromLightDid( ) } const publicKey = decodeAddress(address, false, ss58Format) - const authentication: [NewVerificationMethod] = [ - encodeVerificationMethodToMultiKey(uri, authenticationKeyId, { - keyType, - publicKey, - }), + const authentication: [NewLightDidVerificationKey] = [ + { publicKey, type: keyType }, ] if (!encodedDetails) { return createLightDidDocument({ authentication }) diff --git a/packages/did/src/DidDetailsv2/index.ts b/packages/did/src/DidDetailsv2/index.ts new file mode 100644 index 000000000..9977d6fdb --- /dev/null +++ b/packages/did/src/DidDetailsv2/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +export * from './DidDetailsV2.js' +export * from './LightDidDetailsV2.js' +export * from './FullDidDetailsV2.js' diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index fea6b0f1c..9d49bfdc6 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -11,7 +11,7 @@ import { cbor } from '@kiltprotocol/utils' import { linkedInfoFromChain } from '../Did.rpc' import { toChain } from '../Did2.chain.js' import { - encodeVerificationMethodToMultiKey, + didKeyToVerificationMethod, getFullDidUri, parse, validateUri, @@ -53,7 +53,7 @@ async function resolveInternal( id: did, authentication: [document.authentication[0].id], verificationMethod: [ - encodeVerificationMethodToMultiKey(did, document.authentication[0].id, { + didKeyToVerificationMethod(did, document.authentication[0].id, { publicKey: document.authentication[0].publicKey, keyType: document.authentication[0].type, }), @@ -64,7 +64,7 @@ async function resolveInternal( newDidDocument.assertionMethod = document.assertionMethod.map((k) => k.id) newDidDocument.verificationMethod.push( ...document.assertionMethod.map((k) => - encodeVerificationMethodToMultiKey(did, k.id, { + didKeyToVerificationMethod(did, k.id, { keyType: k.type, publicKey: k.publicKey, }) @@ -75,7 +75,7 @@ async function resolveInternal( newDidDocument.assertionMethod = document.keyAgreement.map((k) => k.id) newDidDocument.verificationMethod.push( ...document.keyAgreement.map((k) => - encodeVerificationMethodToMultiKey(did, k.id, { + didKeyToVerificationMethod(did, k.id, { keyType: k.type, publicKey: k.publicKey, }) @@ -88,7 +88,7 @@ async function resolveInternal( ) newDidDocument.verificationMethod.push( ...document.capabilityDelegation.map((k) => - encodeVerificationMethodToMultiKey(did, k.id, { + didKeyToVerificationMethod(did, k.id, { keyType: k.type, publicKey: k.publicKey, }) diff --git a/packages/did/src/index.ts b/packages/did/src/index.ts index 38320500d..4901cdb29 100644 --- a/packages/did/src/index.ts +++ b/packages/did/src/index.ts @@ -17,3 +17,10 @@ export * from './Did.rpc.js' export * from './Did.utils.js' export * from './Did.signature.js' export * from './DidLinks/AccountLinks.chain.js' + +export * as DidDetailsV2 from './DidDetailsv2/index.js' +export * as DidResolverV2 from './DidResolver/DidResolverV2.js' +export * as DidChainV2 from './Did2.chain.js' +export * as DidUtilsV2 from './Did2.utils.js' +export * as DidSignatureV2 from './Did2.signature.js' +export * as DidLinksV2 from './DidLinks/AccountLinks2.chain.js' diff --git a/packages/types/src/CryptoCallbacksV2.ts b/packages/types/src/CryptoCallbacksV2.ts index c56f74e44..4771cabef 100644 --- a/packages/types/src/CryptoCallbacksV2.ts +++ b/packages/types/src/CryptoCallbacksV2.ts @@ -39,7 +39,7 @@ export interface SignResponseData { /** * The did key uri used for signing. */ - verificationMethod: DidDocumentV2.VerificationMethod + verificationMethodPublicKey: DidDocumentV2.VerificationMethod['publicKeyMultibase'] } /** diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts new file mode 100644 index 000000000..3354e8605 --- /dev/null +++ b/tests/testUtils/TestUtils2.ts @@ -0,0 +1,400 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' + +import type { + CryptoCallbacksV2, + DidDocument, + DidDocumentV2, + DidKey, + DidServiceEndpoint, + DidVerificationKey, + KeyRelationship, + KeyringPair, + KiltEncryptionKeypair, + KiltKeyringPair, + LightDidSupportedVerificationKeyType, + NewLightDidVerificationKey, +} from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import * as Did from '@kiltprotocol/did' + +import { Blockchain } from '@kiltprotocol/chain-helpers' +import { ConfigService } from '@kiltprotocol/config' +import { getStoreTxFromDidDocument } from 'did/src/Did2.chain' + +export type EncryptionKeyToolCallback = ( + didDocument: DidDocumentV2.DidDocument +) => CryptoCallbacksV2.EncryptCallback + +/** + * Generates a callback that can be used for encryption. + * + * @param secretKey The options parameter. + * @param secretKey.secretKey The key to use for encryption. + * @returns The callback. + */ +export function makeEncryptCallback({ + secretKey, +}: KiltEncryptionKeypair): EncryptionKeyToolCallback { + return (didDocument) => { + return async function encryptCallback({ data, peerPublicKey }) { + const keyId = didDocument.keyAgreement?.[0] + if (keyId === undefined) { + throw new Error(`Encryption key not found in did "${didDocument.id}"`) + } + const verificationMethod = didDocument.verificationMethod.find( + (v) => v.id === keyId + ) as DidDocumentV2.VerificationMethod + const { box, nonce } = Crypto.encryptAsymmetric( + data, + peerPublicKey, + secretKey + ) + return { + nonce, + data: box, + verificationMethod, + } + } + } +} + +/** + * Generates a callback that can be used for decryption. + * + * @param secretKey The options parameter. + * @param secretKey.secretKey The key to use for decryption. + * @returns The callback. + */ +export function makeDecryptCallback({ + secretKey, +}: KiltEncryptionKeypair): CryptoCallbacksV2.DecryptCallback { + return async function decryptCallback({ data, nonce, peerPublicKey }) { + const decrypted = Crypto.decryptAsymmetric( + { box: data, nonce }, + peerPublicKey, + secretKey + ) + if (decrypted === false) throw new Error('Decryption failed') + return { data: decrypted } + } +} + +export interface EncryptionKeyTool { + keyAgreement: [KiltEncryptionKeypair] + encrypt: EncryptionKeyToolCallback + decrypt: CryptoCallbacksV2.DecryptCallback +} + +/** + * Generates a keypair suitable for encryption. + * + * @param seed {string} Input to generate the keypair from. + * @returns Object with secret and public key and the key type. + */ +export function makeEncryptionKeyTool(seed: string): EncryptionKeyTool { + const keypair = Crypto.makeEncryptionKeypairFromSeed(blake2AsU8a(seed, 256)) + + const encrypt = makeEncryptCallback(keypair) + const decrypt = makeDecryptCallback(keypair) + + return { + keyAgreement: [keypair], + encrypt, + decrypt, + } +} + +export type KeyToolSignCallback = ( + didDocument: DidDocumentV2.DidDocument +) => CryptoCallbacksV2.SignCallback + +/** + * Generates a callback that can be used for signing. + * + * @param keypair The keypair to use for signing. + * @returns The callback. + */ +export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { + return (didDocument) => + async function sign({ data, verificationMethodRelationship }) { + const keyId = didDocument[verificationMethodRelationship]?.[0] + if (keyId === undefined) { + throw new Error( + `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` + ) + } + const verificationMethod = didDocument.verificationMethod.find( + (vm) => vm.id === keyId + ) + if (verificationMethod === undefined) { + throw new Error( + `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` + ) + } + const signature = keypair.sign(data, { withType: false }) + + return { + signature, + verificationMethodPublicKey: verificationMethod.publicKeyMultibase, + } + } +} + +type StoreDidCallback = Parameters< + typeof Did.DidChainV2.getStoreTxFromInput +>['2'] + +/** + * Generates a callback that can be used for signing. + * + * @param keypair The keypair to use for signing. + * @returns The callback. + */ +export function makeStoreDidCallback( + keypair: KiltKeyringPair +): StoreDidCallback { + return async function sign({ data }) { + const signature = keypair.sign(data, { withType: false }) + return { + signature, + verificationMethodPublicKey: + Did.DidUtilsV2.keypairToMultibaseKey(keypair), + } + } +} + +export interface KeyTool { + keypair: KiltKeyringPair + getSignCallback: KeyToolSignCallback + storeDidCallback: StoreDidCallback + authentication: [NewLightDidVerificationKey] +} + +/** + * Generates a keypair usable for signing and a few related values. + * + * @param type The type to use for the keypair. + * @returns The keypair, matching sign callback, a key usable as DID authentication key. + */ +export function makeSigningKeyTool( + type: KiltKeyringPair['type'] = 'sr25519' +): KeyTool { + const keypair = Crypto.makeKeypairFromSeed(undefined, type) + const getSignCallback = makeSignCallback(keypair) + const storeDidCallback = makeStoreDidCallback(keypair) + + return { + keypair, + getSignCallback, + storeDidCallback, + authentication: [keypair as NewLightDidVerificationKey], + } +} + +/** + * Given a keypair, creates a light DID with an authentication and an encryption key. + * + * @param keypair KeyringPair instance for authentication key. + * @returns DidDocument. + */ +export async function createMinimalLightDidFromKeypair( + keypair: KeyringPair +): Promise { + const type = keypair.type as LightDidSupportedVerificationKeyType + return Did.DidDetailsV2.createLightDidDocument({ + authentication: [{ publicKey: keypair.publicKey, type }], + keyAgreement: makeEncryptionKeyTool(`${keypair.publicKey}//enc`) + .keyAgreement, + }) +} + +// Mock function to generate a key ID without having to rely on a real chain metadata. +export function computeKeyId(key: DidKey['publicKey']): DidKey['id'] { + return `#${blake2AsHex(key, 256)}` +} + +function makeDidKeyFromKeypair({ + publicKey, + type, +}: KiltKeyringPair): DidVerificationKey { + return { + id: computeKeyId(publicKey), + publicKey, + type, + } +} + +/** + * Creates [[DidDocument]] for local use, e.g., in testing. Will not work on-chain because key IDs are generated ad-hoc. + * + * @param keypair The KeyringPair for authentication key, other keys derived from it. + * @param generationOptions The additional options for generation. + * @param generationOptions.keyRelationships The set of key relationships to indicate which keys must be added to the DID. + * @param generationOptions.endpoints The set of service endpoints that must be added to the DID. + * + * @returns A promise resolving to a [[DidDocument]] object. The resulting object is NOT stored on chain. + */ +export async function createLocalDemoFullDidFromKeypair( + keypair: KiltKeyringPair, + { + keyRelationships = new Set([ + 'assertionMethod', + 'capabilityDelegation', + 'keyAgreement', + ]), + endpoints = [], + }: { + keyRelationships?: Set> + endpoints?: DidServiceEndpoint[] + } = {} +): Promise { + const authKey = makeDidKeyFromKeypair(keypair) + const uri = Did.getFullDidUriFromKey(authKey) + + const result: DidDocument = { + uri, + authentication: [authKey], + service: endpoints, + } + + if (keyRelationships.has('keyAgreement')) { + const encryptionKeypair = makeEncryptionKeyTool(`${keypair.publicKey}//enc`) + .keyAgreement[0] + const encKey = { + ...encryptionKeypair, + id: computeKeyId(encryptionKeypair.publicKey), + } + result.keyAgreement = [encKey] + } + if (keyRelationships.has('assertionMethod')) { + const attKey = makeDidKeyFromKeypair( + keypair.derive('//att') as KiltKeyringPair + ) + result.assertionMethod = [attKey] + } + if (keyRelationships.has('capabilityDelegation')) { + const delKey = makeDidKeyFromKeypair( + keypair.derive('//del') as KiltKeyringPair + ) + result.capabilityDelegation = [delKey] + } + + return result +} + +/** + * Creates a full DID from a light DID where the verification keypair is enabled for all verification purposes (authentication, assertionMethod, capabilityDelegation). + * This is not recommended, use for demo purposes only! + * + * @param lightDid The light DID whose keys will be used on the full DID. + * @returns A full DID instance that is not yet written to the blockchain. + */ +export async function createLocalDemoFullDidFromLightDid( + lightDid: DidDocument +): Promise { + const { uri, authentication } = lightDid + + return { + uri: Did.getFullDidUri(uri), + authentication, + assertionMethod: authentication, + capabilityDelegation: authentication, + keyAgreement: lightDid.keyAgreement, + } +} + +// It takes the auth key from the light DID and use it as attestation and delegation key as well. +export async function createFullDidFromLightDid( + payer: KiltKeyringPair, + lightDidForId: DidDocumentV2.DidDocument, + sign: StoreDidCallback +): Promise { + const api = ConfigService.get('api') + const fullDidDocumentToBeCreated = lightDidForId + fullDidDocumentToBeCreated.assertionMethod = [ + fullDidDocumentToBeCreated.authentication[0], + ] + fullDidDocumentToBeCreated.capabilityDelegation = [ + fullDidDocumentToBeCreated.authentication[0], + ] + const tx = await getStoreTxFromDidDocument( + fullDidDocumentToBeCreated, + payer.address, + sign + ) + await Blockchain.signAndSubmitTx(tx, payer) + const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid + const encodedDidDetails = await queryFunction( + Did.DidChainV2.toChain(Did.getFullDidUri(fullDidDocumentToBeCreated.id)) + ) + const { + document: { + authentication, + uri, + assertionMethod, + capabilityDelegation, + keyAgreement, + service, + }, + } = await Did.linkedInfoFromChain(encodedDidDetails) + const didDocument: DidDocumentV2.DidDocument = { + id: uri, + authentication: [authentication[0].id], + verificationMethod: [ + Did.DidUtilsV2.didKeyToVerificationMethod(uri, authentication[0].id, { + keyType: authentication[0].type, + publicKey: authentication[0].publicKey, + }), + ], + service, + } + if (assertionMethod !== undefined) { + didDocument.assertionMethod = [assertionMethod[0].id] + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod(uri, assertionMethod[0].id, { + keyType: assertionMethod[0].type, + publicKey: assertionMethod[0].publicKey, + }) + ) + } + if (capabilityDelegation !== undefined) { + didDocument.capabilityDelegation = [capabilityDelegation[0].id] + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod( + uri, + capabilityDelegation[0].id, + { + keyType: capabilityDelegation[0].type, + publicKey: capabilityDelegation[0].publicKey, + } + ) + ) + } + if (keyAgreement !== undefined) { + didDocument.capabilityDelegation = [keyAgreement[0].id] + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod(uri, keyAgreement[0].id, { + keyType: keyAgreement[0].type, + publicKey: keyAgreement[0].publicKey, + }) + ) + } + + return didDocument +} + +export async function createFullDidFromSeed( + payer: KiltKeyringPair, + keypair: KiltKeyringPair +): Promise { + const lightDid = await createMinimalLightDidFromKeypair(keypair) + const sign = makeStoreDidCallback(keypair) + return createFullDidFromLightDid(payer, lightDid, sign) +}