Skip to content

Commit

Permalink
Ensure invalid JSON-LD is rejected when using JCS cryptosuites.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Dec 17, 2024
1 parent e604def commit cf7e12e
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 1 deletion.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# bedrock-vc-issuer ChangeLog

## 28.4.0 - 2024-12-dd

### Added
- Ensure invalid JSON-LD is rejected when using JCS cryptosuites.

## 28.3.0 - 2024-12-17

### Added
Expand Down
18 changes: 18 additions & 0 deletions lib/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import assert from 'assert-plus';
import {createDocumentLoader} from './documentLoader.js';
import {CredentialStatusIssuer} from './CredentialStatusIssuer.js';
import {CredentialStatusWriter} from './CredentialStatusWriter.js';
import jsonld from 'jsonld';
import {v4 as uuid} from 'uuid';
import {named as vcNamedContexts} from '@bedrock/credentials-context';

Expand Down Expand Up @@ -178,6 +179,10 @@ async function _secureWithSuites({credential, documentLoader, suites}) {
// vc-js.issue may be fixed to not mutate credential
// see: https://github.com/digitalbazaar/vc-js/issues/76
credential = {...credential};
// validate JSON-LD for any JCS cryptosuites
if(suites.some(s => s.cryptosuite?.includes('-jcs-'))) {
await _validateJsonLd({document: credential, documentLoader});
}
// issue using each suite
for(const suite of suites) {
// update credential with latest proof(s)
Expand Down Expand Up @@ -226,3 +231,16 @@ function _getISODateTime(date = new Date()) {
// remove milliseconds precision
return date.toISOString().replace(/\.\d+Z$/, 'Z');
}

async function _validateJsonLd({document, documentLoader}) {
// convert to RDF dataset
const options = {
algorithm: 'RDFC-1.0',
base: null,
safe: true,
rdfDirection: 'i18n-datatype',
produceGeneralizedRdf: false,
documentLoader
};
await jsonld.toRDF(document, options);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"bnid": "^3.0.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"jsonld": "^8.3.2",
"klona": "^2.0.6",
"lru-cache": "^6.0.0",
"serialize-error": "^11.0.3",
Expand Down
169 changes: 168 additions & 1 deletion test/mocha/assertions/issueWithoutStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,66 @@ export function testIssueWithoutStatus({
should.not.exist(verifiableCredential.proof.created);
}
});
it('issues a credential w/issuer name languages', async () => {
const credential = klona(mockCredentialV2);
credential.issuer = {
name: [{
'@value': 'Name of issuer',
'@language': 'en',
'@direction': 'ltr'
}, {
'@value': 'Name of issuer, pip pip',
'@language': 'en-GB',
'@direction': 'ltr'
}]
};
const zcapClient = helpers.createZcapClient({capabilityAgent});
const {verifiableCredential} = await assertions.issueAndAssert({
configId: noStatusListIssuerId,
credential,
issueOptions,
zcapClient,
capability: noStatusListIssuerRootZcap
});
should.exist(verifiableCredential.id);
should.not.exist(verifiableCredential.credentialStatus);
// not supported with old `Ed25519Signature2020`
if(suiteName !== 'Ed25519Signature2020') {
// `created` should not be set by default because new issue config
// mechanism was used w/o requesting it
should.not.exist(verifiableCredential.proof.created);
}
});
it('issues a credential w/issuer description languages', async () => {
const credential = klona(mockCredentialV2);
credential.issuer = {
description: [{
'@value': 'Description of issuer',
'@language': 'en',
'@direction': 'ltr'
}, {
'@value': 'Description of issuer, pip pip',
'@language': 'en-GB',
'@direction': 'ltr'
}]
};
const zcapClient = helpers.createZcapClient({capabilityAgent});
const {verifiableCredential} = await assertions.issueAndAssert({
configId: noStatusListIssuerId,
credential,
issueOptions,
zcapClient,
capability: noStatusListIssuerRootZcap
});
should.exist(verifiableCredential.id);
should.not.exist(verifiableCredential.credentialStatus);
// not supported with old `Ed25519Signature2020`
if(suiteName !== 'Ed25519Signature2020') {
// `created` should not be set by default because new issue config
// mechanism was used w/o requesting it
should.not.exist(verifiableCredential.proof.created);
}
});

it('fails to issue an empty credential', async () => {
let error;
Expand All @@ -229,6 +289,110 @@ export function testIssueWithoutStatus({
error.data.type.should.equal('ValidationError');
});

it('fails to issue a credential w/invalid name', async () => {
let error;
try {
const credential = klona(mockCredentialV2);
credential.name = {
'@value': 'Name of credential',
'@language': 'en',
url: 'did:example:credential'
};
const zcapClient = helpers.createZcapClient({capabilityAgent});
await zcapClient.write({
url: `${noStatusListIssuerId}/credentials/issue`,
capability: noStatusListIssuerRootZcap,
json: {
credential,
options: issueOptions
}
});
} catch(e) {
error = e;
}
should.exist(error);
error.data.type.should.equal('ValidationError');
});

it('fails to issue a credential w/invalid description', async () => {
let error;
try {
const credential = klona(mockCredentialV2);
credential.description = {
'@value': 'Description of credential',
'@language': 'en',
url: 'did:example:credential'
};
const zcapClient = helpers.createZcapClient({capabilityAgent});
await zcapClient.write({
url: `${noStatusListIssuerId}/credentials/issue`,
capability: noStatusListIssuerRootZcap,
json: {
credential,
options: issueOptions
}
});
} catch(e) {
error = e;
}
should.exist(error);
error.data.type.should.equal('ValidationError');
});

it('fails to issue a credential w/invalid issuer name', async () => {
let error;
try {
const credential = klona(mockCredentialV2);
credential.issuer = {
name: {
'@value': 'Name of issuer',
'@language': 'en',
url: 'did:example:credential'
}
};
const zcapClient = helpers.createZcapClient({capabilityAgent});
await zcapClient.write({
url: `${noStatusListIssuerId}/credentials/issue`,
capability: noStatusListIssuerRootZcap,
json: {
credential,
options: issueOptions
}
});
} catch(e) {
error = e;
}
should.exist(error);
error.data.type.should.equal('DataError');
});

it('fails to issue a credential w/invalid issuer description', async () => {
let error;
try {
const credential = klona(mockCredentialV2);
credential.issuer = {
description: {
'@value': 'Description of issuer',
'@language': 'en',
url: 'did:example:credential'
}
};
const zcapClient = helpers.createZcapClient({capabilityAgent});
await zcapClient.write({
url: `${noStatusListIssuerId}/credentials/issue`,
capability: noStatusListIssuerRootZcap,
json: {
credential,
options: issueOptions
}
});
} catch(e) {
error = e;
}
should.exist(error);
error.data.type.should.equal('DataError');
});

it('fails to issue a VC missing a "credentialSchema" type', async () => {
const credential = klona(mockCredentialV2);
// `type` is not present, so a validation error should occur
Expand All @@ -242,7 +406,10 @@ export function testIssueWithoutStatus({
await zcapClient.write({
url: `${noStatusListIssuerId}/credentials/issue`,
capability: noStatusListIssuerRootZcap,
json: {credential}
json: {
credential,
options: issueOptions
}
});
} catch(e) {
error = e;
Expand Down

0 comments on commit cf7e12e

Please sign in to comment.