diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5c2b742 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +./test/__fixtures__/* \ No newline at end of file diff --git a/README.md b/README.md index 8f77e0f..08fe0ef 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,342 @@ npm i @transmute/verifiable-credentials@latest --save import * as transmute from "@transmute/verifiable-credentials"; ``` +### Generating Keys + +```ts +const privateKey = await transmute.key.generate({ + alg, + type: "application/jwk+json", +}); +// console.log(new TextDecoder().decode(privateKey)) +// { +// "kid": "xSgm4GQOT_ZyYFApew0GnRvPWt70omVJV9XVB5tsmN8", +// "alg": "ES256", +// "kty": "EC", +// "crv": "P-256", +// "x": "XRkZngz2KSCrLdXKGCRNyDzBgsovioZIqMWnF42nmdg", +// "y": "H2t6Xxdg8p8Cqn2-hsuWnXYj0192He4zTZghAxNXllo", +// ... +// } +const publicKey = await transmute.key.publicFromPrivate({ + type: "application/jwk+json", + content: privateKey, +}); +// console.log(new TextDecoder().decode(publicKey)) +// { +// "kid": "xSgm4GQOT_ZyYFApew0GnRvPWt70omVJV9XVB5tsmN8", +// "alg": "ES256", +// "kty": "EC", +// "crv": "P-256", +// "x": "XRkZngz2KSCrLdXKGCRNyDzBgsovioZIqMWnF42nmdg", +// "y": "H2t6Xxdg8p8Cqn2-hsuWnXYj0192He4zTZghAxNXllo", +// } +``` + +### Issuing Credentials + +```ts +const alg = `ES256`; +const statusListSize = 131072; +const revocationIndex = 94567; +const suspensionIndex = 23452; + +const issuer = `did:example:123`; +const baseURL = `https://vendor.example/api`; +const issued = await transmute + .issuer({ + alg, + type: "application/vc+ld+json+jwt", + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign(bytes) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign( + await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey, + }) + ); + return transmute.text.encoder.encode(jws); + }, + }, + }) + .issue({ + claimset: transmute.text.encoder.encode(` +"@context": + - https://www.w3.org/ns/credentials/v2 + - https://www.w3.org/ns/credentials/examples/v2 + +id: ${baseURL}/credentials/3732 +type: + - VerifiableCredential + - ExampleDegreeCredential +issuer: + id: ${issuer} + name: "Example University" +validFrom: ${moment().toISOString()} +credentialSchema: + id: ${baseURL}/schemas/product-passport + type: JsonSchema +credentialStatus: + - id: ${baseURL}/credentials/status/3#${revocationIndex} + type: BitstringStatusListEntry + statusPurpose: revocation + statusListIndex: "${revocationIndex}" + statusListCredential: "${baseURL}/credentials/status/3" + - id: ${baseURL}/credentials/status/4#${suspensionIndex} + type: BitstringStatusListEntry + statusPurpose: suspension + statusListIndex: "${suspensionIndex}" + statusListCredential: "${baseURL}/credentials/status/4" +credentialSubject: + id: did:example:ebfeb1f712ebc6f1c276e12ec21 + degree: + type: ExampleBachelorDegree + subtype: Bachelor of Science and Arts +`), + }); +// console.log(new TextDecoder().decode(issued)) +// eyJraWQiOiJkaWQ6ZXhhbXBsZToxMjMja2V5LTQyIiwiYWxnIjoiRVMyNTYifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3ZlbmRvci5leGFtcGxlL2FwaS9jb250ZXh0L3YyIl0sImlkIjoiaHR0cHM6Ly92ZW5kb3IuZXhhbXBsZS9hcGkvY3JlZGVudGlhbHMvMzczMiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJFeGFtcGxlRGVncmVlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5In0sInZhbGlkRnJvbSI6IjIwMjQtMDQtMjRUMjI6MjM6MDIuODU2WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL3ZlbmRvci5leGFtcGxlL2FwaS9zY2hlbWFzL3Byb2R1Y3QtcGFzc3BvcnQiLCJ0eXBlIjoiSnNvblNjaGVtYSJ9LCJjcmVkZW50aWFsU3RhdHVzIjpbeyJpZCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy8zIzk0NTY3IiwidHlwZSI6IkJpdHN0cmluZ1N0YXR1c0xpc3RFbnRyeSIsInN0YXR1c1B1cnBvc2UiOiJyZXZvY2F0aW9uIiwic3RhdHVzTGlzdEluZGV4IjoiOTQ1NjciLCJzdGF0dXNMaXN0Q3JlZGVudGlhbCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy8zIn0seyJpZCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy80IzIzNDUyIiwidHlwZSI6IkJpdHN0cmluZ1N0YXR1c0xpc3RFbnRyeSIsInN0YXR1c1B1cnBvc2UiOiJzdXNwZW5zaW9uIiwic3RhdHVzTGlzdEluZGV4IjoiMjM0NTIiLCJzdGF0dXNMaXN0Q3JlZGVudGlhbCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy80In1dLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImRlZ3JlZSI6eyJ0eXBlIjoiRXhhbXBsZUJhY2hlbG9yRGVncmVlIiwic3VidHlwZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.xHjfiUwx61qmoVMGLrHT8FI-ZYUHXQy4B6oF0Cb5EOTYYPXdwjW9sa1l5aa008xvsFvrcNats9TywmN2nNKz6A +``` + +### Validating Credentials + +```ts +const validated = await transmute + .validator({ + resolver: { + resolve: async ({ id, type, content }) => { + // Resolve external resources according to verifier policy + // In this case, we return inline exampes... + if (id === `${baseURL}/schemas/product-passport`) { + return { + type: `application/schema+json`, + content: transmute.text.encoder.encode(` +{ + "$id": "${baseURL}/schemas/product-passport", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Example JSON Schema", + "description": "This is a test schema", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } +} + `), + }; + } + if (id === `${baseURL}/credentials/status/3`) { + return { + type: `application/vc+ld+json+jwt`, + content: await transmute + .issuer({ + alg: "ES384", + type: "application/vc+ld+json+cose", + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign(bytes) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign( + await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey, + }) + ); + return transmute.text.encoder.encode(jws); + }, + }, + }) + .issue({ + claimset: transmute.text.encoder.encode( + ` +"@context": + - https://www.w3.org/ns/credentials/v2 +id: ${baseURL}/status/3#list +type: + - VerifiableCredential + - BitstringStatusListCredential +issuer: + id: ${issuer} +validFrom: ${moment().toISOString()} +credentialSubject: + id: ${baseURL}/status/3#list#list + type: BitstringStatusList + statusPurpose: revocation + encodedList: ${await transmute.status + .bs(statusListSize) + .set(revocationIndex, false) + .encode()} +`.trim() + ), + }), + }; + } + if (id === `${baseURL}/credentials/status/4`) { + return { + type: `application/vc+ld+json+jwt`, + content: await transmute + .issuer({ + alg: "ES384", + type: "application/vc+ld+json+cose", + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign(bytes) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign( + await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey, + }) + ); + return transmute.text.encoder.encode(jws); + }, + }, + }) + .issue({ + claimset: transmute.text.encoder.encode( + ` +"@context": + - https://www.w3.org/ns/credentials/v2 +id: ${baseURL}/status/4#list +type: + - VerifiableCredential + - BitstringStatusListCredential +issuer: + id: ${issuer} +validFrom: ${moment().toISOString()} +credentialSubject: + id: ${baseURL}/status/4#list#list + type: BitstringStatusList + statusPurpose: suspension + encodedList: ${await transmute.status + .bs(statusListSize) + .set(suspensionIndex, false) + .encode()} +`.trim() + ), + }), + }; + } + if (content != undefined && type === `application/vc+ld+json+jwt`) { + const { kid } = jose.decodeProtectedHeader( + transmute.text.decoder.decode(content) + ); + // lookup public key by kid on a trusted resolver + if (kid === `did:example:123#key-42`) { + return { + type: "application/jwk+json", + content: publicKey, + }; + } + } + throw new Error("Resolver option not supported."); + }, + }, + }) + .validate({ + type: "application/vc+ld+json+jwt", + content: issued, + }); + +// expect(validated.valid).toBe(true) +// expect(validated.schema[`${baseURL}/schemas/product-passport`].valid).toBe(true) +// expect(validated.status[`${baseURL}/credentials/status/3#${revocationIndex}`].valid).toBe(false) +// expect(validated.status[`${baseURL}/credentials/status/4#${suspensionIndex}`].valid).toBe(false) +``` + +### Issuing Presentations + +```ts +const presentation = await transmute + .holder({ + alg, + type: "application/vp+ld+json+jwt", + }) + .issue({ + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign(bytes) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign( + await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey, + }) + ); + return transmute.text.encoder.encode(jws); + }, + }, + presentation: { + "@context": ["https://www.w3.org/ns/credentials/v2"], + type: ["VerifiablePresentation"], + holder: `${baseURL}/holders/565049`, + // this part is built from disclosures without key binding below. + // "verifiableCredential": [{ + // "@context": "https://www.w3.org/ns/credentials/v2", + // "id": "data:application/vc+ld+json+sd-jwt;QzVjV...RMjU", + // "type": "EnvelopedVerifiableCredential" + // }] + }, + disclosures: [ + { + type: `application/vc+ld+json+jwt`, + credential: issued, + }, + ], + }); +``` + +### Validating Presentations + ```ts -// todo... +const validation = await transmute + .validator({ + resolver: { + resolve: async ({ type, content }) => { + // Resolve external resources according to verifier policy + // In this case, we return inline exampes... + if (content != undefined && type === `application/vp+ld+json+jwt`) { + const { kid } = jose.decodeProtectedHeader( + transmute.text.decoder.decode(content) + ); + // lookup public key on a resolver + if (kid === `did:example:123#key-42`) { + return { + type: "application/jwk+json", + content: publicKey, + }; + } + } + throw new Error("Resolver option not supported."); + }, + }, + }) + .validate({ + type: `application/vp+ld+json+jwt`, + content: presentation, + }); +// { +// "valid": true, +// "content": { +// "@context": [ +// "https://www.w3.org/ns/credentials/v2" +// ], +// "type": [ +// "VerifiablePresentation" +// ], +// "holder": "https://vendor.example/api/holders/565049", +// "verifiableCredential": [ +// { +// "@context": "https://www.w3.org/ns/credentials/v2", +// "id": "data:application/vc+ld+json+jwt;eyJraWQiOiJkaWQ6ZX... ``` ## Develop diff --git a/package-lock.json b/package-lock.json index 9fceca8..b0f0b38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@transmute/verifiable-credentials", - "version": "0.1.6", + "version": "0.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@transmute/verifiable-credentials", - "version": "0.1.6", + "version": "0.2.1", "license": "Apache-2.0", "dependencies": { "@transmute/cose": "^0.1.1", diff --git a/package.json b/package.json index 7a67d9a..31d0ae9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@transmute/verifiable-credentials", - "version": "0.1.6", + "version": "0.2.1", "description": "An opinionated typescript library for w3c verifiable credentials.", "main": "./dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/cr1/key/generate.ts b/src/cr1/key/generate.ts index 65e60c0..4971399 100644 --- a/src/cr1/key/generate.ts +++ b/src/cr1/key/generate.ts @@ -17,15 +17,15 @@ export type RequestGenerateCredentialKey = { } -export const generate = async (req: RequestGenerateCredentialKey): Promise => { +export const generate = async (req: RequestGenerateCredentialKey): Promise => { if (req.type === 'application/jwk+json') { const obj = await cose.key.generate(req.alg, 'application/jwk+json') const text = JSON.stringify(obj, null, 2) - return encoder.encode(text) + return encoder.encode(text) as T } if (req.type === 'application/cose-key') { const result = await cose.key.generate(req.alg, 'application/cose-key') - return new Uint8Array(cose.cbor.encode(result)) + return new Uint8Array(cose.cbor.encode(result)) as T } if (req.type === 'application/pkcs8') { const result = await cose.certificate.root({ @@ -35,7 +35,7 @@ export const generate = async (req: RequestGenerateCredentialKey): Promise status: Record warnings: ConformanceWarningMessage[] +} + + +export type TraceablePresentationValidationResult = ValidationResult & { + content: VerifiablePresentationWithHolderObject & VerifiablePresentationOfEnveloped } \ No newline at end of file diff --git a/src/cr1/validator/ajv.ts b/src/cr1/validator/ajv.ts new file mode 100644 index 0000000..87200bc --- /dev/null +++ b/src/cr1/validator/ajv.ts @@ -0,0 +1,5 @@ +import Ajv from 'ajv/dist/2020' + +export const ajv = new Ajv({ + strict: false, +}) diff --git a/src/cr1/validator/index.ts b/src/cr1/validator/index.ts index cf5f13d..0ab52cf 100644 --- a/src/cr1/validator/index.ts +++ b/src/cr1/validator/index.ts @@ -1,4 +1,3 @@ -import Ajv from 'ajv' import { RequestValidator, @@ -8,7 +7,8 @@ import { BitstringStatusListCredential, ValidationResult, VerifiableCredential, - JsonSchemaError + JsonSchemaError, + TraceablePresentationValidationResult } from "../types" import { verifier } from "../verifier" @@ -19,13 +19,11 @@ import { bs } from '../../cr1/status-list' import { conformance } from './w3c' -const ajv = new Ajv({ - strict: false, -}) +import { ajv } from "./ajv" export const validator = ({ resolver }: RequestValidator) => { return { - validate: async ({ type, content }: SecuredContentType) => { + validate: async ({ type, content }: SecuredContentType) => { const verified = await verifier({ resolver }).verify({ type, content }) const validation: ValidationResult = { valid: true, @@ -85,7 +83,7 @@ export const validator = ({ resolver }: RequestValidator) => { } } } - return conformance(validation) + return conformance(validation) as T } } } \ No newline at end of file diff --git a/src/cr1/__fixtures__/broken_context.yml b/test/__fixtures__/broken_context.yml similarity index 100% rename from src/cr1/__fixtures__/broken_context.yml rename to test/__fixtures__/broken_context.yml diff --git a/src/cr1/__fixtures__/claimset_0.yml b/test/__fixtures__/claimset_0.yml similarity index 100% rename from src/cr1/__fixtures__/claimset_0.yml rename to test/__fixtures__/claimset_0.yml diff --git a/src/cr1/__fixtures__/claimset_1.yml b/test/__fixtures__/claimset_1.yml similarity index 100% rename from src/cr1/__fixtures__/claimset_1.yml rename to test/__fixtures__/claimset_1.yml diff --git a/src/cr1/__fixtures__/claimset_2.yml b/test/__fixtures__/claimset_2.yml similarity index 100% rename from src/cr1/__fixtures__/claimset_2.yml rename to test/__fixtures__/claimset_2.yml diff --git a/src/cr1/__fixtures__/claimset_disclosable_0.yml b/test/__fixtures__/claimset_disclosable_0.yml similarity index 100% rename from src/cr1/__fixtures__/claimset_disclosable_0.yml rename to test/__fixtures__/claimset_disclosable_0.yml diff --git a/src/cr1/__fixtures__/claimset_disclosable_0_disclosure_0.yml b/test/__fixtures__/claimset_disclosable_0_disclosure_0.yml similarity index 100% rename from src/cr1/__fixtures__/claimset_disclosable_0_disclosure_0.yml rename to test/__fixtures__/claimset_disclosable_0_disclosure_0.yml diff --git a/src/cr1/__fixtures__/claimset_disclosable_1.yml b/test/__fixtures__/claimset_disclosable_1.yml similarity index 100% rename from src/cr1/__fixtures__/claimset_disclosable_1.yml rename to test/__fixtures__/claimset_disclosable_1.yml diff --git a/src/cr1/__fixtures__/holder_0_private_key.cbor b/test/__fixtures__/holder_0_private_key.cbor similarity index 100% rename from src/cr1/__fixtures__/holder_0_private_key.cbor rename to test/__fixtures__/holder_0_private_key.cbor diff --git a/src/cr1/__fixtures__/holder_0_public_key.cbor b/test/__fixtures__/holder_0_public_key.cbor similarity index 100% rename from src/cr1/__fixtures__/holder_0_public_key.cbor rename to test/__fixtures__/holder_0_public_key.cbor diff --git a/src/cr1/__fixtures__/index.ts b/test/__fixtures__/index.ts similarity index 60% rename from src/cr1/__fixtures__/index.ts rename to test/__fixtures__/index.ts index 495983e..61f5abb 100644 --- a/src/cr1/__fixtures__/index.ts +++ b/test/__fixtures__/index.ts @@ -1,51 +1,51 @@ import fs from 'fs' import * as jose from "jose"; -import { importKeyLike } from '../key/importKeyLike' +import { importKeyLike } from '../../src/cr1/key/importKeyLike' -import { encoder, decoder } from '../text'; +import { encoder, decoder } from '../../src/cr1/text'; -import { issuer } from '../credential' -import { validator } from '../validator' +import { issuer } from '../../src/cr1/credential' +import { validator } from '../../src/cr1/validator' // keys export const issuer_0_key_type = 'application/jwk+json' -export const issuer_0_private_key = fs.readFileSync('./src/cr1/__fixtures__/issuer_0_private_key.json') -export const issuer_0_public_key = fs.readFileSync('./src/cr1/__fixtures__/issuer_0_public_key.json') +export const issuer_0_private_key = fs.readFileSync('./test/__fixtures__/issuer_0_private_key.json') +export const issuer_0_public_key = fs.readFileSync('./test/__fixtures__/issuer_0_public_key.json') export const holder_0_key_type = 'application/cose-key' -export const holder_0_private_key = fs.readFileSync('./src/cr1/__fixtures__/holder_0_private_key.cbor') -export const holder_0_public_key = fs.readFileSync('./src/cr1/__fixtures__/holder_0_public_key.cbor') +export const holder_0_private_key = fs.readFileSync('./test/__fixtures__/holder_0_private_key.cbor') +export const holder_0_public_key = fs.readFileSync('./test/__fixtures__/holder_0_public_key.cbor') // vc -export const claimset_0 = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/claimset_0.yml')) +export const claimset_0 = new Uint8Array(fs.readFileSync('./test/__fixtures__/claimset_0.yml')) // vp -export const claimset_1 = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/claimset_1.yml')) +export const claimset_1 = new Uint8Array(fs.readFileSync('./test/__fixtures__/claimset_1.yml')) // sd claims without key binding -export const claimset_disclosable_0 = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/claimset_disclosable_0.yml')) +export const claimset_disclosable_0 = new Uint8Array(fs.readFileSync('./test/__fixtures__/claimset_disclosable_0.yml')) -export const claimset_disclosable_0_disclosure = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/claimset_disclosable_0_disclosure_0.yml')) +export const claimset_disclosable_0_disclosure = new Uint8Array(fs.readFileSync('./test/__fixtures__/claimset_disclosable_0_disclosure_0.yml')) // sd claims with key binding -export const claimset_disclosable_1 = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/claimset_disclosable_1.yml')) +export const claimset_disclosable_1 = new Uint8Array(fs.readFileSync('./test/__fixtures__/claimset_disclosable_1.yml')) // a credential with a schema -export const claimset_2 = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/claimset_2.yml')) +export const claimset_2 = new Uint8Array(fs.readFileSync('./test/__fixtures__/claimset_2.yml')) // data model conformance examples -export const minimal_credential = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/minimal_credential.yml')) +export const minimal_credential = new Uint8Array(fs.readFileSync('./test/__fixtures__/minimal_credential.yml')) -export const minimal_credential_with_dids = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/minimal_credential_with_dids.yml')) +export const minimal_credential_with_dids = new Uint8Array(fs.readFileSync('./test/__fixtures__/minimal_credential_with_dids.yml')) -export const minimal_credential_with_bad_urls = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/minimal_credential_with_bad_urls.yml')) +export const minimal_credential_with_bad_urls = new Uint8Array(fs.readFileSync('./test/__fixtures__/minimal_credential_with_bad_urls.yml')) -export const broken_context = new Uint8Array(fs.readFileSync('./src/cr1/__fixtures__/broken_context.yml')) +export const broken_context = new Uint8Array(fs.readFileSync('./test/__fixtures__/broken_context.yml')) const jws = { diff --git a/src/cr1/__fixtures__/issuer_0_private_key.json b/test/__fixtures__/issuer_0_private_key.json similarity index 100% rename from src/cr1/__fixtures__/issuer_0_private_key.json rename to test/__fixtures__/issuer_0_private_key.json diff --git a/src/cr1/__fixtures__/issuer_0_public_key.json b/test/__fixtures__/issuer_0_public_key.json similarity index 100% rename from src/cr1/__fixtures__/issuer_0_public_key.json rename to test/__fixtures__/issuer_0_public_key.json diff --git a/src/cr1/__fixtures__/minimal_credential.yml b/test/__fixtures__/minimal_credential.yml similarity index 100% rename from src/cr1/__fixtures__/minimal_credential.yml rename to test/__fixtures__/minimal_credential.yml diff --git a/src/cr1/__fixtures__/minimal_credential_with_bad_urls.yml b/test/__fixtures__/minimal_credential_with_bad_urls.yml similarity index 100% rename from src/cr1/__fixtures__/minimal_credential_with_bad_urls.yml rename to test/__fixtures__/minimal_credential_with_bad_urls.yml diff --git a/src/cr1/__fixtures__/minimal_credential_with_dids.yml b/test/__fixtures__/minimal_credential_with_dids.yml similarity index 100% rename from src/cr1/__fixtures__/minimal_credential_with_dids.yml rename to test/__fixtures__/minimal_credential_with_dids.yml diff --git a/test/jwt-product-passports/integration.test.ts b/test/jwt-product-passports/integration.test.ts new file mode 100644 index 0000000..5e429c9 --- /dev/null +++ b/test/jwt-product-passports/integration.test.ts @@ -0,0 +1,287 @@ + +import * as jose from 'jose' +import moment from 'moment' + +import * as transmute from '../../src' + +const alg = `ES256` +const statusListSize = 131072 +const revocationIndex = 94567 +const suspensionIndex = 23452 + +const issuer = `did:example:123` +const baseURL = `https://vendor.example/api` + +describe('product passport', () => { + it('issue application/vc+ld+json+jwt using application/jwk+json', async () => { + const privateKey = await transmute.key.generate({ + alg, + type: 'application/jwk+json' + }) + + expect(transmute.text.decoder.decode(privateKey).startsWith(`{ + "kid"`)).toBe(true) + const publicKey = await transmute.key.publicFromPrivate({ + type: 'application/jwk+json', + content: privateKey + }) + const issued = await transmute + .issuer({ + alg, + type: 'application/vc+ld+json+jwt', + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign( + bytes + ) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign(await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey + })) + return transmute.text.encoder.encode(jws) + } + } + }) + .issue({ + claimset: transmute.text.encoder.encode(` +"@context": + - https://www.w3.org/ns/credentials/v2 + - ${baseURL}/context/v2 + +id: ${baseURL}/credentials/3732 +type: + - VerifiableCredential + - ExampleDegreeCredential +issuer: + id: ${issuer} + name: "Example University" +validFrom: ${moment().toISOString()} +credentialSchema: + id: ${baseURL}/schemas/product-passport + type: JsonSchema +credentialStatus: + - id: ${baseURL}/credentials/status/3#${revocationIndex} + type: BitstringStatusListEntry + statusPurpose: revocation + statusListIndex: "${revocationIndex}" + statusListCredential: "${baseURL}/credentials/status/3" + - id: ${baseURL}/credentials/status/4#${suspensionIndex} + type: BitstringStatusListEntry + statusPurpose: suspension + statusListIndex: "${suspensionIndex}" + statusListCredential: "${baseURL}/credentials/status/4" +credentialSubject: + id: did:example:ebfeb1f712ebc6f1c276e12ec21 + degree: + type: ExampleBachelorDegree + subtype: Bachelor of Science and Arts +`), + }) + + + + const validated = await transmute.validator({ + resolver: { + resolve: async ({ id, type, content }) => { + // Resolve external resources according to verifier policy + // In this case, we return inline exampes... + if (id === `${baseURL}/schemas/product-passport`) { + return { + type: `application/schema+json`, + content: transmute.text.encoder.encode(` +{ + "$id": "${baseURL}/schemas/product-passport", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Example JSON Schema", + "description": "This is a test schema", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } +} + `) + } + } + if (id === `${baseURL}/credentials/status/3`) { + return { + type: `application/vc+ld+json+jwt`, + content: await transmute + .issuer({ + alg: 'ES384', + type: 'application/vc+ld+json+cose', + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign( + bytes + ) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign(await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey + })) + return transmute.text.encoder.encode(jws) + } + } + }) + .issue({ + claimset: transmute.text.encoder.encode( + ` +"@context": + - https://www.w3.org/ns/credentials/v2 +id: ${baseURL}/status/3#list +type: + - VerifiableCredential + - BitstringStatusListCredential +issuer: + id: ${issuer} +validFrom: ${moment().toISOString()} +credentialSubject: + id: ${baseURL}/status/3#list#list + type: BitstringStatusList + statusPurpose: revocation + encodedList: ${await transmute.status.bs(statusListSize).set(revocationIndex, false).encode()} +`.trim() + ) + }) + } + } + if (id === `${baseURL}/credentials/status/4`) { + return { + type: `application/vc+ld+json+jwt`, + content: await transmute + .issuer({ + alg: 'ES384', + type: 'application/vc+ld+json+cose', + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign( + bytes + ) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign(await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey + })) + return transmute.text.encoder.encode(jws) + } + } + }) + .issue({ + claimset: transmute.text.encoder.encode( + ` +"@context": + - https://www.w3.org/ns/credentials/v2 +id: ${baseURL}/status/4#list +type: + - VerifiableCredential + - BitstringStatusListCredential +issuer: + id: ${issuer} +validFrom: ${moment().toISOString()} +credentialSubject: + id: ${baseURL}/status/4#list#list + type: BitstringStatusList + statusPurpose: suspension + encodedList: ${await transmute.status.bs(statusListSize).set(suspensionIndex, false).encode()} +`.trim() + ) + }) + } + } + if (content != undefined && type === `application/vc+ld+json+jwt`) { + const { kid } = jose.decodeProtectedHeader(transmute.text.decoder.decode(content)) + // lookup public key on a resolver + if (kid === `did:example:123#key-42`) { + return { + type: "application/jwk+json", + content: publicKey + } + } + } + throw new Error('Resolver option not supported.') + } + } + }).validate({ + type: 'application/vc+ld+json+jwt', + content: issued, + }) + expect(validated.valid).toBe(true) + expect(validated.schema[`${baseURL}/schemas/product-passport`].valid).toBe(true) + expect(validated.status[`${baseURL}/credentials/status/3#${revocationIndex}`].valid).toBe(false) + expect(validated.status[`${baseURL}/credentials/status/4#${suspensionIndex}`].valid).toBe(false) + + + const vp = await transmute + .holder({ + alg, + type: 'application/vp+ld+json+jwt', + }) + .issue({ + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign( + bytes + ) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign(await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey + })) + return transmute.text.encoder.encode(jws) + } + }, + presentation: { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + ], + "type": ["VerifiablePresentation"], + holder: `${baseURL}/holders/565049`, + // this part is built from disclosures without key binding below. + // "verifiableCredential": [{ + // "@context": "https://www.w3.org/ns/credentials/v2", + // "id": "data:application/vc+ld+json+sd-jwt;QzVjV...RMjU", + // "type": "EnvelopedVerifiableCredential" + // }] + }, + disclosures: [ + { + type: `application/vc+ld+json+jwt`, + credential: issued + } + ] + }) + const presentation = await transmute.validator({ + resolver: { + resolve: async ({ type, content }) => { + // Resolve external resources according to verifier policy + // In this case, we return inline exampes... + if (content != undefined && type === `application/vp+ld+json+jwt`) { + const { kid } = jose.decodeProtectedHeader(transmute.text.decoder.decode(content)) + // lookup public key on a resolver + if (kid === `did:example:123#key-42`) { + return { + type: "application/jwk+json", + content: publicKey + } + } + } + throw new Error('Resolver option not supported.') + } + } + }) + .validate({ + type: `application/vp+ld+json+jwt`, + content: vp + }) + expect(presentation.content.holder).toBe('https://vendor.example/api/holders/565049') + expect(presentation.content.verifiableCredential[0].id.startsWith('data:application/vc+ld+json+jwt;')).toBe(true) + }) +}) diff --git a/test/w3c-cr-1/0-keys.test.ts b/test/w3c-cr-1/0-keys.test.ts index 98ad300..f44fab6 100644 --- a/test/w3c-cr-1/0-keys.test.ts +++ b/test/w3c-cr-1/0-keys.test.ts @@ -39,8 +39,8 @@ describe.skip('key generation', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...publicKeyJwk } = jwk const publicKeyContent = encoder.encode(JSON.stringify(publicKeyJwk, null, 2)) - fs.writeFileSync('./src/cr1/__fixtures__/issuer_0_private_key.json', k1) - fs.writeFileSync('./src/cr1/__fixtures__/issuer_0_public_key.json', publicKeyContent) + fs.writeFileSync('./test/__fixtures__/issuer_0_private_key.json', k1) + fs.writeFileSync('./test/__fixtures__/issuer_0_public_key.json', publicKeyContent) }) it('application/cose-key', async () => { const k1 = await cr1.key.generate({ @@ -52,8 +52,8 @@ describe.skip('key generation', () => { const importedKey = await jose.importJWK(jwk) expect(importedKey).toBeDefined() const publicKeyCose = await cose.key.publicFromPrivate(coseKey) - fs.writeFileSync('./src/cr1/__fixtures__/holder_0_private_key.cbor', k1) - fs.writeFileSync('./src/cr1/__fixtures__/holder_0_public_key', cose.cbor.encode(publicKeyCose)) + fs.writeFileSync('./test/__fixtures__/holder_0_private_key.cbor', k1) + fs.writeFileSync('./test/__fixtures__/holder_0_public_key', cose.cbor.encode(publicKeyCose)) }) it('application/pkcs8', async () => { const k1 = await cr1.key.generate({ diff --git a/test/w3c-cr-1/1-credentials.test.ts b/test/w3c-cr-1/1-credentials.test.ts index 4184a17..422186e 100644 --- a/test/w3c-cr-1/1-credentials.test.ts +++ b/test/w3c-cr-1/1-credentials.test.ts @@ -3,7 +3,7 @@ import * as jose from 'jose' import * as cose from '@transmute/cose' import * as transmute from '../../src' -import * as fixtures from '../../src/cr1/__fixtures__' +import * as fixtures from '../../test/__fixtures__' const jws = { diff --git a/test/w3c-cr-1/2-presentations.test.ts b/test/w3c-cr-1/2-presentations.test.ts index 0e03c81..0e6525f 100644 --- a/test/w3c-cr-1/2-presentations.test.ts +++ b/test/w3c-cr-1/2-presentations.test.ts @@ -2,7 +2,7 @@ import * as jose from 'jose' import * as transmute from '../../src' import * as cose from '@transmute/cose' -import * as fixtures from '../../src/cr1/__fixtures__' +import * as fixtures from '../../test/__fixtures__' const coseSign1 = { sign: async (bytes: Uint8Array) => { diff --git a/test/w3c-cr-1/3-schema.test.ts b/test/w3c-cr-1/3-schema.test.ts index 5be62db..4829e65 100644 --- a/test/w3c-cr-1/3-schema.test.ts +++ b/test/w3c-cr-1/3-schema.test.ts @@ -3,8 +3,8 @@ import * as cose from '@transmute/cose' import * as transmute from '../../src' const privateKeyType = 'application/jwk+json' -const privateKeyContent = fs.readFileSync('./src/cr1/__fixtures__/issuer_0_private_key.json') -const publicKeyContent = fs.readFileSync('./src/cr1/__fixtures__/issuer_0_public_key.json') +const privateKeyContent = fs.readFileSync('./test/__fixtures__/issuer_0_private_key.json') +const publicKeyContent = fs.readFileSync('./test/__fixtures__/issuer_0_public_key.json') const coseSign1 = { sign: async (bytes: Uint8Array) => { diff --git a/test/w3c-cr-1/4-status.test.ts b/test/w3c-cr-1/4-status.test.ts index 9009876..453d667 100644 --- a/test/w3c-cr-1/4-status.test.ts +++ b/test/w3c-cr-1/4-status.test.ts @@ -3,8 +3,8 @@ import * as cose from '@transmute/cose' import * as transmute from '../../src' const privateKeyType = 'application/jwk+json' -const privateKeyContent = fs.readFileSync('./src/cr1/__fixtures__/issuer_0_private_key.json') -const publicKeyContent = fs.readFileSync('./src/cr1/__fixtures__/issuer_0_public_key.json') +const privateKeyContent = fs.readFileSync('./test/__fixtures__/issuer_0_private_key.json') +const publicKeyContent = fs.readFileSync('./test/__fixtures__/issuer_0_public_key.json') const coseSign1 = { sign: async (bytes: Uint8Array) => { diff --git a/test/w3c-cr-1/5-data-model.test.ts b/test/w3c-cr-1/5-data-model.test.ts index f2c686a..6b8b200 100644 --- a/test/w3c-cr-1/5-data-model.test.ts +++ b/test/w3c-cr-1/5-data-model.test.ts @@ -1,5 +1,5 @@ -import * as fixtures from "../../src/cr1/__fixtures__"; +import * as fixtures from "../../test/__fixtures__"; const { text, review } = fixtures @@ -182,7 +182,7 @@ credentialSubject: // https://www.w3.org/TR/2024/CRD-vc-data-model-2.0-20240205/#issuer // MUST BE: URL, or Object with ID that is URL describe("Issuer", () => { - it("can be object with id as url", async () => { + it("can be object with id as url", async () => { const validation = await review( text(` "@context": @@ -199,7 +199,7 @@ credentialSubject: expect(validation.warnings).toEqual([]); }) - it("can be url", async () => { + it("can be url", async () => { const validation = await review( text(` "@context": @@ -215,7 +215,7 @@ credentialSubject: expect(validation.warnings).toEqual([]); }) - it("gives warning if issuer not valid url", async () => { + it("gives warning if issuer not valid url", async () => { const validation = await review( text(` "@context": @@ -236,7 +236,7 @@ credentialSubject: }); }) - it("gives warning if issuer.id not valid url", async () => { + it("gives warning if issuer.id not valid url", async () => { const validation = await review( text(` "@context": @@ -262,7 +262,7 @@ credentialSubject: // https://www.w3.org/TR/2024/CRD-vc-data-model-2.0-20240205/#presentations-0 describe("Presentations", () => { describe("verifiableCredential", () => { - it("can be an array of enveloped credentials and credentials", async () => { + it("can be an array of enveloped credentials and credentials", async () => { const validation = await review( text(` "@context": @@ -293,7 +293,7 @@ describe("Presentations", () => { }]); }) - it("warns when non-object value is used", async () => { + it("warns when non-object value is used", async () => { const validation = await review( text(` "@context": @@ -314,7 +314,7 @@ describe("Presentations", () => { }]); }) - it("warns when enveloped credential id is not valid", async () => { + it("warns when enveloped credential id is not valid", async () => { const validation = await review( text(` "@context": @@ -352,7 +352,7 @@ describe("Presentations", () => { }) }); describe("holder", () => { - it("can be object with id as url", async () => { + it("can be object with id as url", async () => { const validation = await review( text(` "@context": @@ -365,8 +365,8 @@ describe("Presentations", () => { ); expect(validation.warnings).toEqual([]); }) - - it("can be url", async () => { + + it("can be url", async () => { const validation = await review( text(` "@context": @@ -378,8 +378,8 @@ describe("Presentations", () => { ); expect(validation.warnings).toEqual([]); }) - - it("gives warning if issuer not valid url", async () => { + + it("gives warning if issuer not valid url", async () => { const validation = await review( text(` "@context": @@ -396,8 +396,8 @@ describe("Presentations", () => { reference: 'https://www.w3.org/TR/vc-data-model-2.0/#presentations-0' }); }) - - it("gives warning if issuer.id not valid url", async () => { + + it("gives warning if issuer.id not valid url", async () => { const validation = await review( text(` "@context": @@ -416,7 +416,7 @@ describe("Presentations", () => { }); }) }); - + }); // it.todo('data model tests')