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