diff --git a/.gitignore b/.gitignore index 29e83ef..ba4e144 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ dist .DS_Store .env -sd-jwt \ No newline at end of file +sd-jwt + +example \ No newline at end of file diff --git a/README.md b/README.md index c3e0058..b9560c9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ 🚧 Experimental implementation of sd-jwt for use with W3C Verifiable Credentials. 🔥 +🚧 Extra experimental implementation of sd-cwt for SPICE 🔥 + #### [Questions? Contact Transmute](https://transmute.typeform.com/to/RshfIw?typeform-source=vc-jwt-sd) @@ -112,7 +114,7 @@ Example verification: ```json { - "protectedHEader": { + "protectedHeader": { "alg": "ES384" }, "claimset": { diff --git a/jest.config.js b/jest.config.js index 5231184..797178c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - testPathIgnorePatterns: ['examples', 'attic'], + testPathIgnorePatterns: ['example', 'attic'], coverageReporters: ['json-summary'], }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f6be3c5..db0cb74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "@transmute/vc-jwt-sd", - "version": "0.0.0", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@transmute/vc-jwt-sd", - "version": "0.0.0", + "version": "0.0.4", "license": "Apache-2.0", "dependencies": { + "@transmute/cose": "^0.0.13", + "cbor-web": "9.0.0", "jose": "^4.13.1", "json-pointer": "^0.6.2", "moment": "^2.29.4", @@ -1283,6 +1285,22 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@transmute/cose": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@transmute/cose/-/cose-0.0.13.tgz", + "integrity": "sha512-v1GJOYWrX08cZCdl2Nfk4QAoZwssxmbj5VJ2MhTf9fg8c/Pb9XtNKVRodyzON8YARtKSomO0H9/GfUSydkzGyg==", + "dependencies": { + "@transmute/rfc9162": "^0.0.4", + "cbor-web": "^9.0.0", + "cose-js": "^0.8.4", + "jose": "^4.14.4" + } + }, + "node_modules/@transmute/rfc9162": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@transmute/rfc9162/-/rfc9162-0.0.4.tgz", + "integrity": "sha512-ChTvT9RN2MnQTvN56FhH6kCLwNRcnepeaSw9lXUdcP+2tD2/1yPrX7txzB8yhDnzG51QUej1vdKxXSpF1d4yIA==" + }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -1615,6 +1633,11 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/aes-cbc-mac": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/aes-cbc-mac/-/aes-cbc-mac-1.0.1.tgz", + "integrity": "sha512-F1U0qNBNyrW82LRdQvYWKOpljZFnJ9LBUGWNCzCBTC3/+Fki77KgDrfJ+rBVlCpcmMT3jDEGhG61RMVeyHAKog==" + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1682,6 +1705,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1710,6 +1738,14 @@ "node": ">=8" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/babel-jest": { "version": "29.6.2", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.2.tgz", @@ -1807,6 +1843,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1829,6 +1870,11 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, "node_modules/browserslist": { "version": "4.21.10", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", @@ -1926,6 +1972,25 @@ } ] }, + "node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/cbor-web": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-9.0.0.tgz", + "integrity": "sha512-bTCCiR0brj9RShibl2wirK+y99JuZBhCLXo114N7HtwjKnLa43D14X9Ay0SdIslCYhyOH6kagtMp9HhVkqyPqQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2032,6 +2097,22 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cose-js": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/cose-js/-/cose-js-0.8.4.tgz", + "integrity": "sha512-TYt82olRQS/iZyb/qchG4KZSnzVBlOVXJjTCCgwKZUIkqqFyUIA+JG8OQdX5+ZyiWLj9W118Kuf3/jII0Gb/Bg==", + "dependencies": { + "aes-cbc-mac": "^1.0.1", + "any-promise": "^1.3.0", + "cbor": "^8.1.0", + "elliptic": "^6.4.0", + "node-hkdf-sync": "^1.0.0", + "node-rsa": "^1.1.1" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2140,6 +2221,20 @@ "integrity": "sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==", "dev": true }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -2431,6 +2526,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2724,6 +2827,25 @@ "node": ">=8" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2805,8 +2927,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/is-arrayish": { "version": "0.2.1", @@ -3747,6 +3868,16 @@ "node": ">=6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3785,6 +3916,17 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/node-hkdf-sync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-hkdf-sync/-/node-hkdf-sync-1.0.0.tgz", + "integrity": "sha512-dKe4X44YGLxPITIMdbnVw0URGTLw2lUUKmClar5iz53ZRrl3xGKk3k7KsBASRMHGh6bJCE1Gmuirb/QaL7rJuw==", + "dependencies": { + "vows": "0.5.13" + }, + "engines": { + "node": ">= 0.6.5" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3797,6 +3939,22 @@ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "dependencies": { + "asn1": "^0.2.4" + } + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "engines": { + "node": ">=12.19" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4273,6 +4431,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4703,6 +4866,20 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/vows": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/vows/-/vows-0.5.13.tgz", + "integrity": "sha512-m2+3s/ITbI95b7uzsBnA7oZg7/bJZFb+sKp2TUvomrIQxjtuNOivf/yOxAAyhEAX8gkIogoXfBJRmj9ys8r3gQ==", + "dependencies": { + "eyes": ">=0.1.6" + }, + "bin": { + "vows": "bin/vows" + }, + "engines": { + "node": ">=0.2.6" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index a1d5191..31c4e8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@transmute/vc-jwt-sd", - "version": "0.0.1", + "version": "0.0.4", "description": "Experimental", "main": "./dist/index.js", "typings": "dist/index.d.ts", @@ -42,9 +42,11 @@ "typescript": "^4.9.4" }, "dependencies": { + "@transmute/cose": "^0.0.13", "jose": "^4.13.1", "json-pointer": "^0.6.2", "moment": "^2.29.4", - "yaml": "^2.3.1" + "yaml": "^2.3.1", + "cbor-web": "9.0.0" } } diff --git a/src/Holder.ts b/src/Holder.ts index 407b9f4..6a9f548 100644 --- a/src/Holder.ts +++ b/src/Holder.ts @@ -8,7 +8,6 @@ import _select_disclosures from './_select_disclosures' import Parse from "./Parse"; -import digester from "./digester"; // SDJWTHolder export default class Holder { @@ -28,9 +27,9 @@ export default class Holder { // todo: verify const sd_jwt_payload = jose.decodeJwt(parsed.jwt); - const config = { digester } + const config = { digester: this.digester } - const {disclosureMap, hashToEncodedDisclosureMap} = Parse.expload(credential, config) + const {disclosureMap, hashToEncodedDisclosureMap} = await Parse.expload(credential, config) const state = { hs_disclosures: [], diff --git a/src/Issuer.ts b/src/Issuer.ts index c2b03e5..e6c9af0 100644 --- a/src/Issuer.ts +++ b/src/Issuer.ts @@ -13,12 +13,18 @@ import { issuancePayload } from "./YAML-SD/issuancePayload"; export default class Issuer { public iss?: string; public alg: string; + public kid?: string; + public typ?: string; + public cty?: string; public digester: Digest; public signer: CompactSign; public salter: Salter; constructor(ctx: IssuerCtx) { this.iss = ctx.iss; this.alg = ctx.alg; + this.kid = ctx.kid; + this.typ = ctx.typ; + this.cty = ctx.cty; this.digester = ctx.digester; this.signer = ctx.signer; this.salter = ctx.salter @@ -27,9 +33,9 @@ export default class Issuer { const config = { disclosures: {}, salter: this.salter, - digester: this.digester.digest, + digester: this.digester, } - const issuedPayload = issuancePayload(claims, config); + const issuedPayload = await issuancePayload(claims, config); const claimset = issuedPayload as Record; claimset[DIGEST_ALG_KEY] = this.digester.name; if (this.iss) { @@ -46,10 +52,22 @@ export default class Issuer { jwk: JWK.getPublicKey(holder), }; } + const protectedHeader = {} as any; + + if (this.alg) { + protectedHeader.alg = this.alg; + } + if (this.kid) { + protectedHeader.kid = this.kid; + } + if (this.typ) { + protectedHeader.typ = this.typ; + } + if (this.cty) { + protectedHeader.cty = this.cty; + } const jws = await this.signer.sign({ - protectedHeader: { - alg: this.alg, - }, + protectedHeader, claimset, }); return jws + COMBINED_serialization_FORMAT_SEPARATOR + Object.keys(config.disclosures) diff --git a/src/JWK.ts b/src/JWK.ts index 216d5bc..0228b7f 100644 --- a/src/JWK.ts +++ b/src/JWK.ts @@ -1,5 +1,7 @@ import { PrivateKeyJwk, PublicKeyJwk } from "./types"; +import { generateKeyPair, exportJWK } from 'jose' + const format = (jwk: PublicKeyJwk | PrivateKeyJwk) => { const { kid, x5u, x5c, x5t, kty, crv, alg, key_ops, x, y, d, ...rest } = jwk; return JSON.parse( @@ -25,6 +27,19 @@ export const getPublicKey = (jwk: any): PublicKeyJwk => { return format(publicKeyJwk); }; -const JWK = { format, getPublicKey }; +const getExtractableKeyPair = async (alg: string) =>{ + const keypair = await generateKeyPair(alg, {extractable: true}) + const publicKeyJwk = await exportJWK(keypair.publicKey) + publicKeyJwk.alg = alg + const secretKeyJwk = await exportJWK(keypair.privateKey) + secretKeyJwk.alg = alg + return { + publicKeyJwk: format(publicKeyJwk), + secretKeyJwk: format(secretKeyJwk) + } +} + + +const JWK = { format, getPublicKey, generate: getExtractableKeyPair }; export default JWK; diff --git a/src/Parse.ts b/src/Parse.ts index d188819..a224ffc 100644 --- a/src/Parse.ts +++ b/src/Parse.ts @@ -24,18 +24,20 @@ const compact = (jws: string, options = { decodeDisclosure: false })=>{ return result } -const expload = (jws: string, config: any)=>{ +const expload = async (jws: string, config: any)=>{ const parsed = compact(jws) as any const decodedIssuance = decodeJwt(parsed.jwt) parsed.issued = decodedIssuance - const hash = config.digester(parsed.issued._sd_alg) + + const hash = config.digester const hashToDisclosureMap = {} as any const hashToEncodedDisclosureMap = {} as any - parsed.disclosures.map((encoded: string)=>{ - const hashed = hash.digest(encoded) + for (const encoded of parsed.disclosures){ + const hashed = await hash.digest(encoded) hashToEncodedDisclosureMap[hashed] = encoded hashToDisclosureMap[hashed] = JSON.parse(new TextDecoder().decode(base64url.decode(encoded))) - }) + } + parsed.disclosureMap = hashToDisclosureMap parsed.hashToEncodedDisclosureMap = hashToEncodedDisclosureMap return parsed diff --git a/src/SD-CWT/Holder.ts b/src/SD-CWT/Holder.ts new file mode 100644 index 0000000..0e0d166 --- /dev/null +++ b/src/SD-CWT/Holder.ts @@ -0,0 +1,120 @@ + +import cose from '@transmute/cose' +import * as cbor from 'cbor-web' +import { exportJWK, generateKeyPair } from 'jose'; +import {issuancePayload} from './yaml-to-cbor'; +import YAML from '../YAML-SD'; +import filterCredential from './filter-credential'; +import postVerifyProcessing from './post-verify-processing'; + +import { RequestVerify } from './types' + +export type CWTHolder = { + alg: number + + signer: any + verifier: any + salter: any + digester: any + + publicKeyJwk: any + + disclosures?: Map +} + +export type RequestPresentation = { + vc: Uint8Array; // cose sign 1 / cwt. + disclose: string // really yaml disclose structure +} + +export type HolderBuilder = { + alg: number + salter: () => Promise + digester: { + name: 'sha-256', + digest: (cbor: Buffer) => Promise + } +} + +const algStringToNumber = { + 'ES384': -35 +} + +const algNumberToString:any = { + '-35': 'ES384' +} + +export class Holder { + static build = async (arg: HolderBuilder) => { + const alg = algNumberToString[`${arg.alg}`] + const keyPair = await generateKeyPair(alg) + const secretKeyJwk = await exportJWK(keyPair.privateKey) + secretKeyJwk.alg = alg + const publicKeyJwk = await exportJWK(keyPair.publicKey) + publicKeyJwk.alg = alg + const signer = await cose.signer({ + privateKeyJwk: secretKeyJwk as any, + }) + const verifier = await cose.verifier({ + publicKeyJwk: publicKeyJwk as any, + }) + return new Holder({ + ...arg, + publicKeyJwk, + signer, + verifier, + }) + } + constructor(public config: CWTHolder){ + // console.log({ config }) + } + public present = async (req: RequestPresentation)=>{ + const decodedToken = await cbor.decodeFirst(req.vc) + const parsed = YAML.load(req.disclose) + const revealMap = await issuancePayload(parsed, this.config) + const decodedPayload = await cbor.decodeFirst(decodedToken.value[2], {}) + + const disclosures = decodedToken.value[1].get(333) as Buffer[] + + const decodedPayloadMap = decodedPayload instanceof Map ? decodedPayload : new Map(Object.entries(decodedPayload)); + + const disclosureMap = new Map() + // consider refactoring this mess + const disclosureArray = await Promise.all(disclosures.map(async (d) => { + const item = { + encoded: d, + decoded: await cbor.decodeFirst(d), + digest: (await this.config.digester.digest(d)).toString('hex'), + } + disclosureMap.set(item.digest, item.decoded) + return item + })) + await filterCredential(decodedPayloadMap, revealMap, disclosureMap ) + const redactedDisclosures = [] + for (const [key, value] of disclosureMap) { + redactedDisclosures.push(await cbor.encodeAsync(value)) + } + + const unprotectedHeader = new Map(); + unprotectedHeader.set(333, redactedDisclosures) + const presentation = cose.unprotectedHeader.set(req.vc, unprotectedHeader) + return presentation + } + + public verify = async ({vc}: RequestVerify)=>{ + const unprotectedHeader = cose.unprotectedHeader.get(vc) + const verified = await this.config.verifier.verify(vc) + const disclosures = unprotectedHeader.get(333) as Buffer[] + const disclosureMap = new Map() + await Promise.all(disclosures.map(async (d) => { + const item = { + digest: (await this.config.digester.digest(d)).toString('hex'), + decoded: await cbor.decodeFirst(d) + } + disclosureMap.set(item.digest, item.decoded) + })) + const claims = await cbor.decodeFirst(verified) + + return postVerifyProcessing(claims, disclosureMap) + } +} \ No newline at end of file diff --git a/src/SD-CWT/Issuer.ts b/src/SD-CWT/Issuer.ts new file mode 100644 index 0000000..7f9cd03 --- /dev/null +++ b/src/SD-CWT/Issuer.ts @@ -0,0 +1,102 @@ + +import cose from '@transmute/cose' +import * as cbor from 'cbor-web' +import { exportJWK, generateKeyPair } from 'jose'; +import yamlToCbor from './yaml-to-cbor'; + +import postVerifyProcessing from './post-verify-processing'; + +import { RequestVerify } from './types' + +export type CWTIssuer = { + alg: number + + signer: any + verifier: any + salter: any + digester: any + + publicKeyJwk: any + + disclosures?: Map +} + +export type RequestIssuance = { + claims: string; // really yaml. +} + +export type IssuerBuilder = { + alg: number + salter: () => Promise + digester: { + name: 'sha-256', + digest: (cbor: Buffer) => Promise + } +} + + +const algStringToNumber = { + 'ES384': -35 +} + +const algNumberToString:any = { + '-35': 'ES384' +} + + +export class Issuer { + static build = async (arg: IssuerBuilder) => { + const alg = algNumberToString[`${arg.alg}`] + const keyPair = await generateKeyPair(alg) + const secretKeyJwk = await exportJWK(keyPair.privateKey) + secretKeyJwk.alg = alg + const publicKeyJwk = await exportJWK(keyPair.publicKey) + publicKeyJwk.alg = alg + const signer = await cose.signer({ + privateKeyJwk: secretKeyJwk as any, + }) + const verifier = await cose.verifier({ + publicKeyJwk: publicKeyJwk as any, + }) + return new Issuer({ + ...arg, + publicKeyJwk, + signer, + verifier, + }) + } + constructor(public config: CWTIssuer){ + // console.log({ config }) + } + public issue = async (issuance: RequestIssuance)=>{ + const protectedHeader = { alg: algNumberToString[`${this.config.alg}`] } + + const payload = await yamlToCbor(issuance.claims, this.config) + const disclosureMap = this.config.disclosures as Map + const unprotectedHeader = new Map(); + const disclosures = Array.from(disclosureMap, ([_, value]) => value); + unprotectedHeader.set(333, disclosures) + + const signArguments = { protectedHeader, unprotectedHeader, payload: Uint8Array.from(payload) } + const signature = await this.config.signer.sign(signArguments) + const signatureWithDisclosuresInUnprotectedHeader = cose.unprotectedHeader.set(signature, unprotectedHeader) + return signatureWithDisclosuresInUnprotectedHeader + } + + public verify = async ({vc}: RequestVerify)=>{ + const unprotectedHeader = cose.unprotectedHeader.get(vc) + const verified = await this.config.verifier.verify(vc) + const disclosures = unprotectedHeader.get(333) as Buffer[] + const disclosureMap = new Map() + await Promise.all(disclosures.map(async (d) => { + const item = { + digest: (await this.config.digester.digest(d)).toString('hex'), + decoded: await cbor.decodeFirst(d) + } + disclosureMap.set(item.digest, item.decoded) + })) + const claims = await cbor.decodeFirst(verified) + const claimsMap = claims instanceof Map ? claims : new Map(Object.entries(claims)); + return postVerifyProcessing(claimsMap, disclosureMap) + } +} \ No newline at end of file diff --git a/src/SD-CWT/filter-credential.ts b/src/SD-CWT/filter-credential.ts new file mode 100644 index 0000000..80458ac --- /dev/null +++ b/src/SD-CWT/filter-credential.ts @@ -0,0 +1,60 @@ + +import {sdCwtMapProp, sdCwtArrayProp} from './yaml-to-cbor' + +export type WalkMapConfig = { + disclosures: Map +} + +const walkList = async (claimsList: any[], revealList: any[], config: WalkMapConfig)=>{ + for (const key in claimsList) { + const value = claimsList[key] + const revealValue = revealList ? revealList[key] : value + if (value instanceof Map){ + const item = value.get(sdCwtArrayProp) + if (item){ + const disclosureDigest = item.toString('hex') + if (!revealValue){ + config.disclosures.delete(disclosureDigest) + } + } else { + await walkMap(value, revealValue, config) + } + } else if (value instanceof Array){ + await walkList(value, revealValue, config) + } else { + // console.log('walkList ', key, value) + } + } +} + +const walkMap = async (claimsMap: Map, revealMap: Map, config: WalkMapConfig)=>{ + + for (const [key, value] of claimsMap) { + let revealValue = revealMap.get(key) + if (key === sdCwtMapProp){ + const [digest] = value + const disclosureDigest = digest.toString('hex') + const disclosed = config.disclosures.get(disclosureDigest) + const [salt, dataKey, dataValue] = disclosed as any[] + revealValue = revealMap.get(dataKey) + if (!revealValue){ + config.disclosures.delete(disclosureDigest) + } + } else if (value instanceof Map){ + await walkMap(value, revealValue, config) + } else if (value instanceof Array){ + await walkList(value, revealValue, config) + } else { + // console.log('walkMap ', key, value) + } + } +} + + +const filterCredential = async (claimsMap: Map, revealMap: Map, disclosureMap: Map): Promise> => { + const config = { disclosures: disclosureMap } + await walkMap(claimsMap, revealMap, config) + return claimsMap +} + +export default filterCredential \ No newline at end of file diff --git a/src/SD-CWT/index.ts b/src/SD-CWT/index.ts new file mode 100644 index 0000000..ed91b79 --- /dev/null +++ b/src/SD-CWT/index.ts @@ -0,0 +1,2 @@ +export * from './Issuer' +export * from './Holder' \ No newline at end of file diff --git a/src/SD-CWT/post-verify-processing.ts b/src/SD-CWT/post-verify-processing.ts new file mode 100644 index 0000000..be61867 --- /dev/null +++ b/src/SD-CWT/post-verify-processing.ts @@ -0,0 +1,64 @@ + +import {sdCwtMapProp, sdCwtArrayProp} from './yaml-to-cbor' + +export type WalkMapConfig = { + disclosures: Map +} + +const walkList = async (list: any[], config: WalkMapConfig)=>{ + for (const key in list) { + const value = list[key] + if (value instanceof Map){ + const item = value.get(sdCwtArrayProp) + if (item){ + list.splice(parseInt(key, 10), 1) + const disclosedDigest = item.toString('hex') + const disclosed = config.disclosures.get(disclosedDigest) + if (!disclosed){ + // console.log('skipping undisclosed', value) + continue + } + const [salt, dataValue] = disclosed as any[] + list[key] = dataValue + } else { + await walkMap(value, config) + } + } else if (value instanceof Array){ + await walkList(value, config) + } else { + // console.log('walkList ', key, value) + } + } +} + +const walkMap = async (map: Map, config: WalkMapConfig)=>{ + for (const [key, value] of map) { + if (key === sdCwtMapProp){ + map.delete(key) + const [digest] = value + const disclosedDigest = digest.toString('hex') + const disclosed = config.disclosures.get(disclosedDigest) + if (!disclosed){ + // console.log('skipping undisclosed', value) + continue + } + const [salt, dataKey, dataValue] = disclosed as any[] + map.set(dataKey, dataValue) + } else if (value instanceof Map){ + await walkMap(value, config) + } else if (value instanceof Array){ + await walkList(value, config) + } else { + // console.log('walkMap ', key, value) + } + } +} + + +const postVerifyProcessing = async (map: Map, disclosures: Map): Promise> => { + const config = { disclosures } + await walkMap(map, config) + return map +} + +export default postVerifyProcessing \ No newline at end of file diff --git a/src/SD-CWT/types.ts b/src/SD-CWT/types.ts new file mode 100644 index 0000000..0d4dca5 --- /dev/null +++ b/src/SD-CWT/types.ts @@ -0,0 +1,4 @@ + +export type RequestVerify = { + vc: Uint8Array +} \ No newline at end of file diff --git a/src/SD-CWT/yaml-to-cbor.ts b/src/SD-CWT/yaml-to-cbor.ts new file mode 100644 index 0000000..19677e7 --- /dev/null +++ b/src/SD-CWT/yaml-to-cbor.ts @@ -0,0 +1,236 @@ +import cose from '@transmute/cose' +import * as cbor from 'cbor-web' +import { + Pair, + Scalar, + YAMLMap, + YAMLSeq, +} from "yaml"; +// import { base64url } from 'jose'; + +import YAML from '../YAML-SD'; +import { walkMap } from "../YAML-SD/walkMap"; + +export const discloseTag = `!sd`; +// const sdJwtMapProp = `_sd` +export const sdCwtMapProp = 111 + +// const sdJwtArrayProp = `...` +export const sdCwtArrayProp = 222 + + +const discloseReplace = (source: Scalar | YAMLSeq | YAMLMap | Pair) => { + if ( + source instanceof Scalar || + source instanceof YAMLSeq || + source instanceof YAMLMap + ) { + const mutate = source as any; + delete mutate.toJSON; + delete mutate.sd; + delete mutate.tag; + } else if (source instanceof Pair) { + const mutate = source as any; + delete mutate.key.tag; + delete mutate.value.toJSON; + delete mutate.value.sd; + delete mutate.value.tag; + } else { + console.log(source) + throw new Error("discloseReplace, Unhandled disclosure case"); + } +}; + +const redactSource = (source: any, indexList: number[]) => { + source.items = source.items.filter((_: any, i: number) => { + discloseReplace(source.items[i]); + return !indexList.includes(i); + }); +}; + + +const serializeDisclosure = (salt: Uint8Array, item: any): Uint8Array => { + const list:any = [salt] + if (item instanceof Pair){ + list.push(item.key.value); + list.push(JSON.parse(JSON.stringify(item.value))); + } else if (item instanceof YAMLSeq){ + list.push(JSON.parse(JSON.stringify(item))); + } else if (item instanceof YAMLMap){ + list.push(JSON.parse(JSON.stringify(item))); + } else { + list.push(JSON.parse(JSON.stringify(item))); + } + return Buffer.from(cose.cbor.encode(list)) +} + +const updateTarget = (source: any, sourceItem: any, index: any, targetItem: any) => { + if (sourceItem instanceof Pair) { + let foundExistingDisclosure = source.items.find((item: any) => { + return item.key.value === sdCwtMapProp + }) + if (!foundExistingDisclosure) { + const disclosureKeyScalar = new Scalar(sdCwtMapProp) + const disclosureKeySeq = new YAMLSeq() + foundExistingDisclosure = new Pair(disclosureKeyScalar, disclosureKeySeq) + source.items.push(foundExistingDisclosure) + } + foundExistingDisclosure.value.items.push(targetItem) + } else { + source.items[index] = targetItem + } +} + + +const getDisclosureItem = async (salt: Uint8Array, source: any, config: any) => { + const cbor = serializeDisclosure(salt, source) + const disclosureHash = await config.digester.digest(cbor) + config.disclosures.set(disclosureHash.toString('hex'), cbor) + const disclosureHashScalar = new Scalar(disclosureHash) + if (source instanceof Pair) { + return disclosureHashScalar + } else { + const disclosePair = new Pair(new Scalar(sdCwtArrayProp), disclosureHashScalar) + const discloseElement = new YAMLMap() + discloseElement.add(disclosePair) + return discloseElement + } +} + +const addDisclosure = async (source: any, index: string, sourceItem: any, config: any) => { + const salt = await config.salter(sourceItem) + if (!salt) { + console.warn(JSON.stringify(sourceItem, null, 2)) + throw new Error('Unhandled salt disclosure...') + } + const item = await getDisclosureItem(salt, sourceItem, config) + updateTarget(source, sourceItem, index, item) +} + +const issuanceWalkMap = async (source: YAMLMap, config: any) => { + if (source === null){ + return + } + const indexList = [] as number[]; + for (const index in source.items) { + const sourcePair = source.items[index] as any; + if (sourcePair.value instanceof YAMLSeq) { + await issuanceWalkList(sourcePair.value as YAMLSeq, config); + } + if (sourcePair.value instanceof YAMLMap) { + await issuanceWalkMap(sourcePair.value, config); + } + if (sourcePair.key.tag === discloseTag) { + await addDisclosure(source, index, sourcePair, config) + indexList.push(parseInt(index, 10)); + } + } + redactSource(source, indexList); +}; + +const issuanceWalkList = async (source: YAMLSeq, config: any) => { + const indexList = [] as number[]; + for (const index in source.items) { + const sourceElement = source.items[index] as any; + if (sourceElement instanceof YAMLSeq) { + await issuanceWalkList(sourceElement, config); + } + if (sourceElement instanceof YAMLMap) { + await issuanceWalkMap(sourceElement, config); + } + if (sourceElement.tag === discloseTag) { + await addDisclosure(source, index, sourceElement, config) + } + } + redactSource(source, indexList); +}; + + +const disclosureSorter = (pair: any) => { + if (pair.key && pair.key.value === sdCwtMapProp) { + pair.value.items.sort((a: any, b: any) => { + if (a.value >= b.value) { + return 1 + } else { + return -1 + } + }) + } +} + +const preconditionChecker = (pair: any) => { + if (pair.key && pair.key.value === sdCwtMapProp) { + throw new Error('claims may not contain _sd') + } +} + + + +const cborWalkMap = async (source: YAMLMap, target: Map, config: any) => { + if (source === null){ + return + } + for (const index in source.items) { + const sourcePair = source.items[index] as any; + if (sourcePair.value instanceof YAMLSeq) { + const targetValue = new Array() + target.set(sourcePair.key.value, targetValue) + await cborWalkList(sourcePair.value as YAMLSeq, targetValue, config); + } + if (sourcePair.value instanceof YAMLMap) { + const targetValue = new Map(); + target.set(sourcePair.key.value, targetValue) + await cborWalkMap(sourcePair.value, targetValue, config,); + } + if (sourcePair.value instanceof Scalar) { + const mapKey = typeof sourcePair.key === 'string' ? sourcePair.key : sourcePair.key.value + target.set(mapKey, sourcePair.value.value) + } + + } + +}; + +const cborWalkList = async (source: YAMLSeq, target: Array, config: any) => { + for (const index in source.items) { + const sourceElement = source.items[index] as any; + if (sourceElement instanceof YAMLSeq) { + const targetValue = new Array() + target.push(targetValue) + await cborWalkList(sourceElement, targetValue, config); + } else if (sourceElement instanceof YAMLMap) { + const targetValue = new Map(); + target.push(targetValue) + await cborWalkMap(sourceElement, targetValue, config); + } else if (sourceElement.value instanceof Scalar || sourceElement.value instanceof Buffer || typeof sourceElement.value === 'string' || typeof sourceElement.value === 'number') { + target.push(sourceElement.value) + } else if (sourceElement instanceof Scalar) { + target.push(sourceElement.value) + } else { + throw new Error('Unhandled case... ' + JSON.stringify(sourceElement, null, 2)) + } + } +}; + +const yamlMapToJsMap = async (doc: YAMLMap, config: any): Promise> => { + const finalMap = new Map(); + await cborWalkMap(doc, finalMap, config) + return finalMap +} + +export const issuancePayload = async (doc: any, config: any) => { + walkMap(doc, preconditionChecker) + await issuanceWalkMap(doc, config); + walkMap(doc, disclosureSorter) + return yamlMapToJsMap(doc, config) +} + +const yamlToCbor = async (yamlClaims: string, config: any) => { + const parsed = YAML.load(yamlClaims) + config.disclosures = new Map(); + const payloadMap = await issuancePayload(parsed, config) + return cbor.encodeAsync(payloadMap) + +} + +export default yamlToCbor; diff --git a/src/Verifier.ts b/src/Verifier.ts index f6e529e..1e15cf2 100644 --- a/src/Verifier.ts +++ b/src/Verifier.ts @@ -6,7 +6,6 @@ import { VerifierCtx, RequestPresentationVerify, PublicKeyJwk } from './types' import JWS from './JWS'; import Parse from './Parse'; -import digester from "./digester"; import _unpack_disclosed_claims from './_unpack_disclosed_claims' @@ -56,14 +55,10 @@ export default class Verifier { } } } - - const config = { digester } - - const {disclosureMap, hashToEncodedDisclosureMap} = Parse.expload(presentation, config) - + const config = { digester: this.digester } + const {disclosureMap, hashToEncodedDisclosureMap} = await Parse.expload(presentation, config) const state = { _hash_to_disclosure: hashToEncodedDisclosureMap, _hash_to_decoded_disclosure: disclosureMap } - const output = _unpack_disclosed_claims(verifiedIssuanceToken.claimset, state) - return {protectedHEader: verifiedIssuanceToken.protectedHeader, claimset: output} + return JSON.parse(JSON.stringify({protectedHeader: verifiedIssuanceToken.protectedHeader, claimset: output})) } } \ No newline at end of file diff --git a/src/YAML-SD/issuancePayload.ts b/src/YAML-SD/issuancePayload.ts index b5b1d55..8d1001b 100644 --- a/src/YAML-SD/issuancePayload.ts +++ b/src/YAML-SD/issuancePayload.ts @@ -33,11 +33,11 @@ const updateTarget = (source: any, sourceItem: any, index: any, targetItem: any) } -const getDisclosureItem = (salt: string, source: any, config: any)=>{ +const getDisclosureItem = async (salt: string, source: any, config: any)=>{ const json = serializeDisclosure(salt, source) const encoded = base64url.encode(json) // spy here... - const disclosureHash = config.digester(encoded) + const disclosureHash = await config.digester.digest(encoded) config.disclosures[encoded] = disclosureHash const disclosureHashScalar = new Scalar(disclosureHash) if (source instanceof Pair){ @@ -50,47 +50,47 @@ const getDisclosureItem = (salt: string, source: any, config: any)=>{ } } -const addDisclosure = (source: any, index: string, sourceItem:any, config: any) => { +const addDisclosure = async (source: any, index: string, sourceItem:any, config: any) => { const salt = config.salter(sourceItem) if (!salt){ console.warn(JSON.stringify(sourceItem, null, 2)) throw new Error('Unhandled salt disclosure...') } - const item = getDisclosureItem(salt, sourceItem, config) + const item = await getDisclosureItem(salt, sourceItem, config) updateTarget(source, sourceItem, index, item) } -const issuanceWalkMap = (source: YAMLMap, config: any) => { +const issuanceWalkMap = async(source: YAMLMap, config: any) => { const indexList = [] as number[]; for (const index in source.items) { const sourcePair = source.items[index] as any; if (sourcePair.value instanceof YAMLSeq) { - issuanceWalkList(sourcePair.value as YAMLSeq, config); + await issuanceWalkList(sourcePair.value as YAMLSeq, config); } if (sourcePair.value instanceof YAMLMap) { - issuanceWalkMap(sourcePair.value, config); + await issuanceWalkMap(sourcePair.value, config); } if (sourcePair.key.tag === discloseTag) { - addDisclosure(source, index, sourcePair, config) + await addDisclosure(source, index, sourcePair, config) indexList.push(parseInt(index, 10)); } } redactSource(source, indexList); }; -const issuanceWalkList = (source: YAMLSeq, config: any) => { +const issuanceWalkList = async (source: YAMLSeq, config: any) => { const indexList = [] as number[]; for (const index in source.items) { const sourceElement = source.items[index] as any; if (sourceElement instanceof YAMLSeq) { - issuanceWalkList(sourceElement, config); + await issuanceWalkList(sourceElement, config); } if (sourceElement instanceof YAMLMap) { - issuanceWalkMap(sourceElement, config); + await issuanceWalkMap(sourceElement, config); } if (sourceElement.tag === discloseTag) { - addDisclosure(source, index, sourceElement, config) + await addDisclosure(source, index, sourceElement, config) // indexList.push(parseInt(index, 10)); } } @@ -116,9 +116,9 @@ const preconditionChecker = (pair: any)=>{ } } -export const issuancePayload = (doc: any, config: any)=>{ +export const issuancePayload = async (doc: any, config: any)=>{ walkMap(doc, preconditionChecker) - issuanceWalkMap(doc, config); + await issuanceWalkMap(doc, config); walkMap(doc, disclosureSorter) return JSON.parse(JSON.stringify(doc)) } \ No newline at end of file diff --git a/src/YAML-SD/serializeDisclosure.ts b/src/YAML-SD/serializeDisclosure.ts index cadeca8..345101a 100644 --- a/src/YAML-SD/serializeDisclosure.ts +++ b/src/YAML-SD/serializeDisclosure.ts @@ -3,11 +3,12 @@ import { YAMLMap, YAMLSeq, Pair, - parse + parse, + Scalar } from "yaml"; const serializeList = (list: YAMLSeq)=>{ - return JSON.stringify(JSON.parse(JSON.stringify(list))).replace(/\:/g, ': ').replace(/,/g, ', ') + return JSON.stringify(JSON.parse(JSON.stringify(list))).replace(/"\:/g, '": ').replace(/,/g, ', ') } const serializeMap = (map: YAMLMap)=>{ @@ -16,7 +17,11 @@ const serializeMap = (map: YAMLMap)=>{ if (Array.isArray(_sd)){ _sd.sort() } - return JSON.stringify({_sd, ...rest}).replace(/\:/g, ': ').replace(/,/g, ', ') + return JSON.stringify({_sd, ...rest}).replace(/"\:/g, '": ').replace(/,/g, ', ') +} + +const serializeScalar = (value: Scalar) => { + return `${JSON.stringify(value.value).replace(/,/g, ', ')}` } export const serializeDisclosure = (salt: string, item: any) => { @@ -26,7 +31,7 @@ export const serializeDisclosure = (salt: string, item: any) => { } else if (item.value instanceof YAMLMap){ return `["${salt}", "${item.key.value}", ${serializeMap(item.value)}]` } else { - return `["${salt}", ${JSON.stringify(item.key.value).replace(/,/g, ', ') }, ${JSON.stringify(item.value.value).replace(/,/g, ', ')}]` + return `["${salt}", ${JSON.stringify(item.key.value).replace(/,/g, ', ') }, ${serializeScalar(item.value)}]` } } else if (item instanceof YAMLSeq){ return `["${salt}", ${serializeList(item)}]` @@ -36,65 +41,3 @@ export const serializeDisclosure = (salt: string, item: any) => { return `["${salt}", ${JSON.stringify(JSON.parse(JSON.stringify(parse(item.value)))).replace(/\:/g, ': ') }]` } } - - // //// WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgeyJfc2QiOiBbInNrd3p0bTY1dWkxUC1jenJjX08wbXFLM2ljdG9yQ0VvanI5OFU2TGtEUWciXSwgImZvbyI6ICJiYXIifV0 - - // if (disclosureHash === 'jLlkIcGUjlc0jteTpWe61mj_41z1yvN0-1FJsz3heHg'){ - // const theirs = `WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgeyJfc2QiOiBbInNrd3p0bTY1dWkxUC1jenJjX08wbXFLM2ljdG9yQ0VvanI5OFU2TGtEUWciXSwgImZvbyI6ICJiYXIifV0` - - // const theirsDecoded = new TextDecoder().decode(base64url.decode(theirs)) - // const mineDecoded = new TextDecoder().decode(base64url.decode(encoded)) - // console.warn(theirsDecoded) - // console.warn(mineDecoded) - // // disclosureHash = 'JZvSesAZw0Ngs4RUyukL18dsLlqKWnu05HMa1yA-NKI' - // } - -// if (disclosureHash === '0yeikKLfiJsNhNht1N0matFWFh7QUvFk728Xla4F-og'){ -// console.log(salt, source) - -// const encodedFromPython = `WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInNkX2FycmF5IiwgWzMyLCAyM11d` -// // [ -// // "2GLC42sKQveCfGfryNRN9w", -// // "sd_array", -// // [ -// // 32, -// // 23 -// // ] -// // ] -// const theirsDecoded = new TextDecoder().decode(base64url.decode(encodedFromPython)) -// const mineDecoded = new TextDecoder().decode(base64url.decode(encoded)) -// console.warn(theirsDecoded) -// console.warn(mineDecoded) -// disclosureHash = '0L4NSi1iP7pNwrkqqc63NcfnPJkSzSV6Rg2h3TlPoQw' -// } - -// if (disclosureHash === 'OgGrwYNbIl-IXiBIM6FmIBGRijN27IATvn0pPyxt9UQ'){ -// console.log(salt, source) - -// // WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgbnVsbF0 -// // [ -// // "eluV5Og3gSNII8EYnsxA_A", -// // null -// // ] - -// // WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgbnVsbF0 -// // [ -// // "2GLC42sKQveCfGfryNRN9w", -// // null -// // ] - -// // const encodedFromPython = `WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInNkX2FycmF5IiwgWzMyLCAyM11d` -// // // [ -// // // "2GLC42sKQveCfGfryNRN9w", -// // // "sd_array", -// // // [ -// // // 32, -// // // 23 -// // // ] -// // // ] -// // const theirsDecoded = new TextDecoder().decode(base64url.decode(encodedFromPython)) -// // const mineDecoded = new TextDecoder().decode(base64url.decode(encoded)) -// // console.warn(theirsDecoded) -// // console.warn(mineDecoded) -// // disclosureHash = 'OgGrwYNbIl-IXiBIM6FmIBGRijN27IATvn0pPyxt9UQ' -// } \ No newline at end of file diff --git a/src/YAML-SD/tokenToSchema.ts b/src/YAML-SD/tokenToSchema.ts index 0f59bed..d83f2cc 100644 --- a/src/YAML-SD/tokenToSchema.ts +++ b/src/YAML-SD/tokenToSchema.ts @@ -114,8 +114,8 @@ const walkMap = (obj:any, map: YAMLMap, config: any)=>{ } } -export const tokenToSchema = (token: string, config: any) => { - const parsed = Parse.expload(token, config) +export const tokenToSchema = async (token: string, config: any) => { + const parsed = await Parse.expload(token, config) const schema = new YAMLMap() config.disclosureMap = parsed.disclosureMap delete parsed.issued._sd_alg diff --git a/src/YAML-SD/walkMap.ts b/src/YAML-SD/walkMap.ts index 65fac81..1d87f3a 100644 --- a/src/YAML-SD/walkMap.ts +++ b/src/YAML-SD/walkMap.ts @@ -5,6 +5,9 @@ import { import {walkList} from './walkList' export const walkMap = (obj: YAMLMap, replacer: any) => { + if (obj === null){ + return + } for (const pair of obj.items) { if (pair.value instanceof YAMLSeq) { walkList(pair.value, replacer); diff --git a/src/digester.ts b/src/digester.ts deleted file mode 100644 index fd27ceb..0000000 --- a/src/digester.ts +++ /dev/null @@ -1,13 +0,0 @@ -import crypto from "crypto"; -import { base64url } from "jose"; - -const digester = (name: 'sha-256')=>{ - return { - name, - digest: (json: string) => { - return base64url.encode(crypto.createHash("sha256").update(json).digest()) - } - } -} - -export default digester diff --git a/src/index.ts b/src/index.ts index b10f855..f206889 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,13 +3,16 @@ import Issuer from './Issuer' import Holder from './Holder' import Verifier from './Verifier' -import digester from './digester' import JWK from './JWK' import JWS from './JWS' import Parse from './Parse' import YAML from './YAML-SD' -const SD = { YAML, JWK, JWS, digester, Issuer, Holder, Verifier, Parse } +import web from './web' + +import * as v2 from './SD-CWT' + +const SD = { v2, web, YAML, JWK, JWS, Issuer, Holder, Verifier, Parse } export default SD \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 0d38823..fb552ba 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,7 +23,7 @@ export type SignParams = { export type CompactSign = { sign: ({protectedHeader, claimset}: SignParams) => Promise } export type CompactVerify = { verify: (jws: string) => Promise } -export type Digest = { name: 'sha-256', digest: (json: string) => string } +export type Digest = { name: 'sha-256', digest: (json: string) => Promise } export type Salter = () => string @@ -31,6 +31,9 @@ export type IssuerCtx = { iss?: string, _sd_alg ?: string alg: string, + kid?: string, + typ?: string, + cty?: string, digester: Digest, signer: CompactSign salter: Salter diff --git a/src/web.ts b/src/web.ts new file mode 100644 index 0000000..d337674 --- /dev/null +++ b/src/web.ts @@ -0,0 +1,31 @@ + +import { base64url } from 'jose'; + + +const getSalter = () => { + const salter = () => { + const array = new Uint8Array(16); + window.crypto.getRandomValues(array); + const encoded = base64url.encode(array); + return encoded + } + return salter +} + +const getDigester = (name = 'sha-256') => { + if (name !== 'sha-256'){ + throw new Error('hash function not supported') + } + return { + name, + digest: async (json: string) => { + const content = new TextEncoder().encode(json); + const digest = await window.crypto.subtle.digest(name.toUpperCase(), content); + return base64url.encode(new Uint8Array(digest)); + } + }; +}; + +const web = { salter: getSalter, digester: getDigester } + +export default web \ No newline at end of file diff --git a/test/api-specification.test.ts b/test/api-specification.test.ts index 2af9e39..25d91c5 100644 --- a/test/api-specification.test.ts +++ b/test/api-specification.test.ts @@ -7,9 +7,6 @@ import SD from "../src"; const testcases = fs.readdirSync('testcases/', { withFileTypes: true }); - - - const test = { name: 'array_full_sd'} it(test.name, async () => { const spec = testcase.getSpec(`testcases/${test.name}/specification.yml`) as any @@ -17,7 +14,7 @@ it(test.name, async () => { const issuer = new SD.Issuer({ alg: 'ES256', iss: settings.identifiers.issuer, - digester: SD.digester('sha-256'), + digester: testcase.digester('sha-256'), signer: await SD.JWS.signer(settings.key_settings.issuer_key), salter }) @@ -33,7 +30,7 @@ it(test.name, async () => { expect(computed.parsed.disclosures).toEqual(expected.parsed.disclosures) const holder = new SD.Holder({ alg: 'ES256', - digester: SD.digester('sha-256'), + digester: testcase.digester('sha-256'), signer: spec.key_binding ? await SD.JWS.signer(settings.key_settings.holder_key) : undefined }) const vp = await holder.present({ @@ -55,7 +52,7 @@ it(test.name, async () => { } const verifier = new SD.Verifier({ alg: 'ES256', - digester: SD.digester('sha-256'), + digester: testcase.digester('sha-256'), verifier: issuerVerifier }) const verified = await verifier.verify({ @@ -84,7 +81,7 @@ describe("testcases", () => { const issuer = new SD.Issuer({ alg: 'ES256', iss: settings.identifiers.issuer, - digester: SD.digester('sha-256'), + digester: testcase.digester('sha-256'), signer: await SD.JWS.signer(settings.key_settings.issuer_key), salter }) @@ -100,7 +97,7 @@ describe("testcases", () => { expect(computed.parsed.disclosures).toEqual(expected.parsed.disclosures) const holder = new SD.Holder({ alg: 'ES256', - digester: SD.digester('sha-256'), + digester: testcase.digester('sha-256'), signer: spec.key_binding ? await SD.JWS.signer(settings.key_settings.holder_key) : undefined }) const vp = await holder.present({ @@ -122,7 +119,7 @@ describe("testcases", () => { } const verifier = new SD.Verifier({ alg: 'ES256', - digester: SD.digester('sha-256'), + digester: testcase.digester('sha-256'), verifier: issuerVerifier }) const verified = await verifier.verify({ diff --git a/test/cbor-map-sanity.test.ts b/test/cbor-map-sanity.test.ts new file mode 100644 index 0000000..40c3ec2 --- /dev/null +++ b/test/cbor-map-sanity.test.ts @@ -0,0 +1,11 @@ +import * as cbor from 'cbor-web' + +it('cbor maps', async () => { + const input = new Map(); + input.set('a', 'b') + input.set(1, 2) + input.set(true, false) + const encoded = await cbor.encodeAsync(input) + const decoded = await cbor.decodeFirst(encoded) + // console.log(decoded) +}); \ No newline at end of file diff --git a/test/e2e.test.ts b/test/e2e.test.ts index 28f2d28..4182375 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -2,7 +2,7 @@ import crypto from 'crypto' import moment from 'moment'; import { base64url, exportJWK, generateKeyPair } from 'jose'; import SD from "../src"; - +import testcase from './testcase' it('End to End Test', async () => { const alg = 'ES384' const iss = 'did:web:issuer.example' @@ -10,7 +10,7 @@ it('End to End Test', async () => { const aud = 'did:web:verifier.example' const issuerKeyPair = await generateKeyPair(alg) const holderKeyPair = await generateKeyPair(alg) - const digester = SD.digester('sha-256') + const digester = testcase.digester('sha-256') const issuerPublicKey = await exportJWK(issuerKeyPair.publicKey) const issuerPrivateKey = await exportJWK(issuerKeyPair.privateKey) const issuerSigner = await SD.JWS.signer(issuerPrivateKey) diff --git a/test/headers.test.ts b/test/headers.test.ts new file mode 100644 index 0000000..31ca13d --- /dev/null +++ b/test/headers.test.ts @@ -0,0 +1,105 @@ + +import moment from 'moment'; + +import SD from "../src"; + +import testcase from './testcase' + +const salter = testcase.salter + +it('W3C Example', async () => { + const alg = 'ES384' + const iss = 'did:web:issuer.example' + const nonce = '9876543210' + const aud = 'did:web:verifier.example' + const issuerKeyPair = await SD.JWK.generate(alg) + const holderKeyPair = await SD.JWK.generate(alg) + const digester = testcase.digester('sha-256') + const issuer = new SD.Issuer({ + alg, + kid: `${iss}#key-42`, + typ: `application/vc+ld+json+sd-jwt`, + cty: `application/vc+ld+json`, + iss, + digester, + signer: await SD.JWS.signer(issuerKeyPair.secretKeyJwk), + salter + }) + const vc = await issuer.issue({ + iat: moment().unix(), + exp: moment().add(1, 'month').unix(), + holder: holderKeyPair.publicKeyJwk, + claims: SD.YAML.load(` +"@context": + - https://www.w3.org/ns/credentials/v2 + - https://w3id.org/traceability/v1 +id: http://supply-chain.example/credentials/dd0c6f9a-5df6-40a3-bb34-863cd1fda606 +type: + - VerifiableCredential + - EntryNumberCredential +validFrom: ${moment().toISOString()} +validUntil: ${moment().add(1, 'month').toISOString()} +issuer: + type: + - Organization + id: ${iss} + name: ACME Customs Broker + !sd location: + type: + - Place + address: + type: + - PostalAddress + streetAddress: 123 Example Street + addressLocality: Toronto + addressRegion: ON + addressCountry: CA + postalCode: M3B 1A2 +credentialSubject: + type: + - EntryNumber + !sd entryNumber: "12345123456" +`) + }) + const holder = new SD.Holder({ + alg, + digester, + signer: await SD.JWS.signer(holderKeyPair.secretKeyJwk) + }) + const vp = await holder.present({ + credential: vc, + nonce, + aud, + disclosure: SD.YAML.load(` +issuer: + location: False +credentialSubject: + entryNumber: True + `), + }) + const verifier = new SD.Verifier({ + alg, + digester, + verifier: { + verify: async (token: string) => { + const parsed = SD.Parse.compact(token) + const verifier = await SD.JWS.verifier(issuerKeyPair.publicKeyJwk) + return verifier.verify(parsed.jwt) + } + } + }) + const verified = await verifier.verify({ + presentation: vp, + nonce, + aud + }) + expect(verified.claimset.issuer.location).toBeUndefined() + expect(verified.claimset.credentialSubject.entryNumber).toBe('12345123456') + expect(JSON.stringify(verified.protectedHeader)).toBe(JSON.stringify({ + "alg": "ES384", + "kid": "did:web:issuer.example#key-42", + "typ": "application/vc+ld+json+sd-jwt", + "cty": "application/vc+ld+json" + })) + +}); \ No newline at end of file diff --git a/test/json-pointer.test.ts b/test/json-pointer.test.ts index 8593898..3d4f1a9 100644 --- a/test/json-pointer.test.ts +++ b/test/json-pointer.test.ts @@ -2,7 +2,7 @@ import crypto from 'crypto' import moment from 'moment'; import { base64url, exportJWK, generateKeyPair } from 'jose'; - +import testcase from './testcase' import SD from "../src"; it('JSON Pointer', async () => { @@ -12,7 +12,7 @@ it('JSON Pointer', async () => { const aud = 'did:web:verifier.example' const issuerKeyPair = await generateKeyPair(alg) const holderKeyPair = await generateKeyPair(alg) - const digester = SD.digester('sha-256') + const digester = testcase.digester('sha-256') const issuerPrivateKey = await exportJWK(issuerKeyPair.privateKey) const issuerSigner = await SD.JWS.signer(issuerPrivateKey) const holderPublicKey = await exportJWK(holderKeyPair.publicKey) @@ -67,7 +67,7 @@ expect_verified_user_claims: }) // pointers - const result = SD.YAML.tokenToSchema(vc, { digester: SD.digester }) + const result = await SD.YAML.tokenToSchema(vc, { digester }) // console.log(result) const holder = new SD.Holder({ diff --git a/test/precondition.test.ts b/test/precondition.test.ts index 596e4f7..16b1a53 100644 --- a/test/precondition.test.ts +++ b/test/precondition.test.ts @@ -5,6 +5,8 @@ import crypto from 'crypto' import { base64url, exportJWK, generateKeyPair } from 'jose'; +import testcase from './testcase' + const salter = () => { return base64url.encode(crypto.randomBytes(16)); } @@ -13,7 +15,7 @@ it('throws when _sd is present in user claims', async () => { expect.assertions(1) const alg = 'ES384' const issuerKeyPair = await generateKeyPair(alg) - const digester = SD.digester('sha-256') + const digester = testcase.digester('sha-256') const issuerPrivateKey = await exportJWK(issuerKeyPair.privateKey) const issuerSigner = await SD.JWS.signer(issuerPrivateKey) const issuer = new SD.Issuer({ diff --git a/test/resolver.test.ts b/test/resolver.test.ts index efa74af..1fb1412 100644 --- a/test/resolver.test.ts +++ b/test/resolver.test.ts @@ -2,7 +2,7 @@ import crypto from 'crypto' import moment from 'moment'; import { base64url, decodeJwt, exportJWK, generateKeyPair, decodeProtectedHeader, calculateJwkThumbprintUri } from 'jose'; - +import testcase from './testcase' import SD from "../src"; it('End to End Test', async () => { @@ -12,7 +12,7 @@ it('End to End Test', async () => { const aud = 'did:web:verifier.example' const issuerKeyPair = await generateKeyPair(alg) const holderKeyPair = await generateKeyPair(alg) - const digester = SD.digester('sha-256') + const digester = testcase.digester('sha-256') const issuerPublicKey = await exportJWK(issuerKeyPair.publicKey) const issuerPrivateKey = await exportJWK(issuerKeyPair.privateKey) diff --git a/test/sd-cwt/cose-key-thumbprint.test.ts b/test/sd-cwt/cose-key-thumbprint.test.ts new file mode 100644 index 0000000..3ed8b01 --- /dev/null +++ b/test/sd-cwt/cose-key-thumbprint.test.ts @@ -0,0 +1,21 @@ +import crypto from 'crypto' +import * as cbor from 'cbor-web' +// https://www.ietf.org/archive/id/draft-ietf-cose-key-thumbprint-01.html#section-6 +// { +// 1:2, +// -1:1, +// -2:h'65eda5a12577c2bae829437fe338701a +// 10aaa375e1bb5b5de108de439c08551d', +// -3:h'1e52ed75701163f7f9e40ddf9f341b3d +// c9ba860af7e0ca7ca7e9eecd0084d19c' +// } +it('https://www.ietf.org/archive/id/draft-ietf-cose-key-thumbprint', async () => { + const coseKey = new Map(); + coseKey.set(1, 2) + coseKey.set(-1, 1) + coseKey.set(-2, Buffer.from('65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d', 'hex')) + coseKey.set(-3, Buffer.from('1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c', 'hex')) + const encoded = cbor.encode(coseKey) + const digest = crypto.createHash("sha256").update(encoded).digest() + expect(digest.toString('hex')).toBe('496bd8afadf307e5b08c64b0421bf9dc01528a344a43bda88fadd1669da253ec') +}); \ No newline at end of file diff --git a/test/sd-cwt/generate-cd-cwt-testcases.test.ts b/test/sd-cwt/generate-cd-cwt-testcases.test.ts new file mode 100644 index 0000000..1b7ecc9 --- /dev/null +++ b/test/sd-cwt/generate-cd-cwt-testcases.test.ts @@ -0,0 +1,77 @@ +import fs from 'fs' +import yaml from 'yaml' +import crypto from 'crypto' + +import SD from "../../src"; +import { base64url } from 'jose'; +import * as cbor from 'cbor-web' +const salter = async () => { + return crypto.randomBytes(16); +} + +const digester = { + name: 'sha-256' as 'sha-256', + digest: async (cbor: Buffer) => { + return crypto.createHash("sha256").update(cbor).digest(); + } +} + +const testcases = fs.readdirSync('test/sd-cwt/testcases', { withFileTypes: true }); + +const focusTestNames:string[] = [ + // 'data-types-arrays' +] + +let issuer: any; +let holder: any; + +describe("testcases", () => { + beforeAll(async()=>{ + issuer = await SD.v2.Issuer.build({ + alg: -35, + digester, + salter + }) + holder = await SD.v2.Holder.build({ + alg: -35, + digester, + salter + }) + }) + it('issuer and holder must be defined', async () => { + expect(issuer).toBeDefined(); + expect(holder).toBeDefined(); + }); + for (const test of testcases){ + if (focusTestNames.length && !focusTestNames.includes(test.name)){ + continue + } + it(test.name, async () => { + const payload = fs.readFileSync(`test/sd-cwt/testcases/${test.name}/payload.yaml`).toString() + const disclosure = fs.readFileSync(`test/sd-cwt/testcases/${test.name}/payload-disclosure.yaml`).toString() + const vc = await issuer.issue({ + claims: payload, + }) + const vp = await holder.present({ + vc, + disclose: disclosure + }) + const verified = await issuer.verify({ + vc: vp + }) + const spec = new yaml.YAMLMap() + const decodedVc = await cbor.decodeFirst(vc) + const protectedHeader = await cbor.decodeFirst(decodedVc.value[0]) + spec.add(new yaml.Pair('protected_header', protectedHeader)) + spec.add(new yaml.Pair('payload', SD.YAML.load(payload))) + spec.add(new yaml.Pair('disclosure', SD.YAML.load(disclosure))) + spec.add(new yaml.Pair('issuer_key', issuer.config.publicKeyJwk)) + spec.add(new yaml.Pair('holder_key', holder.config.publicKeyJwk)) + spec.add(new yaml.Pair('issuance', base64url.encode(vc))) + spec.add(new yaml.Pair('presentation', base64url.encode(vp))) + spec.add(new yaml.Pair('verified', verified)) + // console.log(verified) + fs.writeFileSync(`test/sd-cwt/testcases/${test.name}/spec.yaml`, SD.YAML.dumps(spec)) + }) + } +}); \ No newline at end of file diff --git a/test/sd-cwt/testcases/data-types-arrays/payload-disclosure.yaml b/test/sd-cwt/testcases/data-types-arrays/payload-disclosure.yaml new file mode 100644 index 0000000..62e5968 --- /dev/null +++ b/test/sd-cwt/testcases/data-types-arrays/payload-disclosure.yaml @@ -0,0 +1,15 @@ +array_1: + - True + - True +array_2: + - True + - - True +array_3: + - True + - False +array_4: + - True + - - False +array_5: + - True + - False \ No newline at end of file diff --git a/test/sd-cwt/testcases/data-types-arrays/payload.yaml b/test/sd-cwt/testcases/data-types-arrays/payload.yaml new file mode 100644 index 0000000..7b7e36a --- /dev/null +++ b/test/sd-cwt/testcases/data-types-arrays/payload.yaml @@ -0,0 +1,21 @@ + +array_1: + - "bar" + - !sd "redactable_1" + +array_2: + - 10 + - - !sd "redactable_2" + +array_3: + - "bar" + - !sd "redactable_3" + +array_4: +- 10 +- - !sd "redactable_4" + +array_5: +- 10 +- !sd + - "redactable_4" \ No newline at end of file diff --git a/test/sd-cwt/testcases/data-types-arrays/spec.yaml b/test/sd-cwt/testcases/data-types-arrays/spec.yaml new file mode 100644 index 0000000..444d5e6 --- /dev/null +++ b/test/sd-cwt/testcases/data-types-arrays/spec.yaml @@ -0,0 +1,67 @@ +protected_header: + 1: -35 +payload: + array_1: + - "bar" + - !sd "redactable_1" + + array_2: + - 10 + - - !sd "redactable_2" + + array_3: + - "bar" + - !sd "redactable_3" + + array_4: + - 10 + - - !sd "redactable_4" + + array_5: + - 10 + - !sd + - "redactable_4" +disclosure: + array_1: + - True + - True + array_2: + - True + - - True + array_3: + - True + - False + array_4: + - True + - - False + array_5: + - True + - False +issuer_key: + kty: EC + x: oV7sQx_8WkK0Xv8SG-rKWkJmJDUojEiFUGbdFBW_yRSVouEweBsclVxqNanzI3tK + y: eHdxu_FPDCC3ZI2X5en14eh1MZTnFzQXPR0I0iCNyiDv16sLJ-objWUc5vegIT22 + crv: P-384 + alg: ES384 +holder_key: + kty: EC + x: EtnDHrvppwO2F4oBNBt83LzJnPFXdQVpvRco1cj3EFNvkzI_qy0C9P3lAGtFks1b + y: 8cGLlYEOfwKDRsRItvtP9KZvujNklScB8HihCRX9dY3pzZVItt_ryGGalNLaLv25 + crv: P-384 + alg: ES384 +issuance: 0oREoQE4IqEZAU2FWB-CUF5dlEOQD8R50rMiP-aPpbpscmVkYWN0YWJsZV8xWB-CUM7paeOrnOrWw3pLLIJ42rlscmVkYWN0YWJsZV8yWB-CUKwDX2bQDMJ-ohLklplvOv9scmVkYWN0YWJsZV8zWB-CUJjoVQIlZT-VVXHr3HIxHb1scmVkYWN0YWJsZV80WCCCUCN6-rWeBcd1JZwf-kDkHpuBbHJlZGFjdGFibGVfNNhAWPSlZ2FycmF5XzGCY2JhcqEY3lggGsE0IXy17qHjFK3BczKgTiZPEn-mV_vEB7hh3LvoAxtnYXJyYXlfMoIKgaEY3lggjbE4CtUU-yac3di1JqGx9tiM_xPapcMIHukTxXVPoVZnYXJyYXlfM4JjYmFyoRjeWCDTkIX3jxYcqL4cGN7Sn7IzsCbFT4BWWs084CGiACPQJ2dhcnJheV80ggqBoRjeWCAmVXfTfGTmQ3xEUWTxbeb9rNhRba2ajZfuOrXwCYIEjmdhcnJheV81ggqhGN5YIK2JxHL7cDimjDTLcRpuHs8dVk2URvJBtX-0L1r2gonEWGCBDUsC5I1sbmWeySg838I_5CoUdbJCnOOIiSnj_Hh_FTodL4Jg9sHiV4LnpfwTiObXFlgT6_9Fk6MUr-RZrWl5AuZWjDjqf3ejO_KivPFvstEiV3hBQn-REGT8AcTB_yw +presentation: 0oREoQE4IqEZAU2CWB-CUF5dlEOQD8R50rMiP-aPpbpscmVkYWN0YWJsZV8xWB-CUM7paeOrnOrWw3pLLIJ42rlscmVkYWN0YWJsZV8y2EBY9KVnYXJyYXlfMYJjYmFyoRjeWCAawTQhfLXuoeMUrcFzMqBOJk8Sf6ZX-8QHuGHcu-gDG2dhcnJheV8yggqBoRjeWCCNsTgK1RT7Jpzd2LUmobH22Iz_E9qlwwge6RPFdU-hVmdhcnJheV8zgmNiYXKhGN5YINOQhfePFhyovhwY3tKfsjOwJsVPgFZazTzgIaIAI9AnZ2FycmF5XzSCCoGhGN5YICZVd9N8ZOZDfERRZPFt5v2s2FFtrZqNl-46tfAJggSOZ2FycmF5XzWCCqEY3lggrYnEcvtwOKaMNMtxGm4ezx1WTZRG8kG1f7QvWvaCicRYYIENSwLkjWxuZZ7JKDzfwj_kKhR1skKc44iJKeP8eH8VOh0vgmD2weJXguel_BOI5tcWWBPr_0WToxSv5FmtaXkC5laMOOp_d6M78qK88W-y0SJXeEFCf5EQZPwBxMH_LA +verified: + array_1: + - bar + - redactable_1 + array_2: + - 10 + - - redactable_2 + array_3: + - bar + array_4: + - 10 + - [] + array_5: + - 10 diff --git a/test/sd-cwt/testcases/no-selective-disclosure/payload-disclosure.yaml b/test/sd-cwt/testcases/no-selective-disclosure/payload-disclosure.yaml new file mode 100644 index 0000000..e69de29 diff --git a/test/sd-cwt/testcases/no-selective-disclosure/payload.yaml b/test/sd-cwt/testcases/no-selective-disclosure/payload.yaml new file mode 100644 index 0000000..395e061 --- /dev/null +++ b/test/sd-cwt/testcases/no-selective-disclosure/payload.yaml @@ -0,0 +1,11 @@ +# https://www.iana.org/assignments/cwt/cwt.xhtml +1: 'did:web:issuer.example' +string: a string +number: 10 +10: 100 +arr1: + - "bar" + - "baz" +arr2: + - 10 + - 20 \ No newline at end of file diff --git a/test/sd-cwt/testcases/no-selective-disclosure/spec.yaml b/test/sd-cwt/testcases/no-selective-disclosure/spec.yaml new file mode 100644 index 0000000..375b291 --- /dev/null +++ b/test/sd-cwt/testcases/no-selective-disclosure/spec.yaml @@ -0,0 +1,40 @@ +protected_header: + 1: -35 +payload: + # https://www.iana.org/assignments/cwt/cwt.xhtml + 1: 'did:web:issuer.example' + string: a string + number: 10 + 10: 100 + arr1: + - "bar" + - "baz" + arr2: + - 10 + - 20 +disclosure: null +issuer_key: + kty: EC + x: oV7sQx_8WkK0Xv8SG-rKWkJmJDUojEiFUGbdFBW_yRSVouEweBsclVxqNanzI3tK + y: eHdxu_FPDCC3ZI2X5en14eh1MZTnFzQXPR0I0iCNyiDv16sLJ-objWUc5vegIT22 + crv: P-384 + alg: ES384 +holder_key: + kty: EC + x: EtnDHrvppwO2F4oBNBt83LzJnPFXdQVpvRco1cj3EFNvkzI_qy0C9P3lAGtFks1b + y: 8cGLlYEOfwKDRsRItvtP9KZvujNklScB8HihCRX9dY3pzZVItt_ryGGalNLaLv25 + crv: P-384 + alg: ES384 +issuance: 0oREoQE4IqEZAU2A2EBYSqYBdmRpZDp3ZWI6aXNzdWVyLmV4YW1wbGVmc3RyaW5naGEgc3RyaW5nZm51bWJlcgoKGGRkYXJyMYJjYmFyY2JhemRhcnIyggoUWGC0F2KuJ4fo5_3PPGhd_HHXzSUcxKOOEWRmuzslsCfh-qI5fpYnlyqT8Z1cz70eHJjleTCBYjnOrUFRQFnBqcUQKxTf3Zveq4yZGNr1Q3eiEz0GHV-kJUx9LVAorj_ibaM +presentation: 0oREoQE4IqEZAU2A2EBYSqYBdmRpZDp3ZWI6aXNzdWVyLmV4YW1wbGVmc3RyaW5naGEgc3RyaW5nZm51bWJlcgoKGGRkYXJyMYJjYmFyY2JhemRhcnIyggoUWGC0F2KuJ4fo5_3PPGhd_HHXzSUcxKOOEWRmuzslsCfh-qI5fpYnlyqT8Z1cz70eHJjleTCBYjnOrUFRQFnBqcUQKxTf3Zveq4yZGNr1Q3eiEz0GHV-kJUx9LVAorj_ibaM +verified: + 1: did:web:issuer.example + string: a string + number: 10 + 10: 100 + arr1: + - bar + - baz + arr2: + - 10 + - 20 diff --git a/test/testcase.ts b/test/testcase.ts index 4d343bc..0925f4d 100644 --- a/test/testcase.ts +++ b/test/testcase.ts @@ -6,10 +6,22 @@ import { base64url, decodeJwt, decodeProtectedHeader } from "jose"; import YAML from "../src/YAML-SD"; import Parse from "../src/Parse"; -const digester = (json: string) => { - return base64url.encode(crypto.createHash("sha256").update(json).digest()); +const digester = (name: 'sha-256' = 'sha-256') => { + if (name !== 'sha-256'){ + throw new Error('hash function not supported') + } + return { + name, + digest: async (json: string) => { + return base64url.encode(crypto.createHash("sha256").update(json).digest()); + } + }; }; +const salter = () => { + return base64url.encode(crypto.randomBytes(16)); +} + const getSpec = (path: string) => { const spec = fs.readFileSync(path, "utf8"); const doc = YAML.parseCustomTags(spec); @@ -101,6 +113,7 @@ const api = { getExpectedPayload, getSpec, getSalter, + salter, decodeIssuanceForm, decodeExpectedIssuance, getUserClaims, diff --git a/test/token-to-schema.test.ts b/test/token-to-schema.test.ts index 872f37a..8fa8ed7 100644 --- a/test/token-to-schema.test.ts +++ b/test/token-to-schema.test.ts @@ -1,15 +1,17 @@ import crypto from 'crypto' import { base64url, exportJWK, generateKeyPair } from 'jose'; - +import testcase from './testcase' import SD from "../src"; +const digester = testcase.digester('sha-256') + describe('token to schema', () => { it.todo('rescursions and other advanced testcases for sanity') it('array_with_recursive_sd', async () => { const alg = 'ES384' const issuerKeyPair = await generateKeyPair(alg) - const digester = SD.digester('sha-256') + const issuerPrivateKey = await exportJWK(issuerKeyPair.privateKey) const issuerSigner = await SD.JWS.signer(issuerPrivateKey) const salter = () => { @@ -54,9 +56,10 @@ describe('token to schema', () => { const vc = await issuer.issue({ claims: schema.get('user_claims'), }) - const result = SD.YAML.tokenToSchema(vc, { digester: SD.digester }) + const result = await SD.YAML.tokenToSchema(vc, { digester }) // console.log(result.yaml) // console.log(result.json) // console.log(JSON.stringify(result.pretty, null ,2)) + // console.log(JSON.stringify(result.pointers, null ,2)) }); }) diff --git a/test/vc-jose-cose-test/payload-disclosure.yaml b/test/vc-jose-cose-test/payload-disclosure.yaml new file mode 100644 index 0000000..7f70c9b --- /dev/null +++ b/test/vc-jose-cose-test/payload-disclosure.yaml @@ -0,0 +1,10 @@ +"@context": + - True + - False +type: + - True + - True +credentialSubject: True +proof: + verificationMethod: True + proofValue: False \ No newline at end of file diff --git a/test/vc-jose-cose-test/payload.yaml b/test/vc-jose-cose-test/payload.yaml new file mode 100644 index 0000000..2b118e3 --- /dev/null +++ b/test/vc-jose-cose-test/payload.yaml @@ -0,0 +1,21 @@ +"@context": + - https://www.w3.org/ns/credentials/v2 + - !sd https://www.w3.org/ns/credentials/examples/v2 +id: http://university.example/credentials/1872 +type: + - VerifiableCredential + - !sd ExampleAlumniCredential +issuer: https://university.example/issuers/565049 +validFrom: 2010-01-01T19:23:24Z +credentialSubject: + id: did:example:ebfeb1f712ebc6f1c276e12ec21 + alumniOf: + id: !sd did:example:c276e12ec21ebfeb1f712ebc6f1 + name: Example University +!sd proof: + type: DataIntegrityProof + cryptosuite: eddsa-2022 + created: 2023-06-18T21:19:10Z + proofPurpose: assertionMethod + !sd verificationMethod: https://university.example/issuers/565049#key-123 + !sd proofValue: zQeVbY4oey5q2M3XKaxup3tmzN4DRFTLVqpLMweBrSxMY2xHX5XTYV8nQApmEcqaqA3Q1gVHMrXFkXJeV6doDwLWx \ No newline at end of file diff --git a/test/vc-jose-cose-test/vc-jose-cose.test.ts b/test/vc-jose-cose-test/vc-jose-cose.test.ts new file mode 100644 index 0000000..af92d52 --- /dev/null +++ b/test/vc-jose-cose-test/vc-jose-cose.test.ts @@ -0,0 +1,59 @@ +import fs from 'fs' +import SD from "../../src"; + +import testcase from '../testcase' + +const salter = testcase.salter + +it('W3C VC JOSE COSE Test', async () => { + const alg = 'ES384' + const nonce = '9876543210' + const aud = 'did:web:verifier.example' + const issuerKeyPair = await SD.JWK.generate(alg) + const holderKeyPair = await SD.JWK.generate(alg) + const digester = testcase.digester('sha-256') + const issuer = new SD.Issuer({ + alg, + digester, + signer: await SD.JWS.signer(issuerKeyPair.secretKeyJwk), + salter + }) + const holder = new SD.Holder({ + alg, + digester, + signer: await SD.JWS.signer(holderKeyPair.secretKeyJwk) + }) + const verifier = new SD.Verifier({ + alg, + digester, + verifier: { + verify: async (token: string) => { + const parsed = SD.Parse.compact(token) + const verifier = await SD.JWS.verifier(issuerKeyPair.publicKeyJwk) + return verifier.verify(parsed.jwt) + } + } + }) + const claimsYaml = fs.readFileSync(`test/vc-jose-cose-test/payload.yaml`).toString() + const claims = SD.YAML.load(claimsYaml) + const vc = await issuer.issue({ + holder: holderKeyPair.publicKeyJwk, + claims + }) + + const claimsDisclosureYaml = fs.readFileSync(`test/vc-jose-cose-test/payload-disclosure.yaml`).toString() + const disclosure = SD.YAML.load(claimsDisclosureYaml) + const vp = await holder.present({ + credential: vc, + nonce, + aud, + disclosure, + }) + + const verified = await verifier.verify({ + presentation: vp, + nonce, + aud + }) + expect(verified.claimset.proof.created).toBe('2023-06-18T21:19:10Z') +}); \ No newline at end of file diff --git a/test/w3c.test.ts b/test/w3c.test.ts index 388616f..a22fcf2 100644 --- a/test/w3c.test.ts +++ b/test/w3c.test.ts @@ -1,29 +1,31 @@ -import crypto from 'crypto' + import moment from 'moment'; -import { base64url, exportJWK, generateKeyPair } from 'jose'; + import SD from "../src"; +import testcase from './testcase' + +const salter = testcase.salter + it('W3C Example', async () => { const alg = 'ES384' const iss = 'did:web:issuer.example' const nonce = '9876543210' const aud = 'did:web:verifier.example' - const issuerKeyPair = await generateKeyPair(alg) - const holderKeyPair = await generateKeyPair(alg) - const digester = SD.digester('sha-256') + const issuerKeyPair = await SD.JWK.generate(alg) + const holderKeyPair = await SD.JWK.generate(alg) + const digester = testcase.digester('sha-256') const issuer = new SD.Issuer({ alg, iss, digester, - signer: await SD.JWS.signer(await exportJWK(issuerKeyPair.privateKey)), - salter: () => { - return base64url.encode(crypto.randomBytes(16)); - } + signer: await SD.JWS.signer(issuerKeyPair.secretKeyJwk), + salter }) const vc = await issuer.issue({ iat: moment().unix(), exp: moment().add(1, 'month').unix(), - holder: await exportJWK(holderKeyPair.publicKey), + holder: holderKeyPair.publicKeyJwk, claims: SD.YAML.load(` "@context": - https://www.w3.org/ns/credentials/v2 @@ -59,7 +61,7 @@ credentialSubject: const holder = new SD.Holder({ alg, digester, - signer: await SD.JWS.signer(await exportJWK(holderKeyPair.privateKey)) + signer: await SD.JWS.signer(holderKeyPair.secretKeyJwk) }) const vp = await holder.present({ credential: vc, @@ -78,7 +80,7 @@ credentialSubject: verifier: { verify: async (token :string) => { const parsed = SD.Parse.compact(token) - const verifier = await SD.JWS.verifier(await exportJWK(issuerKeyPair.publicKey)) + const verifier = await SD.JWS.verifier(issuerKeyPair.publicKeyJwk) return verifier.verify(parsed.jwt) } } @@ -91,4 +93,5 @@ credentialSubject: expect(verified.claimset.issuer.location).toBeUndefined() expect(verified.claimset.credentialSubject.entryNumber).toBe('12345123456') // console.log(JSON.stringify(verified, null, 2)) + // console.log(vc) }); \ No newline at end of file diff --git a/test/yaml-spec-issuance.test.ts b/test/yaml-spec-issuance.test.ts index 494ff04..3727e72 100644 --- a/test/yaml-spec-issuance.test.ts +++ b/test/yaml-spec-issuance.test.ts @@ -5,12 +5,14 @@ import { Scalar, YAMLSeq, Pair } from "yaml"; const testcases = fs.readdirSync("testcases/", { withFileTypes: true }); +const digester = testcase.digester('sha-256') + describe("array_recursive_sd", () => { const test = { name: "array_recursive_sd" }; it(test.name, async () => { const spec = testcase.getSpec(`testcases/${test.name}/specification.yml`) const salter = testcase.getSalter(`testcases/${test.name}/sd_jwt_issuance.txt`) - const issuedPayload = SD.YAML.issuancePayload(spec.get("user_claims"), { + const issuedPayload = await SD.YAML.issuancePayload(spec.get("user_claims"), { disclosures: {}, salter: (item: any)=>{ const testValue = JSON.stringify(item) @@ -20,7 +22,7 @@ describe("array_recursive_sd", () => { const salt = salter(item) return salt }, - digester: testcase.digester, + digester, }); const expectedPayload = testcase.getExpectedPayload( `testcases/${test.name}/sd_jwt_issuance_payload.json` @@ -34,7 +36,7 @@ describe("recursions", () => { it(test.name, async () => { const spec = testcase.getSpec(`testcases/${test.name}/specification.yml`) // console.log(testcase.decodeExpectedIssuance(`testcases/${test.name}/sd_jwt_issuance.txt`)) - const issuedPayload = SD.YAML.issuancePayload(spec.get("user_claims"), { + const issuedPayload = await SD.YAML.issuancePayload(spec.get("user_claims"), { disclosures: {}, salter: (item: any)=>{ if (item instanceof Scalar){ @@ -105,7 +107,7 @@ describe("recursions", () => { } throw new Error('unhandled hard coded salt') }, - digester: testcase.digester, + digester, }); const expectedPayload = testcase.getExpectedPayload( `testcases/${test.name}/sd_jwt_issuance_payload.json` @@ -125,10 +127,10 @@ describe("yaml specification", () => { } it(test.name, async () => { const spec = testcase.getSpec(`testcases/${test.name}/specification.yml`) - const issuedPayload = SD.YAML.issuancePayload(spec.get("user_claims"), { + const issuedPayload = await SD.YAML.issuancePayload(spec.get("user_claims"), { disclosures: {}, salter: testcase.getSalter(`testcases/${test.name}/sd_jwt_issuance.txt`), - digester: testcase.digester, + digester, }); const expectedPayload = testcase.getExpectedPayload( `testcases/${test.name}/sd_jwt_issuance_payload.json`