diff --git a/.gitignore b/.gitignore
index cbe4d09..c3629d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
dist/
node_modules/
package-lock.json
+.idea
diff --git a/README.md b/README.md
index bb513e1..8478e67 100644
--- a/README.md
+++ b/README.md
@@ -47,10 +47,12 @@ On by default:
* `eddsa-rdfc-2022` - https://www.w3.org/TR/vc-di-eddsa/
* `ecdsa-sd-2023` - https://www.w3.org/TR/vc-di-ecdsa/
* `bbs-2023` - https://www.w3.org/TR/vc-di-bbs/
-* `vc-jwt` - https://w3c.github.io/vc-jose-cose/
+* `jose` - https://w3c.github.io/vc-jose-cose/#with-jose
Optional:
* `Ed25519Signature2020` - https://www.w3.org/TR/vc-di-eddsa/#the-ed25519signature2020-suite
+* `sd-jwt` - https://www.w3.org/TR/vc-jose-cose/#with-sd-jwt
+* `cose` - https://www.w3.org/TR/vc-jose-cose/#securing-with-cose
```html
Only Certain Tabs Example
tabs are displayed.
+ data-vc-tabs="jose">
{
@@ -129,47 +129,47 @@ File Hash Examples
raw (`openssl dgst -sha256`):
+ data-hash-format="openssl dgst -sha256">
digestSRI (sha2-256 base64pad):
+ data-hash-format="sri sha2-256">
digestSRI (sha2-384 base64pad):
+ data-hash-format="sri sha2-384">
digestMultibase (sha2-256 base16):
+ data-hash-format="multihash sha2-256 base16">
digestMultibase (sha2-256 base58btc):
+ data-hash-format="multihash sha2-256 base58btc">
digestMultibase (sha2-256 base64-url-nopad):
+ data-hash-format="multihash sha2-256">
digestMultibase (sha2-384 base64-url-nopad):
+ data-hash-format="multihash sha2-384">
digestMultibase (sha3-256 base64-url-nopad):
+ data-hash-format="multihash sha3-256">
digestMultibase (sha3-384 base64-url-nopad):
+ data-hash-format="multihash sha3-384">
diff --git a/index.js b/index.js
index 746faa3..d75b912 100644
--- a/index.js
+++ b/index.js
@@ -1,40 +1,30 @@
-import * as bbs2023Cryptosuite from '@digitalbazaar/bbs-2023-cryptosuite';
-import * as Bls12381Multikey from '@digitalbazaar/bls12-381-multikey';
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
-import * as ecdsaRdfc2019Cryptosuite from
- '@digitalbazaar/ecdsa-rdfc-2019-cryptosuite';
import * as ecdsaSd2023Cryptosuite
from '@digitalbazaar/ecdsa-sd-2023-cryptosuite';
import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
import * as examples1Context from '@digitalbazaar/credentials-examples-context';
-import * as jose from 'jose';
-import * as mfHasher from 'multiformats/hashes/hasher';
import * as odrlContext from '@digitalbazaar/odrl-context';
-import {base64pad, base64url} from 'multiformats/bases/base64';
import {defaultDocumentLoader, issue} from '@digitalbazaar/vc';
import {extendContextLoader, purposes} from 'jsonld-signatures';
-import {sha3_256, sha3_384} from '@noble/hashes/sha3';
-import {base16} from 'multiformats/bases/base16';
-import {base58btc} from 'multiformats/bases/base58';
+import {getCoseHtml, getJoseHtml, getSdJwtHtml} from './src/html';
import {DataIntegrityProof} from '@digitalbazaar/data-integrity';
import ed25519Context from 'ed25519-signature-2020-context';
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
-import {Ed25519VerificationKey2020} from
- '@digitalbazaar/ed25519-verification-key-2020';
-import {cryptosuite as eddsaRdfc2022CryptoSuite} from
- '@digitalbazaar/eddsa-rdfc-2022-cryptosuite';
+import {
+ Ed25519VerificationKey2020,
+} from '@digitalbazaar/ed25519-verification-key-2020';
+import {
+ cryptosuite as eddsaRdfc2022CryptoSuite,
+} from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite';
import examples2Context from './contexts/credentials/examples/v2';
-import {sha256} from '@noble/hashes/sha256';
-import {sha384} from '@noble/hashes/sha512';
+import {getCoseExample} from './src/cose';
+import {getJoseExample} from './src/jose';
+import {getSdJwtExample} from './src/sd-jwt';
+import {privateKey} from './src/common';
// default types
-const TAB_TYPES = [
- 'ecdsa-rdfc-2019',
- 'eddsa-rdfc-2022',
- 'ecdsa-sd-2023',
- 'bbs-2023',
- 'vc-jwt'
-];
+const TAB_TYPES = ['ecdsa-sd-2023', 'eddsa-rdfc-2022',
+ 'jose', 'sd-jwt', 'cose'];
// additional types: Ed25519Signature2020
// purposes used below
@@ -56,136 +46,12 @@ const documentLoader = extendContextLoader(async function documentLoader(url) {
return {
contextUrl: null,
documentUrl: url,
- document: context
+ document: context,
};
}
return defaultDocumentLoader(url);
});
-async function createBBSExampleProof() {
- const key = await Bls12381Multikey.generateBbsKeyPair({
- algorithm: Bls12381Multikey.ALGORITHMS.BBS_BLS12381_SHA256
- });
-
- const proof = new DataIntegrityProof({
- signer: key.signer(),
- cryptosuite: bbs2023Cryptosuite.createSignCryptosuite({
- mandatoryPointers: ['/issuer']
- })
- });
-
- return {
- proof,
- key,
- label: 'bbs'
- };
-}
-
-async function createEcdsaRdfc2019ExampleProof() {
- const key = await EcdsaMultikey
- .generate({curve: 'P-256'});
-
- // ecdsa-rdfc-2019
- const {cryptosuite: rdfcCryptosuite} = ecdsaRdfc2019Cryptosuite;
- const proof = new DataIntegrityProof({
- signer: key.signer(),
- cryptosuite: rdfcCryptosuite
- });
-
- return {
- proof,
- key,
- label: 'ecdsa'
- };
-}
-
-async function createEcdsaSd2023ExampleProof() {
- const key = await EcdsaMultikey
- .generate({curve: 'P-256'});
-
- // ecdsa-sd-2023
- const {createSignCryptosuite} = ecdsaSd2023Cryptosuite;
- const proof = new DataIntegrityProof({
- signer: key.signer(),
- cryptosuite: createSignCryptosuite({
- mandatoryPointers: ['/issuer']
- })
- });
-
- return {
- proof,
- key,
- label: 'ecdsa-sd'
- };
-}
-
-async function createEddsaRdfc2022ExampleProof() {
- // Ed25519Signature2020
- const keyPairEd25519VerificationKey2020 = await Ed25519VerificationKey2020
- .generate();
-
- const key = await Ed25519Multikey
- .from(keyPairEd25519VerificationKey2020);
-
- // eddsa-rdfc-2022
- const proof = new DataIntegrityProof({
- signer: key.signer(),
- cryptosuite: eddsaRdfc2022CryptoSuite
- });
-
- return {
- proof,
- key,
- label: 'eddsa'
- };
-}
-
-// convert an XML Schema v1.` Datetime value to a UNIX timestamp
-function xmlDateTimeToUnixTimestamp(xmlDateTime) {
- if(!xmlDateTime) {
- return undefined;
- }
-
- return Date.parse(xmlDateTime) / 1000;
-}
-
-// transform the input credential to a JWT
-async function transformToJwt({credential, kid, jwk}) {
- const header = {alg: 'ES256', typ: 'JWT', kid};
- const payload = {
- vc: credential
- };
- if(credential.expirationDate) {
- payload.exp = xmlDateTimeToUnixTimestamp(credential.expirationDate);
- }
- if(credential.issuer) {
- payload.iss = credential.issuer;
- }
- if(credential.issuanceDate) {
- payload.nbf = xmlDateTimeToUnixTimestamp(credential.issuanceDate);
- }
- if(credential.id) {
- payload.jti = credential.id;
- }
- if(credential.credentialSubject.id) {
- payload.sub = credential.credentialSubject.id;
- }
-
- // create the JWT description
- let description = '---------------- JWT header ---------------\n' +
- JSON.stringify(header, null, 2);
- description += '\n\n--------------- JWT payload ---------------\n' +
- '// NOTE: The example below uses a valid VC-JWT serialization\n' +
- '// that duplicates the iss, nbf, jti, and sub fields in the\n' +
- '// Verifiable Credential (vc) field.\n\n' +
- JSON.stringify(payload, null, 2);
- const jwt = await new jose.SignJWT(payload)
- .setProtectedHeader(header)
- .sign(jwk.privateKey);
-
- return description + '\n\n--------------- JWT ---------------\n\n' + jwt;
-}
-
async function attachProof({credential, suite}) {
const credentialCopy = JSON.parse(JSON.stringify(credential));
const options = {credential: credentialCopy, suite, documentLoader};
@@ -241,6 +107,7 @@ function addVcExampleStyles() {
cursor: pointer;
transition: all 0.3s;
}
+
.vc-tab:hover label {
border-left-color: #333;
border-top-color: #333;
@@ -248,6 +115,156 @@ function addVcExampleStyles() {
color: #333;
}
+ .vc-jose-cose-tabbed, .vc-jose-cose-tabbed-jwt, .vc-jose-cose-tabbed-sd-jwt, .vc-jose-cose-tabbed-cose,
+ .sd-jwt-tabbed {
+ overflow-x: hidden;
+ margin: 0 0;
+ }
+
+ .vc-jose-cose-tabbed h1, .vc-jose-cose-jwt-tabbed h1, .vc-jose-cose-sd-jwt-tabbed h1, .vc-jose-cose-cose-tabbed h1,
+ .sd-jwt-tabbed h1 {
+ font-size: 1em;
+ margin: 0 0;
+ }
+
+ .vc-jose-cose-tabbed [type="radio"], .vc-jose-cose-tabbed-jwt [type="radio"], .vc-jose-cose-tabbed-sd-jwt [type="radio"], .vc-jose-cose-tabbed-cose [type="radio"],
+ .sd-jwt-tabbed [type="radio"] {
+ display: none;
+ }
+
+ .vc-jose-cose-tabs, .vc-jose-cose-jwt-tabs, .vc-jose-cose-sd-jwt-tabs, .vc-jose-cose-cose-tabs,
+ .sd-jwt-tabs {
+ display: flex;
+ align-items: stretch;
+ list-style: none;
+ padding: 0;
+ border-bottom: 1px solid #ccc;
+ }
+
+ li.vc-jose-cose-tab, li.vc-jose-cose-jwt-tab, li.vc-jose-cose-sd-jwt-tab, li.vc-jose-cose-cose-tab,
+ li.sd-jwt-tab {
+ margin: 0 0;
+ margin-left: 8px;
+ }
+
+ .vc-jose-cose-tab>label, .vc-jose-cose-jwt-tab>label, .vc-jose-cose-sd-jwt-tab>label, .vc-jose-cose-cose-tab>label,
+ .sd-jwt-tab>label {
+ display: block;
+ margin-bottom: -1px;
+ padding: .4em .5em;
+ border: 1px solid #ccc;
+ border-top-right-radius: .4em;
+ border-top-left-radius: .4em;
+ background: #eee;
+ color: #666;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .vc-jose-cose-tab:hover label, .vc-jose-cose-jwt-tab:hover label, .vc-jose-cose-sd-jwt-tab:hover label, .vc-jose-cose-cose-tab:hover label,
+ .sd-jwt-tab:hover label {
+ border-left-color: #333;
+ border-top-color: #333;
+ border-right-color: #333;
+ color: #333;
+ }
+
+ .vc-jose-cose-tab-content,
+ .sd-jwt-tab-content {
+ display: none;
+ }
+
+ .vc-jose-cose-tabbed [type="radio"]:nth-of-type(1):checked~.vc-jose-cose-tabs .vc-jose-cose-tab:nth-of-type(1) label,
+ .vc-jose-cose-tabbed [type="radio"]:nth-of-type(2):checked~.vc-jose-cose-tabs .vc-jose-cose-tab:nth-of-type(2) label,
+ .vc-jose-cose-tabbed [type="radio"]:nth-of-type(3):checked~.vc-jose-cose-tabs .vc-jose-cose-tab:nth-of-type(3) label,
+ .sd-jwt-tabbed [type="radio"]:nth-of-type(1):checked~.sd-jwt-tabs .sd-jwt-tab:nth-of-type(1) label,
+ .sd-jwt-tabbed [type="radio"]:nth-of-type(2):checked~.sd-jwt-tabs .sd-jwt-tab:nth-of-type(2) label,
+ .sd-jwt-tabbed [type="radio"]:nth-of-type(3):checked~.sd-jwt-tabs .sd-jwt-tab:nth-of-type(3) label {
+ border-bottom-color: #fff;
+ background: #fff;
+ color: #222;
+ }
+
+ .vc-jose-cose-tabbed [type="radio"]:nth-of-type(1):checked~.vc-jose-cose-tab-content:nth-of-type(1),
+ .vc-jose-cose-tabbed [type="radio"]:nth-of-type(2):checked~.vc-jose-cose-tab-content:nth-of-type(2),
+ .vc-jose-cose-tabbed [type="radio"]:nth-of-type(3):checked~.vc-jose-cose-tab-content:nth-of-type(3),
+ .sd-jwt-tabbed [type="radio"]:nth-of-type(1):checked~.sd-jwt-tab-content:nth-of-type(1),
+ .sd-jwt-tabbed [type="radio"]:nth-of-type(2):checked~.sd-jwt-tab-content:nth-of-type(2),
+ .sd-jwt-tabbed [type="radio"]:nth-of-type(3):checked~.sd-jwt-tab-content:nth-of-type(3) {
+ display: block;
+ }
+
+ .sd-jwt-header, .jwt-header, .vc-jose-cose-jwt .header, .vc-jose-cose-sd-jwt .header, .vc-jose-cose-cose .header {
+ color: red;
+ }
+ .sd-jwt-payload, .jwt-payload, .vc-jose-cose-jwt .payload, .vc-jose-cose-sd-jwt .payload, .vc-jose-cose-cose .payload {
+ color: green;
+ }
+
+ .sd-jwt-signature, .jwt-signature, .vc-jose-cose-jwt .signature, .vc-jose-cose-sd-jwt .signature, .vc-jose-cose-cose .signature {
+ color: blue;
+ }
+
+ .sd-jwt-disclosure, .vc-jose-cose-jwt .disclosure, .vc-jose-cose-sd-jwt .disclosure, .vc-jose-cose-cose .disclosure {
+ color: purple;
+ }
+
+ .sd-jwt-compact, .jwt-compact, .vc-jose-cose-jwt .compact, .vc-jose-cose-sd-jwt .compact, .vc-jose-cose-cose .compact {
+ background-color: rgba(0,0,0,.03);
+ }
+
+ .cose-text, .jose-text, .vc-jose-cose-jwt .text, .vc-jose-cose-sd-jwt .text, .vc-jose-cose-cose .text {
+ font-family: monospace;
+ color: green;
+ }
+
+ .disclosure {
+ margin: 10px 0;
+ font-size: 12px;
+ line-height: 1.6;
+ padding: 5px;
+ }
+
+ .disclosure h3 {
+ margin: 0;
+ font-size: 14px;
+ padding-left: 5px;
+ }
+
+ .disclosure .claim-name {
+ color: #333;
+ }
+
+ .disclosure .hash,
+ .disclosure .disclosure-value,
+ .disclosure .contents {
+ color: #555;
+ word-wrap: break-word;
+ display: inline;
+ }
+
+ .disclosure p {
+ margin: 0;
+ padding-left: 5px;
+ }
+
+ .disclosure pre {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ margin: 0;
+ padding-left: 5px;
+ line-height: 1.6;
+ display: inline-block;
+ }
+
+ .header-value {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ margin: 0;
+ padding-left: 5px;
+ line-height: 1.6;
+ font-size: 12px;
+ }
.vc-tab-content {
display: none;
}
@@ -270,128 +287,30 @@ function addContext(url, context) {
}
async function createVcExamples() {
- // process all 'vc-hash' entries
- const sha2256Hasher = mfHasher.from({
- name: 'sha2-256',
- code: 0x12,
- encode: input => sha256(input)
- });
- const sha2384Hasher = mfHasher.from({
- name: 'sha2-384',
- code: 0x20,
- encode: input => sha384(input)
- });
- const sha3256Hasher = mfHasher.from({
- name: 'sha3-256',
- code: 0x16,
- encode: input => sha3_256(input)
- });
- const sha3384Hasher = mfHasher.from({
- name: 'sha3-384',
- code: 0x15,
- encode: input => sha3_384(input)
+ // generate base keypair and signature suites
+ // ecdsa-sd-2023
+ const keyPairEcdsaMultikeyKeyPair = await EcdsaMultikey
+ .generate({curve: 'P-256'});
+ const {createSignCryptosuite} = ecdsaSd2023Cryptosuite;
+ const suiteEcdsaMultiKey = new DataIntegrityProof({
+ signer: keyPairEcdsaMultikeyKeyPair.signer(),
+ cryptosuite: createSignCryptosuite({
+ mandatoryPointers: ['/issuer'],
+ }),
});
-
- const vcHashEntries = document.querySelectorAll('.vc-hash');
- for(const hashEntry of vcHashEntries) {
-
- // get the hash requirements
- const hashUrl = hashEntry.dataset?.hashUrl || 'INVALID_URL';
- const hashFormat = hashEntry.dataset?.hashFormat?.split(/(\s+)/) || [];
- let encodedHash = null;
-
- // select the base encoder (default: base64-url with no padding)
- let baseEncoder;
- if(hashFormat.includes('sri')) {
- baseEncoder = base64pad;
- } else if(hashFormat.includes('base16')) {
- baseEncoder = base16;
- } else if(hashFormat.includes('base58btc')) {
- baseEncoder = base58btc;
- } else {
- baseEncoder = base64url;
- }
-
- // retrieve the file and generate the hash
- try {
- const response = await fetch(hashUrl);
-
- // ensure retrieval succeeded
- if(response.status !== 200) {
- throw new Error('Failed to retrieve ' + hashUrl);
- }
- const hashData = new Uint8Array(await response.arrayBuffer());
-
- // determine the hash algorithm to use and produce the output accordingly
- if(hashFormat.includes('openssl') && hashFormat.includes('-sha256')) {
- const mfHash = await sha2256Hasher.digest(hashData);
- encodedHash = Array.prototype.map.call(mfHash.digest, byte => {
- return ('0' + (byte & 0xFF).toString(16)).slice(-2);
- }).join('');
- } else if(hashFormat.includes('sri')) {
- if(hashFormat.includes('sha2-256')) {
- const mfHash = await sha2256Hasher.digest(hashData);
- encodedHash = 'sha256-' + baseEncoder.encode(mfHash.digest);
- } else if(hashFormat.includes('sha2-384')) {
- const mfHash = await sha2384Hasher.digest(hashData);
- encodedHash = 'sha384-' + baseEncoder.encode(mfHash.digest);
- }
- } else if(hashFormat.includes('multihash')) {
- if(hashFormat.includes('sha2-256')) {
- const mfHash = await sha2256Hasher.digest(hashData).bytes;
- encodedHash = baseEncoder.encode(mfHash);
- } else if(hashFormat.includes('sha2-384')) {
- const mfHash = await sha2384Hasher.digest(hashData).bytes;
- encodedHash = baseEncoder.encode(mfHash);
- } else if(hashFormat.includes('sha3-256')) {
- const mfHash = await sha3256Hasher.digest(hashData).bytes;
- encodedHash = baseEncoder.encode(mfHash);
- } else if(hashFormat.includes('sha3-384')) {
- const mfHash = await sha3384Hasher.digest(hashData).bytes;
- encodedHash = baseEncoder.encode(mfHash);
- }
- }
-
- // set the encodedHash value
- hashEntry.innerText = encodedHash || 'Unsupported hash format: \'' +
- hashEntry.dataset?.hashFormat + '\'';
- } catch(e) {
- console.error('respec-vc error: Failed to create cryptographic hash.',
- e, hashEntry);
- hashEntry.innerText = 'Error generating cryptographic hash for ' +
- hashUrl;
- continue;
- }
- }
-
- // process all 'vc' entries
- const exampleProofs = [];
-
- // ecdsa-rdfc-2019
- const ecdsaRdfc2019 = await createEcdsaRdfc2019ExampleProof();
- exampleProofs.push(ecdsaRdfc2019);
-
// Ed25519Signature2020
const keyPairEd25519VerificationKey2020 = await Ed25519VerificationKey2020
.generate();
+ const keyPairEd25519Multikey = await Ed25519Multikey
+ .from(keyPairEd25519VerificationKey2020);
const suiteEd25519Signature2020 = new Ed25519Signature2020({
- key: keyPairEd25519VerificationKey2020
+ key: keyPairEd25519VerificationKey2020,
});
-
// eddsa-rdfc-2022
- const eddsaRdfc2022 = await createEddsaRdfc2022ExampleProof();
- exampleProofs.push(eddsaRdfc2022);
-
- // ecdsa-sd-2023
- const ecdsaSd2023 = await createEcdsaSd2023ExampleProof();
- exampleProofs.push(ecdsaSd2023);
-
- // bbs-2023
- const bbs2023 = await createBBSExampleProof();
- exampleProofs.push(bbs2023);
-
- // vc-jwt
- const jwk = await jose.generateKeyPair('ES256');
+ const suiteEd25519Multikey = new DataIntegrityProof({
+ signer: keyPairEd25519Multikey.signer(),
+ cryptosuite: eddsaRdfc2022CryptoSuite,
+ });
// add styles for examples
addVcExampleStyles();
@@ -440,10 +359,9 @@ async function createVcExamples() {
*
* @param {string} suffix - One of the TAB_TYPES values (or `unsigned`).
* @param {string} labelText - Human readable label name.
- * @param {string} tabText - Text to display on the tab.
* @param {contentCallback} callback - Function which returns HTML.
*/
- function addTab(suffix, labelText, tabText, callback) {
+ function addTab(suffix, labelText, callback) {
const button = document.createElement('input');
button.setAttribute('type', 'radio');
button.setAttribute('id', `vc-tab${vcProofExampleIndex}${suffix}`);
@@ -457,16 +375,8 @@ async function createVcExamples() {
const label = document.createElement('li');
label.setAttribute('class', 'vc-tab');
-
- const tabLabel = document.createElement('label');
- tabLabel.setAttribute('for', button.getAttribute('id'));
-
- const abbr = document.createElement('abbr');
- abbr.setAttribute('title', labelText);
- abbr.innerText = tabText;
-
- tabLabel.appendChild(abbr);
- label.appendChild(tabLabel);
+ label.innerHTML =
+ `${labelText} `;
tabLabels.appendChild(label);
const content = document.createElement('div');
@@ -483,25 +393,23 @@ async function createVcExamples() {
}
}
- /**
- * Add a Data Integrity based proof example tab.
- *
+ /*
+ * Add a Data Integrity based proof example tab
* @global string verificationMethod
- * @param {object} suite - Suite object.
- * @param {string} tabText - Text to display on the tab.
- * @param {string | undefined} key - Optional key to use for the proof.
+ * @param object suite
*/
- async function addProofTab(suite, tabText, key) {
+ async function addProofTab(suite) {
let verifiableCredentialProof;
const label = suite?.cryptosuite || suite.type;
- if(key) {
- suite.verificationMethod = 'did:key:' + key.publicKeyMultibase;
+ if(label === 'ecdsa-sd-2023') {
+ suite.verificationMethod = 'did:key:' + keyPairEcdsaMultikeyKeyPair
+ .publicKeyMultibase;
} else {
suite.verificationMethod = verificationMethod;
}
- addTab(label, `Secured with Data Integrity - ${label}`, tabText, async () => {
+ addTab(label, `Secured with Data Integrity (${label})`, async () => {
// attach the proof
try {
verifiableCredentialProof = await attachProof({credential, suite});
@@ -515,44 +423,39 @@ async function createVcExamples() {
});
}
- function hasTab(identifier) {
- return tabTypes.indexOf(identifier) > -1;
+ // set up the unsigned button
+ addTab('unsigned', 'Verifiable Credential', () => example.outerHTML);
+
+ if(tabTypes.indexOf(suiteEd25519Signature2020.type) > -1) {
+ await addProofTab(suiteEd25519Signature2020);
+ }
+ if(tabTypes.indexOf(suiteEd25519Multikey.cryptosuite) > -1) {
+ await addProofTab(suiteEd25519Multikey);
+ }
+ if(tabTypes.indexOf(suiteEcdsaMultiKey.cryptosuite) > -1) {
+ await addProofTab(suiteEcdsaMultiKey);
}
- // set up the unsigned button
- addTab(
- 'unsigned',
- 'Unsecured credential',
- 'Credential',
- () => example.outerHTML
- );
-
- for(const {proof, key, label} of exampleProofs) {
- if(hasTab(proof.cryptosuite)) {
- await addProofTab(proof, label, key);
- }
+ if(tabTypes.indexOf('jose') > -1) {
+ addTab('jose', 'Secured with JOSE', async () => {
+ const joseExample = await getJoseExample(privateKey, credential);
+ return getJoseHtml({joseExample});
+ });
}
- if(hasTab(suiteEd25519Signature2020.type)) {
- await addProofTab(suiteEd25519Signature2020, 'Ed25519Signature2020');
+ if(tabTypes.indexOf('sd-jwt') > -1) {
+ addTab('sd-jwt', 'Secured with SD-JWT', async () => {
+ // eslint-disable-next-line max-len
+ const sdJwtExample = await getSdJwtExample(vcProofExampleIndex, privateKey, credential);
+ return getSdJwtHtml({sdJwtExample});
+ });
}
- if(hasTab('vc-jwt')) {
- // set up the signed JWT button
- addTab('vc-jwt', 'Secured with VC-JWT', 'vc-jwt',
- async () => {
- // convert to a JWT
- let verifiableCredentialJwt;
- try {
- verifiableCredentialJwt = await transformToJwt({
- credential, kid: verificationMethod, jwk});
- return `${verifiableCredentialJwt.match(/.{1,75}/g).join('\n')} `;
- } catch(e) {
- console.error(
- 'respec-vc error: Failed to convert Credential to JWT.',
- e, example.innerText);
- }
- });
+ if(tabTypes.indexOf('cose') > -1) {
+ addTab('cose', 'Secured with COSE', async () => {
+ const coseExample = await getCoseExample(privateKey, credential);
+ return getCoseHtml({coseExample});
+ });
}
// append the tabbed content
@@ -569,5 +472,5 @@ async function createVcExamples() {
// setup exports on window
window.respecVc = {
addContext,
- createVcExamples
+ createVcExamples,
};
diff --git a/package.json b/package.json
index 8058379..969e753 100644
--- a/package.json
+++ b/package.json
@@ -18,8 +18,11 @@
},
"homepage": "https://github.com/w3c/respec-vc#readme",
"dependencies": {
+ "@digitalbazaar/bbs-2023-cryptosuite": "^1.2.0",
+ "@digitalbazaar/bls12-381-multikey": "^1.3.0",
"@digitalbazaar/credentials-examples-context": "digitalbazaar/credentials-examples-context#main",
"@digitalbazaar/data-integrity": "^2.1.0",
+ "@digitalbazaar/ecdsa-rdfc-2019-cryptosuite": "^1.0.1",
"@digitalbazaar/ed25519-multikey": "^1.1.0",
"@digitalbazaar/ed25519-signature-2020": "^5.2.0",
"@digitalbazaar/ed25519-verification-key-2020": "^4.1.0",
@@ -27,9 +30,14 @@
"@digitalbazaar/odrl-context": "digitalbazaar/odrl-context#main",
"@digitalbazaar/vc": "digitalbazaar/vc#update-vc-2.0",
"@noble/hashes": "^1.4.0",
+ "@transmute/cose": "^0.1.1",
+ "@transmute/edn": "^0.0.5",
+ "@transmute/verifiable-credentials": "^0.2.0",
"ed25519-signature-2020-context": "^1.1.0",
"jose": "^5.2.4",
- "multiformats": "^13.1.1"
+ "multiformats": "^13.1.1",
+ "url": "^0.11.3",
+ "yaml": "^2.3.2"
},
"devDependencies": {
"@digitalbazaar/bbs-2023-cryptosuite": "^1.2.0",
@@ -37,11 +45,16 @@
"@digitalbazaar/ecdsa-multikey": "^1.7.0",
"@digitalbazaar/ecdsa-rdfc-2019-cryptosuite": "^1.0.1",
"@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.2.1",
+ "buffer": "^6.0.3",
+ "crypto-browserify": "^3.12.0",
"eslint": "^8.57.0",
"eslint-config-digitalbazaar": "^5.2.0",
"eslint-plugin-jsdoc": "^48.2.3",
"eslint-plugin-unicorn": "^52.0.0",
"http-server": "^14.1.1",
+ "process": "^0.11.10",
+ "stream-browserify": "^3.0.0",
+ "vm-browserify": "^1.1.2",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
}
diff --git a/src/common.js b/src/common.js
new file mode 100644
index 0000000..26e25ba
--- /dev/null
+++ b/src/common.js
@@ -0,0 +1,9 @@
+export const privateKey = {
+ kid: 'ExHkBMW9fmbkvV266mRpuP2sUY_N_EWIN1lapUzO8ro',
+ alg: 'ES384',
+ kty: 'EC',
+ crv: 'P-384',
+ x: '8XkmqX3pxcYduNID8cK4mYA531PV7szaGh-yk-XIM9OQKZSO94y3xe67nyFvt0QP',
+ y: 'kWjcg1gBVM752eQzXlMfHwEOzuUB4zCgi2z6X1p03ljAcp6b_FEINR-eONp00gX0',
+ d: 'SC8W4oMb4vZv_OAftZ19tlTJ6Uk83qpT1pIBjAcVUWNtaE1febiIhgpQSZTZaJl_',
+};
diff --git a/src/cose.js b/src/cose.js
new file mode 100644
index 0000000..8df87f3
--- /dev/null
+++ b/src/cose.js
@@ -0,0 +1,107 @@
+import * as cose from '@transmute/cose';
+import * as edn from '@transmute/edn';
+import {holder, issuer} from '@transmute/verifiable-credentials';
+
+function buf2hex(buffer) { // buffer is an ArrayBuffer
+ return [...new Uint8Array(buffer)]
+ .map(x => x.toString(16).padStart(2, '0'))
+ .join('');
+}
+
+const getCredential = async (
+ privateKey,
+ byteSigner,
+ messageType,
+ messageJson,
+) => {
+ return issuer({
+ alg: privateKey.alg,
+ type: messageType,
+ signer: byteSigner,
+ }).issue({
+ claimset: new TextEncoder().encode(JSON.stringify(messageJson, null, 2)),
+ });
+};
+
+const getPresentation = async (
+ privateKey,
+ byteSigner,
+ messageType,
+ message,
+) => {
+ const disclosures = (message.verifiableCredential || []).map(enveloped => {
+ const {id} = enveloped;
+ const type = id.includes('base64url') ?
+ id.split(';base64url,')[0].replace('data:', '') :
+ id.split(';')[0].replace('data:', '');
+ const content = id.includes('base64url') ?
+ new TextEncoder().encode(id.split('base64url,').pop()) :
+ new TextEncoder().encode(id.split(';').pop());
+ return {
+ type,
+ credential: content,
+ };
+ });
+ return holder({
+ alg: privateKey.alg,
+ type: messageType,
+ }).issue({
+ signer: byteSigner,
+ presentation: message,
+ disclosures,
+ });
+};
+
+const getBinaryMessage = async (privateKey, messageType, messageJson) => {
+ const signer = cose.detached.signer({
+ remote: cose.crypto.signer({
+ secretKeyJwk: privateKey,
+ }),
+ });
+ const byteSigner = {
+ sign: async payload => {
+ return signer.sign({
+ protectedHeader: new Map([[1, -35]]),
+ unprotectedHeader: new Map(),
+ payload,
+ });
+ },
+ };
+ switch(messageType) {
+ case 'application/vc+ld+json+cose': {
+ return getCredential(privateKey, byteSigner, messageType, messageJson);
+ }
+ case 'application/vp+ld+json+cose': {
+ return getPresentation(privateKey, byteSigner, messageType, messageJson);
+ }
+ default: {
+ throw new Error('Unknown message type');
+ }
+ }
+};
+
+export const getCoseExample = async (privateKey, messageJson) => {
+ const type = Array.isArray(messageJson.type) ?
+ messageJson.type : [messageJson.type];
+ const messageType = type.includes('VerifiableCredential') ?
+ 'application/vc+ld+json+cose' : 'application/vp+ld+json+cose';
+ const message = await getBinaryMessage(privateKey, messageType, messageJson);
+ const messageHex = buf2hex(message);
+ const messageBuffer = Buffer.from(messageHex, 'hex');
+ const diagnostic =
+ await edn.render(messageBuffer, 'application/cbor-diagnostic');
+ return `
+${messageType.replace('+cose', '')}
+
+${JSON.stringify(messageJson, null, 2)}
+
+application/cbor-diagnostic
+
+${messageType} (detached payload)
+
+${messageHex}
+
+ `.trim();
+};
diff --git a/src/fast-uri-polyfill.js b/src/fast-uri-polyfill.js
new file mode 100644
index 0000000..19db92c
--- /dev/null
+++ b/src/fast-uri-polyfill.js
@@ -0,0 +1,4 @@
+// eslint-disable-next-line unicorn/prefer-module
+module.exports = function(source) {
+ return source.replace(/require\('node:url'\)/g, 'require(\'url\')');
+};
diff --git a/src/html.js b/src/html.js
new file mode 100644
index 0000000..010aaaa
--- /dev/null
+++ b/src/html.js
@@ -0,0 +1,25 @@
+export const getJoseHtml = ({joseExample}) => {
+ return `
+`;
+};
+
+export const getSdJwtHtml = ({sdJwtExample}) => {
+ return `
+
+${sdJwtExample}
+
+`.trim();
+};
+
+export const getCoseHtml = ({coseExample}) => {
+ return `
+`;
+};
diff --git a/src/jose.js b/src/jose.js
new file mode 100644
index 0000000..f2dc0a0
--- /dev/null
+++ b/src/jose.js
@@ -0,0 +1,110 @@
+import {
+ holder,
+ issuer,
+ key,
+ text,
+} from '@transmute/verifiable-credentials';
+
+import * as jose from 'jose';
+
+const getCredential = async (
+ privateKey,
+ byteSigner,
+ messageType,
+ messageJson
+) => {
+ return issuer({
+ alg: privateKey.alg,
+ type: messageType,
+ signer: byteSigner,
+ }).issue({
+ claimset: new TextEncoder().encode(JSON.stringify(messageJson, null, 2)),
+ });
+};
+
+const getPresentation = async (
+ privateKey,
+ byteSigner,
+ messageType,
+ message
+) => {
+ const disclosures = (message.verifiableCredential || []).map(enveloped => {
+ const {id} = enveloped;
+ const type = id.includes('base64url') ? id.split(';base64url,')[0].
+ replace('data:', '') : id.split(';')[0].replace('data:', '');
+ const content = id.includes('base64url') ?
+ new TextEncoder().encode(id.split('base64url,').pop()) :
+ new TextEncoder().encode(id.split(';').pop());
+ return {
+ type,
+ credential: content,
+ };
+ });
+ return holder({
+ alg: privateKey.alg,
+ type: messageType,
+ }).issue({
+ signer: byteSigner,
+ presentation: message,
+ disclosures,
+ });
+};
+
+const getJoseHtml = token => {
+ const [header, payload, signature] = token.split('.');
+ return `
+
+
+.${payload}
+.${signature}
+
`.trim();
+};
+
+const getBinaryMessage = async (privateKey, messageType, messageJson) => {
+ const byteSigner = {
+ sign: async bytes => {
+ const jws = await new jose.CompactSign(bytes)
+ .setProtectedHeader({kid: privateKey.kid, alg: privateKey.alg})
+ .sign(await key.importKeyLike({
+ type: 'application/jwk+json',
+ content: new TextEncoder().encode(JSON.stringify(privateKey)),
+ }));
+ return text.encoder.encode(jws);
+ },
+ };
+ switch(messageType) {
+ case 'application/vc+ld+json+jwt': {
+ return getCredential(privateKey, byteSigner, messageType, messageJson);
+ }
+ case 'application/vp+ld+json+jwt': {
+ return getPresentation(privateKey, byteSigner, messageType, messageJson);
+ }
+ default: {
+ throw new Error('Unknown message type');
+ }
+ }
+};
+
+export const getJoseExample = async (privateKey, messageJson) => {
+ const type = Array.isArray(messageJson.type) ?
+ messageJson.type : [messageJson.type];
+ const messageType = type.includes('VerifiableCredential') ?
+ 'application/vc+ld+json+jwt' : 'application/vp+ld+json+jwt';
+ const message = await getBinaryMessage(privateKey, messageType, messageJson);
+ const messageEncoded = new TextDecoder().decode(message);
+ const decodedHeader = jose.decodeProtectedHeader(messageEncoded);
+ return `
+Protected Headers
+
+${JSON.stringify(decodedHeader, null, 2)}
+
+${messageType.replace('+jwt', '')}
+
+${JSON.stringify(messageJson, null, 2)}
+
+${messageType}
+
+${getJoseHtml(messageEncoded)}
+
+ `.trim();
+};
diff --git a/src/sd-jwt.js b/src/sd-jwt.js
new file mode 100644
index 0000000..781952b
--- /dev/null
+++ b/src/sd-jwt.js
@@ -0,0 +1,176 @@
+import * as jose from 'jose';
+import {base64url, issuer, key, text} from '@transmute/verifiable-credentials';
+import crypto from 'crypto';
+import yaml from 'yaml';
+
+const calculateHash = value => {
+ return base64url.encode(crypto.createHash('sha256').update(value).digest());
+};
+
+const customJSONStringify = obj => {
+ return JSON.stringify(obj, null, 2)
+ .replace(/\n/g, ' ').replace(/\s/g, ' ');
+};
+
+const generateDisclosureHtml = (claimName, hash, disclosure, contents) => {
+ return `
+
+
Claim: ${claimName}
+
SHA-256 Hash: ${hash}
+
Disclosure(s): ${disclosure}
+
Contents: ${customJSONStringify(contents)}
+
+`;
+};
+
+const getSdHtml = vc => {
+ const [token, ...disclosure] = vc.split('~');
+ const [header, payload, signature] = token.split('.');
+ const disclosures = disclosure.map(d => {
+ return `~${d} `;
+ }).join('');
+ return `
+
+
+.${payload}
+.${signature}
+${disclosures}
+
`;
+};
+
+const getHeadersHtml = vc => {
+ const [token] = vc.split('~');
+ const [header] = token.split('.');
+ const decoded = new TextDecoder().decode(base64url.decode(header));
+ const headerJson = JSON.parse(decoded);
+ return ``;
+};
+
+const getPayloadHtml = vc => {
+ const [token] = vc.split('~');
+ const [, payload] = token.split('.');
+ const decoded = new TextDecoder().decode(base64url.decode(payload));
+ const payloadJson = JSON.parse(decoded);
+ return ``;
+};
+
+const getDisclosuresHtml = async vc => {
+ const [, ...disclosures] = vc.split('~');
+ const disclosureHtml = disclosures.map(disclosure => {
+ const decoded = new TextDecoder().decode(base64url.decode(disclosure));
+ const decodedDisclosure = JSON.parse(decoded);
+ const [, ...claimPath] = decodedDisclosure;
+ claimPath.pop();
+ const hash = calculateHash(disclosure);
+ return generateDisclosureHtml(claimPath, hash, disclosure,
+ decodedDisclosure);
+ });
+
+ return `${disclosureHtml.join('\n')}
`;
+};
+
+export const generateIssuerClaims = example => {
+ return yaml.stringify(example).replace(/id: /g, '!sd id: ')
+ .replace(/type:/g, '!sd type:');
+};
+
+const getCredential = async (
+ privateKey,
+ byteSigner,
+ messageType,
+ messageJson,
+) => {
+ return issuer({
+ alg: privateKey.alg,
+ type: messageType,
+ signer: byteSigner,
+ }).issue({
+ claimset: new TextEncoder().encode(generateIssuerClaims(messageJson)),
+ });
+};
+
+const getPresentation = async (
+ privateKey,
+ byteSigner,
+ messageType,
+ messageJson,
+) => {
+ const mediaType = 'application/vc+ld+json+sd-jwt';
+ return getCredential(privateKey, byteSigner, mediaType, messageJson);
+};
+
+export const getBinaryMessage = async (
+ privateKey,
+ messageType,
+ messageJson,
+) => {
+ const byteSigner = {
+ sign: async bytes => {
+ const jws = await new jose.CompactSign(bytes)
+ .setProtectedHeader({kid: privateKey.kid, alg: privateKey.alg})
+ .sign(await key.importKeyLike({
+ type: 'application/jwk+json',
+ content: new TextEncoder().encode(JSON.stringify(privateKey)),
+ }));
+ return text.encoder.encode(jws);
+ },
+ };
+ switch(messageType) {
+ case 'application/vc+ld+json+sd-jwt': {
+ return getCredential(privateKey, byteSigner, messageType, messageJson);
+ }
+ case 'application/vp+ld+json+sd-jwt': {
+ return getPresentation(privateKey, byteSigner, messageType, messageJson);
+ }
+ default: {
+ throw new Error('Unknown message type');
+ }
+ }
+};
+
+export const getSdJwtExample = async (
+ index,
+ privateKey,
+ messageJson,
+ prefix = 'sd-jwt',
+) => {
+ const type = Array.isArray(messageJson.type) ?
+ messageJson.type : [messageJson.type];
+ const messageType = type.includes('VerifiableCredential') ?
+ 'application/vc+ld+json+sd-jwt' : 'application/vp+ld+json+sd-jwt';
+ const binaryMessage =
+ await getBinaryMessage(privateKey, messageType, messageJson);
+ const message = new TextDecoder().decode(binaryMessage);
+ const encoded = getSdHtml(message);
+ const header = getHeadersHtml(message);
+ const payload = getPayloadHtml(message);
+ const disclosures = await getDisclosuresHtml(message);
+ return `
+
+
+
+
+
+
+ Encoded
+
+
+ Decoded
+
+
+ Issuer Disclosures
+
+
+
+ ${encoded}
+
+
+ ${header}
+ ${payload}
+
+
+ ${disclosures}
+
+
+`;
+};
diff --git a/webpack.config.js b/webpack.config.js
index 440056e..22bac75 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,6 +1,35 @@
+const path = require('path');
+const webpack = require('webpack');
+
module.exports = [{
mode: 'development',
entry: './index.js',
- plugins: [],
- watch: true
+ plugins: [
+ new webpack.ProvidePlugin({
+ process: 'process/browser.js',
+ Buffer: ['buffer', 'Buffer'],
+ }),
+ ],
+ watch: true,
+ resolve: {
+ fallback: {
+ buffer: require.resolve("buffer/"),
+ crypto: require.resolve("crypto-browserify"),
+ stream: require.resolve("stream-browserify"),
+ vm: require.resolve("vm-browserify"),
+ url: require.resolve("url/"),
+ },
+ },
+ module: {
+ rules: [
+ {
+ test: /node_modules\/fast-uri/,
+ use: [
+ {
+ loader: path.resolve('src/fast-uri-polyfill.js')
+ }
+ ]
+ }
+ ]
+ }
}];