diff --git a/license-header.js b/license-header.js index c170d5c..06e83a5 100644 --- a/license-header.js +++ b/license-header.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2023, Built on KILT. + * Copyright (c) 2018-2024, Built on KILT. * * 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. diff --git a/package.json b/package.json index c9e94b0..91548e7 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,9 @@ "@kiltprotocol/sdk-js": "^0.35.0", "@kiltprotocol/types": "^0.35.0", "@kiltprotocol/vc-export": "^0.35.0", + "@kiltprotocol/es256k-jcs-2023": "latest-rc", + "@kiltprotocol/sr25519-jcs-2023": "latest-rc", + "@kiltprotocol/eddsa-jcs-2022": "latest-rc", "@polkadot/keyring": "^12.3.2", "@polkadot/util": "^12.3.2", "yargs": "^17.7.2" diff --git a/src/cli/createDidConfig.ts b/src/cli/createDidConfig.ts index f8dc6db..3327a22 100644 --- a/src/cli/createDidConfig.ts +++ b/src/cli/createDidConfig.ts @@ -7,17 +7,27 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { Credential, Did, connect, disconnect } from '@kiltprotocol/sdk-js' -import { DidResourceUri, DidUri, ICredentialPresentation, SignCallback } from '@kiltprotocol/types' +import { createSigner as eddsaSigner } from '@kiltprotocol/eddsa-jcs-2022' +import { createSigner as es256kSigner } from '@kiltprotocol/es256k-jcs-2023' +import { ConformingDidDocument, Did, DidResourceUri, DidUri, connect, disconnect } from '@kiltprotocol/sdk-js' +import { createSigner as sr25519Signer } from '@kiltprotocol/sr25519-jcs-2023' -import { Keyring } from '@polkadot/keyring' +import Keyring from '@polkadot/keyring' +import { decodePair } from '@polkadot/keyring/pair/decode' import { u8aEq } from '@polkadot/util' +import { base58Decode } from '@polkadot/util-crypto' import { readFile, writeFile } from 'fs/promises' import yargs from 'yargs/yargs' -import { didConfigResourceFromCredential, createCredential } from '../wellKnownDidConfiguration/index.js' -import type { DidConfigResource } from '../types/index.js' +import { DidConfigResource, DomainLinkageCredential } from '../types/Credential.js' +import { + DATA_INTEGRITY_PROOF_TYPE, + KILT_SELF_SIGNED_PROOF_TYPE, + createCredential, + didConfigResourceFromCredentials, + verifyDomainLinkageCredential, +} from '../wellKnownDidConfiguration/index.js' type KeyType = 'sr25519' | 'ed25519' | 'ecdsa' @@ -41,22 +51,39 @@ const createCredentialOpts = { wsAddress: { alias: 'w', type: 'string', demandOption: true, default: 'wss://spiritnet.kilt.io' }, } as const -async function issueCredential(did: DidUri, origin: string, seed: string, keyType: KeyType, nodeAddress: string) { - await connect(nodeAddress) - const didDocument = await Did.resolve(did) - const assertionMethod = didDocument?.document?.assertionMethod?.[0] +async function issueCredential(did: DidUri, origin: string, seed: string, keyType: KeyType, proofType?: string) { + const { didDocument } = await Did.resolveCompliant(did) + const assertionMethodId = didDocument?.assertionMethod?.[0] + const assertionMethod = didDocument?.verificationMethod?.find(({ id }) => id.endsWith(assertionMethodId ?? '')) if (!assertionMethod) { throw new Error( `Could not resolve assertionMethod of ${did}. Make sure the DID is registered to this chain and has an assertionMethod key.` ) } - const keypair = new Keyring({ type: keyType }).addFromUri(seed) - if (assertionMethod.type !== keypair.type || !u8aEq(assertionMethod.publicKey, keypair.publicKey)) { - throw new Error('public key and/or key type of the DIDs assertionMethod does not match the supplied signing key') + // generate keypair and extract private key + const keypair = new Keyring().addFromUri(seed, undefined, keyType) + const { secretKey } = decodePair(undefined, keypair.encodePkcs8(), 'none') + if (!u8aEq(keypair.publicKey, base58Decode(assertionMethod.publicKeyBase58))) { + throw new Error('seed does not match DID assertion method') } - const keyUri: DidResourceUri = `${didDocument!.document!.uri}${assertionMethod.id}` - const signCallback: SignCallback = async ({ data }) => ({ signature: keypair.sign(data), keyUri, keyType }) - const credential = await createCredential(signCallback, origin, did) + // create signer + const keyUri: DidResourceUri = assertionMethod.id + const createSigner = { + sr25519: sr25519Signer, + ed25519: eddsaSigner, + ecdsa: es256kSigner, + } + const signer = await createSigner[keyType]({ id: keyUri, secretKey, publicKey: keypair.publicKey }) + + const credential = await createCredential( + signer, + origin, + didDocument as ConformingDidDocument, + { proofType } as { proofType: typeof DATA_INTEGRITY_PROOF_TYPE | typeof KILT_SELF_SIGNED_PROOF_TYPE } + ) + + await verifyDomainLinkageCredential(credential, origin, { expectedDid: did }) + return credential } @@ -72,29 +99,30 @@ async function write(toWrite: unknown, outPath?: string) { async function run() { await yargs(process.argv.slice(2)) .command( - 'fromCredential ', - 'create a Did Configuration Resource from an existing Kilt Credential Presentation', + 'fromCredential [pathToCredential..]', + 'create a Did Configuration Resource from one or more existing Domain Linkage Credentials', (ygs) => ygs.options(commonOpts).positional('pathToCredential', { - describe: 'Path to a json file containing the credential presentation', + describe: 'Path to a json file containing a Domain Linkage Credential', type: 'string', demandOption: true, + array: true, }), async ({ pathToCredential, outFile }) => { - let credential: ICredentialPresentation - try { - credential = JSON.parse(await readFile(pathToCredential, { encoding: 'utf-8' })) - } catch (cause) { - throw new Error(`Cannot parse file ${pathToCredential}`, { cause }) - } - if (!Credential.isPresentation(credential)) { - throw new Error(`Malformed Credential Presentation loaded from ${pathToCredential}`) - } + const credentials: DomainLinkageCredential[] = await Promise.all( + pathToCredential.map(async (path) => { + try { + return JSON.parse(await readFile(path, { encoding: 'utf-8' })) + } catch (cause) { + throw new Error(`Cannot parse file ${pathToCredential}`, { cause }) + } + }) + ) let didResource: DidConfigResource try { - didResource = await didConfigResourceFromCredential(credential) + didResource = didConfigResourceFromCredentials(credentials) } catch (cause) { - throw new Error('Credential Presentation is not suitable for use in a Did Configuration Resource', { + throw new Error('Credential is not suitable for use in a Did Configuration Resource', { cause, }) } @@ -103,20 +131,36 @@ async function run() { ) .command( 'credentialOnly', - 'issue a new Kilt Credential Presentation for use in a Did Configuration Resource', - { ...createCredentialOpts, ...commonOpts }, - async ({ origin, seed, keyType, wsAddress, outFile, did }) => { - const credential = await issueCredential(did as DidUri, origin, seed, keyType, wsAddress) + 'issue a new Domain Linkage Credential for use in a Did Configuration Resource', + { + ...createCredentialOpts, + ...commonOpts, + proofType: { + alias: 'p', + choices: [DATA_INTEGRITY_PROOF_TYPE, KILT_SELF_SIGNED_PROOF_TYPE] as const, + default: KILT_SELF_SIGNED_PROOF_TYPE, + describe: + 'Which proof type to use in the credential. DataIntegrity is the more modern proof type, but might not be accepted by all extensions yet. Did Configuration Resources can contain multiple credentials, though.', + }, + }, + async ({ origin, seed, keyType, wsAddress, outFile, did, proofType }) => { + await connect(wsAddress) + const credential = await issueCredential(did as DidUri, origin, seed, keyType, proofType) await write(credential, outFile) } ) .command( '$0', - 'create a Did Configuration Resource from a freshly issued Kilt Credential', + 'create a Did Configuration Resource containing newly issued Domain Linkage Credentials', { ...createCredentialOpts, ...commonOpts }, async ({ origin, seed, keyType, wsAddress, outFile, did }) => { - const credential = await issueCredential(did as DidUri, origin, seed, keyType, wsAddress) - const didResource = await didConfigResourceFromCredential(credential) + await connect(wsAddress) + const credentials = await Promise.all( + [DATA_INTEGRITY_PROOF_TYPE, KILT_SELF_SIGNED_PROOF_TYPE].map((proofType) => + issueCredential(did as DidUri, origin, seed, keyType, proofType) + ) + ) + const didResource = didConfigResourceFromCredentials(credentials) await write(didResource, outFile) } ) diff --git a/src/tests/utils.ts b/src/tests/utils.ts index b69d83c..8170aac 100644 --- a/src/tests/utils.ts +++ b/src/tests/utils.ts @@ -158,7 +158,7 @@ export async function createCtype( export async function startContainer(): Promise { const WS_PORT = 9944 - const image = process.env.TESTCONTAINERS_NODE_IMG || 'kiltprotocol/mashnet-node' + const image = process.env.TESTCONTAINERS_NODE_IMG || 'kiltprotocol/standalone-node' console.log(`using testcontainer with image ${image}`) const testcontainer = new GenericContainer(image) .withCommand(['--dev', `--rpc-port=${WS_PORT}`, '--rpc-external']) diff --git a/src/types/Credential.ts b/src/types/Credential.ts index f40f220..574f396 100644 --- a/src/types/Credential.ts +++ b/src/types/Credential.ts @@ -5,17 +5,13 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidUri } from '@kiltprotocol/types' -import { VerifiableCredential, constants } from '@kiltprotocol/vc-export' +import type { DidUri } from '@kiltprotocol/types' +import type { constants } from '@kiltprotocol/vc-export' -import { IMessageWorkflow } from './index.js' -import { DomainLinkageProof } from './Window.js' +import { DOMAIN_LINKAGE_CREDENTIAL_TYPE } from '../wellKnownDidConfiguration/index.js' +import type { SelfSignedProof } from './LegacyProofs.js' -export type ICredentialRequest = IMessageWorkflow & { - challenge: string -} - -export interface CredentialSubject { +export type CredentialSubject = { id: DidUri origin: string } @@ -23,11 +19,35 @@ export interface CredentialSubject { type Contexts = [ typeof constants.DEFAULT_VERIFIABLECREDENTIAL_CONTEXT, 'https://identity.foundation/.well-known/did-configuration/v1', + ...string[], ] -export interface DomainLinkageCredential - extends Omit { +export type DataIntegrityProof = { + type: 'DataIntegrity' + verificationMethod: string + cryptosuite: string + proofPurpose: string + proofValue: string + created?: string + expires?: string + domain?: string + challenge?: string + previousProof?: string +} + +export type DomainLinkageProof = SelfSignedProof | DataIntegrityProof + +export type DidConfigResource = { + '@context': string + linked_dids: DomainLinkageCredential[] +} + +export type DomainLinkageCredential = { '@context': Contexts + type: (typeof constants.DEFAULT_VERIFIABLECREDENTIAL_TYPE | typeof DOMAIN_LINKAGE_CREDENTIAL_TYPE | string)[] credentialSubject: CredentialSubject + issuer: string + issuanceDate: string + expirationDate: string proof: DomainLinkageProof } diff --git a/src/types/LegacyProofs.ts b/src/types/LegacyProofs.ts new file mode 100644 index 0000000..6ad511e --- /dev/null +++ b/src/types/LegacyProofs.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018-2024, Built on KILT. + * + * 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 { DidUri, DidResourceUri } from '@kiltprotocol/types' + +export interface Proof { + type: string + created?: string + proofPurpose?: string + [key: string]: any +} + +export type ConformingDidDocumentKeyType = + | 'Ed25519VerificationKey2018' + | 'Sr25519VerificationKey2020' + | 'EcdsaSecp256k1VerificationKey2019' + | 'X25519KeyAgreementKey2019' + +export type IPublicKeyRecord = { + /** + * The full key URI, in the form of #. + */ + id: DidResourceUri + /** + * The key controller, in the form of . + */ + controller: DidUri + /** + * The base58-encoded public component of the key. + */ + publicKeyBase58: string + /** + * The key type signalling the intended signing/encryption algorithm for the use of this key. + */ + type: ConformingDidDocumentKeyType +} + +export interface SelfSignedProof extends Proof { + type: typeof KILT_SELF_SIGNED_PROOF_TYPE + verificationMethod: IPublicKeyRecord['id'] | IPublicKeyRecord + signature: string + challenge?: string +} + +export const KILT_VERIFIABLECREDENTIAL_TYPE = 'KiltCredential2020' +export const KILT_SELF_SIGNED_PROOF_TYPE = 'KILTSelfSigned2020' diff --git a/src/types/Message.ts b/src/types/Message.ts index 9632d36..7d24b8a 100644 --- a/src/types/Message.ts +++ b/src/types/Message.ts @@ -211,3 +211,26 @@ export type IEncryptedMessage { export interface ApiWindow extends This { kilt: Record> } - -export type DomainLinkageProof = { - type: Array - rootHash: string -} & Pick & - Pick - -export interface DidConfigResource { - '@context': string - linked_dids: [DomainLinkageCredential] -} diff --git a/src/types/index.ts b/src/types/index.ts index 6253a55..941e7e3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,120 +5,9 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidResourceUri, DidUri, IEncryptedMessage, KiltAddress } from '@kiltprotocol/types' -import { HexString } from './Imported.js' -import { CredentialDigestProof, SelfSignedProof, VerifiableCredential, constants } from '@kiltprotocol/vc-export' -import { IMessage } from './Message.js' - export * from './Message.js' export * from './Quote.js' export * from './Imported.js' export * from './Session.js' export * from './Credential.js' export * from './Window.js' - -export type This = typeof globalThis - -export interface IEncryptedMessageV1 { - /** ID of the key agreement key of the receiver DID used to encrypt the message */ - receiverKeyId: DidResourceUri - - /** ID of the key agreement key of the sender DID used to encrypt the message */ - senderKeyId: DidResourceUri - - /** ciphertext as hexadecimal */ - ciphertext: string - - /** 24 bytes nonce as hexadecimal */ - nonce: string -} - -export interface PubSubSessionV1 { - /** Configure the callback the extension must use to send messages to the dApp. Overrides previous values. */ - listen: (callback: (message: IEncryptedMessageV1) => Promise) => Promise - - /** send the encrypted message to the extension */ - send: (message: IEncryptedMessageV1) => Promise - - /** close the session and stop receiving further messages */ - close: () => Promise - - /** ID of the key agreement key of the temporary DID the extension will use to encrypt the session messages */ - encryptionKeyId: string - - /** bytes as hexadecimal */ - encryptedChallenge: string - - /** 24 bytes nonce as hexadecimal */ - nonce: string -} - -export interface PubSubSessionV2 { - /** Configure the callback the extension must use to send messages to the dApp. Overrides previous values. */ - listen: (callback: (message: IEncryptedMessage) => Promise) => Promise - - /** send the encrypted message to the extension */ - send: (message: IEncryptedMessage) => Promise - - /** close the session and stop receiving further messages */ - close: () => Promise - - /** ID of the key agreement key of the temporary DID the extension will use to encrypt the session messages */ - encryptionKeyUri: DidResourceUri - - /** bytes as hexadecimal */ - encryptedChallenge: string - - /** 24 bytes nonce as hexadecimal */ - nonce: string -} - -export interface InjectedWindowProvider { - startSession: (dAppName: string, dAppEncryptionKeyId: DidResourceUri, challenge: string) => Promise - name: string - version: string - specVersion: '1.0' | '3.0' - signWithDid: (plaintext: string) => Promise<{ signature: string; didKeyUri: DidResourceUri }> - signExtrinsicWithDid: ( - extrinsic: HexString, - signer: KiltAddress - ) => Promise<{ signed: HexString; didKeyUri: DidResourceUri }> - getSignedDidCreationExtrinsic: (submitter: KiltAddress) => Promise<{ signedExtrinsic: HexString }> -} - -export interface ApiWindow extends This { - kilt: Record> -} - -export interface CredentialSubject { - id: DidUri - origin: string -} - -type Contexts = [ - typeof constants.DEFAULT_VERIFIABLECREDENTIAL_CONTEXT, - 'https://identity.foundation/.well-known/did-configuration/v1', -] - -export type DomainLinkageProof = { - type: Array - rootHash: string -} & Pick & - Pick - -export interface DomainLinkageCredential - extends Omit { - '@context': Contexts - credentialSubject: CredentialSubject - proof: DomainLinkageProof -} - -export interface DidConfigResource { - '@context': string - linked_dids: [DomainLinkageCredential] -} - -export interface IMessageWorkflow { - message: IMessage - encryptedMessage: IEncryptedMessage -} diff --git a/src/wellKnownDidConfiguration/index.ts b/src/wellKnownDidConfiguration/index.ts index 74ffb68..c1d5224 100644 --- a/src/wellKnownDidConfiguration/index.ts +++ b/src/wellKnownDidConfiguration/index.ts @@ -5,35 +5,22 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { - Did, - CType, - Credential, - Claim, - SignCallback, - DidUri, - Utils, - ICredentialPresentation, - DidResourceUri, -} from '@kiltprotocol/sdk-js' -import type { DomainLinkageCredential, DomainLinkageProof, DidConfigResource } from '../types/index.js' -import { - SelfSignedProof, - constants, - fromCredentialAndAttestation, - Proof, - CredentialDigestProof, - verification, - VerifiableCredential, -} from '@kiltprotocol/vc-export' -import { hexToU8a, isHex } from '@polkadot/util' +import { Did, CType, DidUri, DidResourceUri, ConformingDidDocument } from '@kiltprotocol/sdk-js' +import type { DomainLinkageCredential, DidConfigResource, DataIntegrityProof } from '../types/index.js' +import { SelfSignedProof, constants } from '@kiltprotocol/vc-export' +import { hexToU8a, u8aToHex } from '@polkadot/util' +import type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' +import * as ed25519 from '@kiltprotocol/eddsa-jcs-2022' +import * as sr25519 from '@kiltprotocol/sr25519-jcs-2023' +import * as es256k from '@kiltprotocol/es256k-jcs-2023' + +import { base58Decode, base58Encode, blake2AsU8a } from '@polkadot/util-crypto' const { DEFAULT_VERIFIABLECREDENTIAL_TYPE, KILT_VERIFIABLECREDENTIAL_TYPE, KILT_SELF_SIGNED_PROOF_TYPE, DEFAULT_VERIFIABLECREDENTIAL_CONTEXT, - KILT_CREDENTIAL_DIGEST_PROOF_TYPE, } = constants export { @@ -43,6 +30,7 @@ export { DEFAULT_VERIFIABLECREDENTIAL_CONTEXT as DID_VC_CONTEXT, } export const DID_CONFIGURATION_CONTEXT = 'https://identity.foundation/.well-known/did-configuration/v1' +export const DATA_INTEGRITY_CONTEXT = 'https://w3id.org/security/data-integrity/v1' export const ctypeDomainLinkage = CType.fromProperties('Domain Linkage Credential', { origin: { @@ -51,6 +39,8 @@ export const ctypeDomainLinkage = CType.fromProperties('Domain Linkage Credentia }, }) +const MULTIBASE_BASE58BTC_HEADER = 'z' + function checkOrigin(input: string) { const { origin, protocol } = new URL(input) if (input !== origin) { @@ -61,155 +51,269 @@ function checkOrigin(input: string) { } } +export const DOMAIN_LINKAGE_CREDENTIAL_TYPE = 'DomainLinkageCredential' as const +export const DATA_INTEGRITY_PROOF_TYPE = 'DataIntegrity' as const + +const suites = [ed25519.cryptosuite, sr25519.cryptosuite, es256k.cryptosuite] + export async function createCredential( - signCallback: SignCallback, + signer: SignerInterface, origin: string, - didUri: DidUri -): Promise { + did: DidUri | ConformingDidDocument, + { + proofType = KILT_SELF_SIGNED_PROOF_TYPE, + expirationDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 5), + }: { + expirationDate?: Date + proofType?: typeof KILT_SELF_SIGNED_PROOF_TYPE | typeof DATA_INTEGRITY_PROOF_TYPE + } = {} +): Promise { checkOrigin(origin) - const fullDid = await Did.resolve(didUri) - - if (!fullDid?.document) { - throw new Error('No Did found: Please create a Full DID') + const document = await (async () => { + if (typeof did === 'string') { + const { didDocument } = await Did.resolveCompliant(did) + if (!didDocument) { + throw new Error('Failed to resolve DID ' + did) + } + return didDocument + } else if (typeof did === 'object' && did?.id) { + return did + } else { + throw new Error('must pass a DID or DID Document for the did parameter') + } + })() + + if (!document.assertionMethod?.length) { + throw new Error('DID Document does not contain assertion key: Please add assertion key') } - const { document } = fullDid - - const assertionKey = document.assertionMethod?.[0] - - if (!assertionKey) { - throw new Error('Full DID doesnt have assertion key: Please add assertion key') + const credentialBody: Omit = { + '@context': [DEFAULT_VERIFIABLECREDENTIAL_CONTEXT, DID_CONFIGURATION_CONTEXT], + issuer: document.id, + issuanceDate: new Date().toISOString(), + expirationDate: expirationDate.toISOString(), + type: [DEFAULT_VERIFIABLECREDENTIAL_TYPE, DOMAIN_LINKAGE_CREDENTIAL_TYPE], + credentialSubject: { + id: document.id, + origin, + }, } - const domainClaimContents = { - origin, + if (!signer) { + throw new Error('No signer available for an assertion method of the DID') } - const claim = Claim.fromCTypeAndClaimContents(ctypeDomainLinkage, domainClaimContents, document.uri) + switch (proofType) { + case KILT_SELF_SIGNED_PROOF_TYPE: { + const credential = { + ...credentialBody, + proof: { + type: KILT_SELF_SIGNED_PROOF_TYPE, + verificationMethod: signer.id, + proofPurpose: 'assertionMethod', + }, + } as DomainLinkageCredential + const docHash = blake2AsU8a(JSON.stringify(credential)) + const signature = await signer.sign({ data: docHash }) + ;(credential.proof as SelfSignedProof).signature = u8aToHex(signature) + // @ts-expect-error for backwards compatibility + credential.credentialSubject.rootHash = u8aToHex(docHash) + return credential + } + case DATA_INTEGRITY_PROOF_TYPE: { + credentialBody['@context'].push(DATA_INTEGRITY_CONTEXT) + const suite = suites.find(({ requiredAlgorithm }) => requiredAlgorithm === signer.algorithm) + if (!suite) { + throw new Error(`unknown signer algorithm ${signer.algorithm}`) + } + const proof = { + type: DATA_INTEGRITY_PROOF_TYPE, + verificationMethod: signer.id, + cryptosuite: suite.name, + proofPurpose: 'assertionMethod', + created: credentialBody.issuanceDate, + expires: credentialBody.expirationDate, + domain: origin, + } as DataIntegrityProof + const verifyData = await suite.createVerifyData({ document: credentialBody, proof }) + const signature = await signer.sign({ data: verifyData }) + proof.proofValue = MULTIBASE_BASE58BTC_HEADER + base58Encode(signature) + return { ...credentialBody, proof } + } + default: + throw new Error(`unknown proof type ${proofType}`) + } +} - const credential = Credential.fromClaim(claim) +function checkIsDomainLinkageCredential(credential: DomainLinkageCredential): void { + if ( + !( + credential['@context']?.[0] === DEFAULT_VERIFIABLECREDENTIAL_CONTEXT && + credential['@context']?.[1] === DID_CONFIGURATION_CONTEXT + ) + ) { + throw new Error( + `credential must include contexts ${[DEFAULT_VERIFIABLECREDENTIAL_CONTEXT, DID_CONFIGURATION_CONTEXT]}` + ) + } + if ( + !( + credential.type?.includes(DOMAIN_LINKAGE_CREDENTIAL_TYPE) && + credential.type?.includes(DEFAULT_VERIFIABLECREDENTIAL_TYPE) + ) + ) { + throw new Error( + `credential must have types ${DEFAULT_VERIFIABLECREDENTIAL_TYPE} & ${DOMAIN_LINKAGE_CREDENTIAL_TYPE}` + ) + } + try { + Did.validateUri(credential.credentialSubject?.id, 'Did') + } catch { + throw new Error('credentialSubject.id must be present and must be a DID') + } + if (credential.issuer !== credential.credentialSubject.id) { + throw new Error('issuer and credentialSubject.id must be identical') + } + try { + checkOrigin(credential.credentialSubject?.origin) + } catch { + throw new Error('credentialSubject.origin must be present and must be a valid domain origin') + } + if (typeof credential.issuanceDate !== 'string' || typeof credential.expirationDate !== 'string') { + throw new Error('issuanceDate & expirationDate must be present and must be iso date-time strings') + } +} - const presentation = await Credential.createPresentation({ - credential, - signCallback, +export function didConfigResourceFromCredentials(credentials: DomainLinkageCredential[]): DidConfigResource { + credentials.forEach(checkIsDomainLinkageCredential) + credentials.reduce((last, next) => { + if (last.credentialSubject.origin !== next.credentialSubject.origin) { + throw new Error('credentials should have the same origin property') + } + return last }) - - if (presentation.claimerSignature.keyUri !== `${document.uri}${assertionKey.id}`) { - throw new Error('The credential presentation needs to be signed with the assertionMethod key') + return { + '@context': DID_CONFIGURATION_CONTEXT, + linked_dids: credentials, } - - return presentation } -export const DOMAIN_LINKAGE_CREDENTIAL_TYPE = 'DomainLinkageCredential' - -export async function didConfigResourceFromCredential( - credential: ICredentialPresentation, - expirationDate: string = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 5).toISOString() +export async function createDidConfigResource( + signer: SignerInterface, + origin: string, + did: DidUri, + { + expirationDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 5), + }: { + expirationDate?: Date + } = {} ): Promise { - if (!Credential.isPresentation(credential)) { - throw new Error('Input must be an IPresentation') - } - const claimContents = credential.claim.contents - - CType.verifyClaimAgainstSchema(claimContents, ctypeDomainLinkage) - - const { origin } = claimContents checkOrigin(origin as string) - if (!(credential.claim.owner && origin)) { - throw new Error('Claim must have an owner and an origin property') - } - const propsToRemove = Object.keys(claimContents).filter((i) => i !== 'origin') - const originOnlyCredential = Credential.removeClaimProperties(credential, propsToRemove) - - const { - proof: allProofs, - credentialSubject, - id: _, - legitimationIds: __, - ...VC - } = fromCredentialAndAttestation(originOnlyCredential, { - owner: credential.claim.owner, - } as any) - - const ssProof = (allProofs as Proof[]).find(({ type }) => type === KILT_SELF_SIGNED_PROOF_TYPE) as SelfSignedProof - const digProof = (allProofs as Proof[]).find( - ({ type }) => type === KILT_CREDENTIAL_DIGEST_PROOF_TYPE - ) as CredentialDigestProof - - const proof: DomainLinkageProof = { - ...ssProof, - ...digProof, - rootHash: credential.rootHash, - type: [KILT_SELF_SIGNED_PROOF_TYPE, KILT_CREDENTIAL_DIGEST_PROOF_TYPE], - } - return { - '@context': DID_CONFIGURATION_CONTEXT, - linked_dids: [ - { - ...VC, - '@context': [DEFAULT_VERIFIABLECREDENTIAL_CONTEXT, DID_CONFIGURATION_CONTEXT], - expirationDate, - type: [DEFAULT_VERIFIABLECREDENTIAL_TYPE, DOMAIN_LINKAGE_CREDENTIAL_TYPE], - proof, - credentialSubject: { - id: credentialSubject['@id'] as DidUri, // canonicalize @id to id - origin: credentialSubject.origin as string, - // @ts-expect-error for compatibility with older implementations, add the credential rootHash (which is also contained in the credential id) - rootHash: credential.rootHash, - }, - }, - ], + const { didDocument } = await Did.resolveCompliant(did) + + if (!didDocument) { + throw new Error('No Did found: Please create a Full DID') } + + const credentials = await Promise.all([ + createCredential(signer, origin, didDocument as ConformingDidDocument, { + proofType: DATA_INTEGRITY_PROOF_TYPE, + expirationDate, + }), + createCredential(signer, origin, didDocument as ConformingDidDocument, { + proofType: KILT_SELF_SIGNED_PROOF_TYPE, + expirationDate, + }), + ]) + + return didConfigResourceFromCredentials(credentials) } -async function verifyDomainLinkageCredential( +export async function verifyDomainLinkageCredential( credential: DomainLinkageCredential, expectedOrigin: string, - expectedDid?: DidUri + { expectedDid, allowUnsafe = false }: { expectedDid?: DidUri; allowUnsafe?: boolean } = {} ): Promise { - const { issuer, credentialSubject, proof } = credential - - if (issuer !== credentialSubject.id) throw new Error('issuer and credential subject must be identical') + checkIsDomainLinkageCredential(credential) - const didUri = credentialSubject.id - Did.validateUri(didUri, 'Did') + const { credentialSubject, proof } = credential + const did = credentialSubject.id if (expectedOrigin !== credentialSubject.origin) throw new Error('origin does not match expected') - if (expectedDid && expectedDid !== didUri) throw new Error('DID does not match expected') + if (expectedDid && expectedDid !== did) throw new Error('DID does not match expected') - // get root hash incl fallback for older domain linkage credential types - const { rootHash = proof.rootHash, ...cleanSubject } = credentialSubject as any - if (!isHex(rootHash)) { - throw new Error('rootHash must be a hex encoded string') + const now = new Date().getTime() + if (new Date(credential.issuanceDate).getTime() > now) { + throw new Error('issuanceDate is in the future') } - - const pType = Array.isArray(proof.type) ? proof.type : [proof.type] - if (!pType.includes(KILT_SELF_SIGNED_PROOF_TYPE)) { - throw new Error(`proof type must include ${KILT_SELF_SIGNED_PROOF_TYPE}`) + if (new Date(credential.expirationDate).getTime() < now) { + throw new Error('expirationDate is in the past') } - await Did.verifyDidSignature({ - expectedVerificationMethod: 'assertionMethod', - signature: hexToU8a(proof.signature), - keyUri: proof.verificationMethod as DidResourceUri, - message: Utils.Crypto.coToUInt8(rootHash), - }) - - if (pType.includes(KILT_CREDENTIAL_DIGEST_PROOF_TYPE)) { - await verification.verifyCredentialDigestProof( - { - ...credential, - credentialSubject: cleanSubject, - id: `kilt:cred:${rootHash}`, - } as unknown as VerifiableCredential, - { ...proof, type: KILT_CREDENTIAL_DIGEST_PROOF_TYPE } - ) + switch (proof.type) { + case KILT_SELF_SIGNED_PROOF_TYPE: { + // @ts-expect-error rootHash is a fallback for older domain linkage credential types + const { rootHash } = credentialSubject + let docHash: Uint8Array + if (rootHash && allowUnsafe) { + docHash = hexToU8a(rootHash) + } else { + const copy = JSON.parse(JSON.stringify(credential)) + delete copy.credentialSubject.rootHash + delete copy.proof.signature + docHash = blake2AsU8a(JSON.stringify(copy)) + } + await Did.verifyDidSignature({ + expectedVerificationMethod: 'assertionMethod', + signature: hexToU8a(proof.signature), + keyUri: proof.verificationMethod as DidResourceUri, + message: docHash, + }) + break + } + case DATA_INTEGRITY_PROOF_TYPE: { + const cryptosuite = suites.find(({ name }) => name === proof.cryptosuite) + if (!cryptosuite) { + throw new Error(`unknown cryptosuite ${proof.cryptosuite}`) + } + if (!proof.proofValue.startsWith(MULTIBASE_BASE58BTC_HEADER)) { + throw new Error('proofValue is required to be in multibase base58btc encoding') + } + if (proof.proofPurpose !== 'assertionMethod') { + throw new Error('proof must have assertionMethod purpose') + } + if (proof.domain && proof.domain !== expectedOrigin) { + throw new Error('proof must have assertionMethod purpose') + } + const signature = base58Decode(proof.proofValue.slice(1)) + const credentialCopy: Record = { ...credential } + delete credentialCopy.proof + const verifyData = await cryptosuite.createVerifyData({ document: credentialCopy, proof }) + + const { didDocument } = await Did.resolveCompliant(Did.parse(proof.verificationMethod as DidUri).did) + + const verificationMethod = didDocument?.verificationMethod?.find(({ id }) => proof.verificationMethod === id) + if ( + !verificationMethod || + !didDocument?.assertionMethod?.some((fragment) => proof.verificationMethod.endsWith(fragment)) + ) { + throw new Error( + `Verification method ${proof.verificationMethod} could not be resolved to a valid assertionMethod` + ) + } + if (verificationMethod.controller !== did) { + throw new Error(`expected controller ${did}, got ${verificationMethod.controller}`) + } + const verifier = await cryptosuite.createVerifier({ verificationMethod } as any) + if ((await verifier.verify({ signature, data: verifyData })) !== true) { + throw new Error('Failed to veriy DataIntegrity proof against signature') + } + break + } } - - return didUri + return did } async function asyncSome( @@ -234,7 +338,7 @@ async function asyncSome( export async function verifyDidConfigResource( didConfig: DidConfigResource, expectedOrigin: string, - expectedDid?: DidUri + { expectedDid, allowUnsafe = false }: { expectedDid?: DidUri; allowUnsafe?: boolean } = {} ): Promise { // Verification steps outlined in Well Known DID Configuration // https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification @@ -242,7 +346,7 @@ export async function verifyDidConfigResource( checkOrigin(expectedOrigin) return asyncSome(didConfig.linked_dids, (credential) => - verifyDomainLinkageCredential(credential, expectedOrigin, expectedDid) + verifyDomainLinkageCredential(credential, expectedOrigin, { expectedDid, allowUnsafe }) ).catch(() => { throw new Error('Did Configuration Resource could not be verified') }) diff --git a/src/wellKnownDidConfiguration/wellKnownDidConfiguration.test.ts b/src/wellKnownDidConfiguration/wellKnownDidConfiguration.test.ts index 3408f1e..709cafc 100644 --- a/src/wellKnownDidConfiguration/wellKnownDidConfiguration.test.ts +++ b/src/wellKnownDidConfiguration/wellKnownDidConfiguration.test.ts @@ -5,32 +5,24 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { - KiltKeyringPair, - DidUri, - DidDocument, - ICredentialPresentation, - IClaim, - DidResourceUri, - connect, - disconnect, - CType, -} from '@kiltprotocol/sdk-js' -import { mnemonicGenerate } from '@polkadot/util-crypto' -import { DidConfigResource } from '../types' -import { BN } from '@polkadot/util' +import { createSigner } from '@kiltprotocol/eddsa-jcs-2022' +import { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' +import { DidDocument, DidResourceUri, DidUri, KiltKeyringPair, connect, disconnect } from '@kiltprotocol/sdk-js' import { Keyring } from '@kiltprotocol/utils' +import { BN } from '@polkadot/util' +import { mnemonicGenerate, mnemonicToMiniSecret } from '@polkadot/util-crypto' +import { createCtype, fundAccount, generateDid, startContainer } from '../tests/utils' +import { DidConfigResource, DomainLinkageCredential } from '../types' import { - createCredential, + DEFAULT_VERIFIABLECREDENTIAL_TYPE, DID_CONFIGURATION_CONTEXT, - verifyDidConfigResource, - didConfigResourceFromCredential, DID_VC_CONTEXT, - DEFAULT_VERIFIABLECREDENTIAL_TYPE, - ctypeDomainLinkage, DOMAIN_LINKAGE_CREDENTIAL_TYPE, + KILT_SELF_SIGNED_PROOF_TYPE, + createCredential, + didConfigResourceFromCredentials, + verifyDidConfigResource, } from './index' -import { fundAccount, generateDid, keypairs, createCtype, assertionSigner, startContainer } from '../tests/utils' describe('Well Known Did Configuration integration test', () => { let mnemonic: string @@ -38,11 +30,10 @@ describe('Well Known Did Configuration integration test', () => { const origin = 'http://localhost:3000' let didDocument: DidDocument let didUri: DidUri - let keypair: any + let signer: SignerInterface let didConfigResource: DidConfigResource - let credential: ICredentialPresentation + let credential: DomainLinkageCredential let keyUri: DidResourceUri - let claim: IClaim beforeAll(async () => { const address = await startContainer() @@ -53,78 +44,62 @@ describe('Well Known Did Configuration integration test', () => { mnemonic = mnemonicGenerate() account = new Keyring({ type: 'ed25519' }).addFromMnemonic(mnemonic) as KiltKeyringPair await fundAccount(account.address, new BN('1000000000000000000')) - keypair = await keypairs(mnemonic) - didDocument = await generateDid(account, mnemonic) - didUri = didDocument.uri keyUri = `${didUri}${didDocument.assertionMethod![0].id}` - claim = { - cTypeHash: CType.idToHash(ctypeDomainLinkage.$id), - contents: { origin }, - owner: didUri, - } + signer = await createSigner({ id: keyUri, secretKey: mnemonicToMiniSecret(mnemonic) }) await createCtype(didUri, account, mnemonic) }, 30_000) it('generate a well known did configuration credential', async () => { - expect( - (credential = await createCredential( - await assertionSigner({ assertionMethod: keypair.assertionMethod, didDocument }), + expect((credential = await createCredential(signer, origin, didUri))).toMatchObject({ + '@context': [DID_VC_CONTEXT, DID_CONFIGURATION_CONTEXT], + credentialSubject: { + id: didUri, origin, - didUri - )) - ).toMatchObject({ - claim, - claimerSignature: { - keyUri, - signature: expect.any(String), }, - claimHashes: expect.any(Array<`0x${string}`>), - claimNonceMap: expect.any(Object), - delegationId: null, - legitimations: [], - rootHash: expect.any(String), + proof: expect.objectContaining({ type: KILT_SELF_SIGNED_PROOF_TYPE }), + type: [DEFAULT_VERIFIABLECREDENTIAL_TYPE, DOMAIN_LINKAGE_CREDENTIAL_TYPE], + issuer: didUri, + issuanceDate: expect.any(String), + expirationDate: expect.any(String), }) }, 30_000) it('fails to generate a well known did configuration credential if origin is not a URL', async () => { - await expect( - createCredential(await assertionSigner({ assertionMethod: keypair.assertion, didDocument }), 'bad origin', didUri) - ).rejects.toThrow() + await expect(createCredential(signer, 'bad origin', didUri)).rejects.toThrow() }, 30_000) it('get domain linkage presentation', async () => { - expect((didConfigResource = await didConfigResourceFromCredential(credential))).toMatchObject({ - '@context': DID_CONFIGURATION_CONTEXT, - linked_dids: [ - { - '@context': [DID_VC_CONTEXT, DID_CONFIGURATION_CONTEXT], - credentialSubject: { - id: didUri, - origin, + expect((didConfigResource = await didConfigResourceFromCredentials([credential]))).toMatchObject( + { + '@context': DID_CONFIGURATION_CONTEXT, + linked_dids: [ + { + '@context': [DID_VC_CONTEXT, DID_CONFIGURATION_CONTEXT], + credentialSubject: { + id: didUri, + origin, + }, + proof: expect.any(Object), + type: [DEFAULT_VERIFIABLECREDENTIAL_TYPE, DOMAIN_LINKAGE_CREDENTIAL_TYPE], + issuer: didUri, + issuanceDate: expect.any(String), + expirationDate: expect.any(String), }, - proof: expect.any(Object), - type: [DEFAULT_VERIFIABLECREDENTIAL_TYPE, DOMAIN_LINKAGE_CREDENTIAL_TYPE], - issuer: didUri, - issuanceDate: expect.any(String), - }, - ], - }) - }, 30_000) - - it('rejects if the domain linkage has no signature', async () => { - delete (credential as any).claimerSignature - await expect(didConfigResourceFromCredential(credential)).rejects.toThrow() + ], + } + ) }, 30_000) it('verify did configuration presentation', async () => { - await expect(verifyDidConfigResource(didConfigResource, origin, didUri)).resolves.not.toThrow() + await expect(verifyDidConfigResource(didConfigResource, origin, { expectedDid: didUri })).resolves.not.toThrow() }, 30_000) it('did not verify did configuration presentation', async () => { + // @ts-expect-error property not present o both proofs didConfigResource.linked_dids[0].proof.signature = '0x' - await expect(verifyDidConfigResource(didConfigResource, origin, didUri)).rejects.toThrow() + await expect(verifyDidConfigResource(didConfigResource, origin, { expectedDid: didUri })).rejects.toThrow() }, 30_000) }) diff --git a/yarn.lock b/yarn.lock index 0387303..9dde871 100644 --- a/yarn.lock +++ b/yarn.lock @@ -696,6 +696,32 @@ "@polkadot/util" "^12.0.0" "@polkadot/util-crypto" "^12.0.0" +"@kiltprotocol/eddsa-jcs-2022@latest-rc": + version "0.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@kiltprotocol/eddsa-jcs-2022/-/eddsa-jcs-2022-0.1.0-rc.2.tgz#a10e37d1eb90d74bcaa795726dd555c83e8bdf60" + integrity sha512-9rGwykwsrZzD0tppdp7HwtWrDb8va8oQ8x+/c2+sMHEBYBTjNTnCK/iwmJVpa2bJKmzcu/ihj0Xm711o7jyWwQ== + dependencies: + "@kiltprotocol/jcs-data-integrity-proofs-common" "^0.1.0-rc.2" + "@noble/curves" "^1.0.0" + "@scure/base" "^1.1.1" + +"@kiltprotocol/es256k-jcs-2023@latest-rc": + version "0.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@kiltprotocol/es256k-jcs-2023/-/es256k-jcs-2023-0.1.0-rc.2.tgz#cd19431dd8bd2c0d26be7c413656e8479a5907ae" + integrity sha512-C3FIt35hJ+SIrBr3lr6UffgoH8FBU7W8KKo5Tx0XoC+mi47v7WoWavkuXmVNZL+kXqIARbEixvv2sULYL0E+8A== + dependencies: + "@kiltprotocol/jcs-data-integrity-proofs-common" "^0.1.0-rc.2" + "@noble/curves" "^1.0.0" + "@scure/base" "^1.1.1" + +"@kiltprotocol/jcs-data-integrity-proofs-common@^0.1.0-rc.2": + version "0.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@kiltprotocol/jcs-data-integrity-proofs-common/-/jcs-data-integrity-proofs-common-0.1.0-rc.2.tgz#59c17827b10f290ee6ed7fe7a7dcbac7881eb704" + integrity sha512-0hMNKODX7eJYTNlwTzyivY/d9+AZa2NwV2jN5sGzt2LGzFB9iA7WBJlw7SzJbGpHNgaSysttWXfy1v62tFrA1w== + dependencies: + "@noble/hashes" "^1.3.0" + canonicalize "^2.0.0" + "@kiltprotocol/messaging@0.35.0": version "0.35.0" resolved "https://registry.yarnpkg.com/@kiltprotocol/messaging/-/messaging-0.35.0.tgz#9789e4b213df2938c3f2af8fd4d5a38d6a6a44c8" @@ -720,6 +746,15 @@ "@kiltprotocol/types" "0.35.0" "@kiltprotocol/utils" "0.35.0" +"@kiltprotocol/sr25519-jcs-2023@latest-rc": + version "0.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@kiltprotocol/sr25519-jcs-2023/-/sr25519-jcs-2023-0.1.0-rc.2.tgz#d827aa6a9e65e8ed78dfd611a35f22371b3b45cb" + integrity sha512-tAleWeyRSQ6gcs0wexgW3DaIlRvy6+7g9nwOQk+7qW4hmL3J+2iYQASEgnZlTWqF4v0EA0oJUNrrmWpzlr4jNg== + dependencies: + "@kiltprotocol/jcs-data-integrity-proofs-common" "^0.1.0-rc.2" + "@polkadot/util-crypto" "^12.0.1" + "@scure/base" "^1.1.1" + "@kiltprotocol/type-definitions@0.35.0": version "0.35.0" resolved "https://registry.yarnpkg.com/@kiltprotocol/type-definitions/-/type-definitions-0.35.0.tgz#365aa633ba0d08983068ad2f01bb2d65455d8b8c" @@ -768,17 +803,17 @@ jsonld-signatures "^5.0.0" vc-js "^0.6.4" -"@noble/curves@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e" - integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA== +"@noble/curves@^1.0.0", "@noble/curves@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" + integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== dependencies: - "@noble/hashes" "1.3.3" + "@noble/hashes" "1.4.0" -"@noble/hashes@1.3.3", "@noble/hashes@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" - integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.0", "@noble/hashes@^1.3.3": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -987,7 +1022,7 @@ rxjs "^7.8.1" tslib "^2.6.2" -"@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.0.0", "@polkadot/util-crypto@^12.3.2", "@polkadot/util-crypto@^12.6.2": +"@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.0.0", "@polkadot/util-crypto@^12.0.1", "@polkadot/util-crypto@^12.3.2", "@polkadot/util-crypto@^12.6.2": version "12.6.2" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.2.tgz#d2d51010e8e8ca88951b7d864add797dad18bbfc" integrity sha512-FEWI/dJ7wDMNN1WOzZAjQoIcCP/3vz3wvAp5QQm+lOrzOLj0iDmaIGIcBkz8HVm3ErfSe/uKP0KS4jgV/ib+Mg== @@ -1126,7 +1161,7 @@ tslib "^2.6.2" ws "^8.15.1" -"@scure/base@^1.1.5": +"@scure/base@^1.1.1", "@scure/base@^1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== @@ -1848,6 +1883,11 @@ canonicalize@^1.0.1: resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.8.tgz#24d1f1a00ed202faafd9bf8e63352cd4450c6df1" integrity sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A== +canonicalize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-2.0.0.tgz#32be2cef4446d67fd5348027a384cae28f17226a" + integrity sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"