diff --git a/packages/credential-ld/contexts/w3_2018_credentials_v1.jsonld b/packages/credential-ld/contexts/w3_2018_credentials_v1.jsonld new file mode 100644 index 000000000..26169278c --- /dev/null +++ b/packages/credential-ld/contexts/w3_2018_credentials_v1.jsonld @@ -0,0 +1,237 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "credentialSchema": { + "@id": "cred:credentialSchema", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + + "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018" + } + }, + "credentialStatus": {"@id": "cred:credentialStatus", "@type": "@id"}, + "credentialSubject": {"@id": "cred:credentialSubject", "@type": "@id"}, + "evidence": {"@id": "cred:evidence", "@type": "@id"}, + "expirationDate": {"@id": "cred:expirationDate", "@type": "xsd:dateTime"}, + "holder": {"@id": "cred:holder", "@type": "@id"}, + "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, + "issuer": {"@id": "cred:issuer", "@type": "@id"}, + "issuanceDate": {"@id": "cred:issuanceDate", "@type": "xsd:dateTime"}, + "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, + "refreshService": { + "@id": "cred:refreshService", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + + "ManualRefreshService2018": "cred:ManualRefreshService2018" + } + }, + "termsOfUse": {"@id": "cred:termsOfUse", "@type": "@id"}, + "validFrom": {"@id": "cred:validFrom", "@type": "xsd:dateTime"}, + "validUntil": {"@id": "cred:validUntil", "@type": "xsd:dateTime"} + } + }, + + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + + "holder": {"@id": "cred:holder", "@type": "@id"}, + "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, + "verifiableCredential": {"@id": "cred:verifiableCredential", "@type": "@id", "@container": "@graph"} + } + }, + + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "EcdsaSecp256r1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "RsaSignature2018": { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@version": 1.1, + "@protected": true, + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "proof": {"@id": "https://w3id.org/security#proof", "@type": "@id", "@container": "@graph"} + } +} \ No newline at end of file diff --git a/packages/credential-ld/src/__tests__/context.loader.test.ts b/packages/credential-ld/src/__tests__/context.loader.test.ts new file mode 100644 index 000000000..3e3af6535 --- /dev/null +++ b/packages/credential-ld/src/__tests__/context.loader.test.ts @@ -0,0 +1,39 @@ +import { ContextDoc } from '../types' +import { LdContextLoader } from '../ld-context-loader' +import { LdDefaultContexts } from '../ld-default-contexts' + +describe('credential-ld context loader', () => { + const customContext: Record = { + 'https://example.com/custom/context': { + '@context': { + '@version': 1.1, + id: '@id', + type: '@type', + nothing: 'https://example.com/nothing', + }, + }, + } + + it('loads custom context from record', async () => { + expect.assertions(2) + const loader = new LdContextLoader({ contextsPaths: [customContext] }) + expect(loader.has('https://example.com/custom/context')).toBe(true) + await expect(loader.get('https://example.com/custom/context')).resolves.toEqual({ + '@context': { + '@version': 1.1, + id: '@id', + type: '@type', + nothing: 'https://example.com/nothing', + }, + }) + }) + + it('loads context from default map', async () => { + expect.assertions(2) + const loader = new LdContextLoader({ contextsPaths: [LdDefaultContexts] }) + expect(loader.has('https://www.w3.org/2018/credentials/v1')).toBe(true) + + const credsContext = await loader.get('https://www.w3.org/2018/credentials/v1') + expect(credsContext['@context']).toBeDefined() + }) +}) diff --git a/packages/credential-ld/src/__tests__/issue-verify-flow.test.ts b/packages/credential-ld/src/__tests__/issue-verify-flow.test.ts new file mode 100644 index 000000000..3036a1b08 --- /dev/null +++ b/packages/credential-ld/src/__tests__/issue-verify-flow.test.ts @@ -0,0 +1,84 @@ +import { + createAgent, + CredentialPayload, + IDIDManager, + IIdentifier, + IKeyManager, + IResolver, + TAgent, +} from '../../../core/src' +import { CredentialIssuer, ICredentialIssuer } from '../../../credential-w3c/src' +import { DIDManager, MemoryDIDStore } from '../../../did-manager/src' +import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../../../key-manager/src' +import { KeyManagementSystem } from '../../../kms-local/src' +import { getDidKeyResolver, KeyDIDProvider } from '../../../did-provider-key/src' +import { DIDResolverPlugin } from '../../../did-resolver/src' +import { ContextDoc } from '../types' +import { CredentialIssuerLD } from '../action-handler' +import { LdDefaultContexts } from '../ld-default-contexts' +import { VeramoEd25519Signature2018 } from '../suites/Ed25519Signature2018' +import { Resolver } from 'did-resolver' + +const customContext: Record = { + 'custom:example.context': { + '@context': { + nothing: 'custom:example.context#blank', + }, + }, +} + +describe('credential-LD full flow', () => { + let didKeyIdentifier: IIdentifier + let agent: TAgent + + beforeAll(async () => { + agent = createAgent({ + plugins: [ + new KeyManager({ + store: new MemoryKeyStore(), + kms: { + local: new KeyManagementSystem(new MemoryPrivateKeyStore()), + }, + }), + new DIDManager({ + providers: { + 'did:key': new KeyDIDProvider({ defaultKms: 'local' }), + }, + store: new MemoryDIDStore(), + defaultProvider: 'did:key', + }), + new DIDResolverPlugin({ + resolver: new Resolver({ ...getDidKeyResolver() }), + }), + new CredentialIssuer(), + new CredentialIssuerLD({ + contextMaps: [LdDefaultContexts, customContext], + suites: [new VeramoEd25519Signature2018()], + }), + ], + }) + didKeyIdentifier = await agent.didManagerCreate() + }) + + it('works with Ed25519Signature2018', async () => { + const credential: CredentialPayload = { + issuer: didKeyIdentifier.did, + '@context': ['custom:example.context'], + credentialSubject: { + nothing: 'else matters', + }, + } + const verifiableCredential = await agent.createVerifiableCredential({ + credential, + proofFormat: 'lds', + }) + + expect(verifiableCredential).toBeDefined() + + const verified = await agent.verifyCredential({ + credential: verifiableCredential, + }) + + expect(verified).toBe(true) + }) +}) diff --git a/packages/credential-ld/src/ld-context-loader.ts b/packages/credential-ld/src/ld-context-loader.ts index 09090fa2b..296deed5e 100644 --- a/packages/credential-ld/src/ld-context-loader.ts +++ b/packages/credential-ld/src/ld-context-loader.ts @@ -1,19 +1,20 @@ /** - * The LdContextLoader is initialized with a List of Map + * The LdContextLoader is initialized with a List of Map> * that it unifies into a single Map to provide to the documentLoader within * the w3c credential module. */ -import { OrPromise, RecordLike } from '@veramo/utils' +import { isIterable, OrPromise, RecordLike } from '@veramo/utils' import { ContextDoc } from './types' export class LdContextLoader { - private contexts: Record> + private readonly contexts: Record> constructor(options: { contextsPaths: RecordLike>[] }) { this.contexts = {} - // generate-plugin-schema is failing unless we use the cast to `any[]` - Array.from(options.contextsPaths as any[], (mapItem) => { - for (const [key, value] of mapItem) { + Array.from(options.contextsPaths, (mapItem) => { + const map = isIterable(mapItem) ? mapItem : Object.entries(mapItem) + // generate-plugin-schema is failing unless we use the cast to `any[]` + for (const [key, value] of map as any[]) { this.contexts[key] = value } }) diff --git a/packages/credential-ld/src/ld-default-contexts.ts b/packages/credential-ld/src/ld-default-contexts.ts index 972669e3d..aa2dd3c55 100644 --- a/packages/credential-ld/src/ld-default-contexts.ts +++ b/packages/credential-ld/src/ld-default-contexts.ts @@ -1,16 +1,25 @@ import * as fs from 'fs' import * as path from 'path' +import { ContextDoc } from './types' -function _read(_path: string) { - return JSON.parse(fs.readFileSync(path.join(__dirname, '../contexts', _path), { encoding: 'utf8' })) +async function _read(_path: string): Promise { + const contextDefinition = await fs.promises.readFile(path.join(__dirname, '../contexts', _path), { + encoding: 'utf8', + }) + return JSON.parse(contextDefinition) } +/** + * Provides a hardcoded map of common context definitions + */ export const LdDefaultContexts = new Map([ + ['https://www.w3.org/2018/credentials/v1', _read('w3_2018_credentials_v1.jsonld')], + ['https://www.w3.org/ns/did/v1', _read('security_context_v1.jsonld')], + ['https://w3id.org/did/v0.11', _read('did_v0.11.jsonld')], + ['https://veramo.io/contexts/socialmedia/v1', _read('socialmedia-v1.jsonld')], ['https://veramo.io/contexts/kyc/v1', _read('kyc-v1.jsonld')], ['https://veramo.io/contexts/profile/v1', _read('profile-v1.jsonld')], - ['https://www.w3.org/ns/did/v1', _read('security_context_v1.jsonld')], - ['https://w3id.org/did/v0.11', _read('did_v0.11.jsonld')], ['https://ns.did.ai/transmute/v1', _read('transmute_v1.jsonld')], [ 'https://identity.foundation/EcdsaSecp256k1RecoverySignature2020/lds-ecdsa-secp256k1-recovery2020-0.0.jsonld', diff --git a/packages/credential-w3c/src/action-handler.ts b/packages/credential-w3c/src/action-handler.ts index 7ee3fb6f8..6493274a1 100644 --- a/packages/credential-w3c/src/action-handler.ts +++ b/packages/credential-w3c/src/action-handler.ts @@ -518,7 +518,7 @@ function wrapSigner( key: IKey, algorithm?: string, ) { - return async (data: string | Uint8Array) => { + return async (data: string | Uint8Array): Promise => { const result = await context.agent.keyManagerSign({ keyRef: key.kid, data: data, algorithm }) return result } diff --git a/packages/utils/src/type-utils.ts b/packages/utils/src/type-utils.ts index 8abc9eab5..fe9595e4d 100644 --- a/packages/utils/src/type-utils.ts +++ b/packages/utils/src/type-utils.ts @@ -1,7 +1,29 @@ +/** + * Checks if a variable is defined and not null + * @param arg + * + * @beta + */ export function isDefined(arg: T): arg is Exclude { return arg !== null && typeof arg !== 'undefined' } +/** + * Transforms an item or an array of items into an array of items + * @param arg + * + * @beta + */ export function asArray(arg?: T | T[] | null): T[] { return isDefined(arg) ? (Array.isArray(arg) ? arg : [arg]) : [] } + +/** + * Checks if an object is iterable (can be used for `for..of`) + * @param obj + * + * @beta + */ +export function isIterable(obj: any): obj is Iterable { + return obj != null && typeof obj[Symbol.iterator] === 'function' +}