Skip to content

Commit

Permalink
Reorganize suites and rework APIs and internals.
Browse files Browse the repository at this point in the history
- Normalize to Uint8Arrays, normalize env code to util functions.
- Rework internal API for creating, attaching, verifying proofs, etc.
- Move signing/verifying into LDKeyPair module.
- Create JwsLinkedDataSignature to handle common JWS suites.
- Better separate proof purpose from suite.
- Remove responsibility for checking key trust, etc. (should be
  done by the caller of this library post verification).
- Remove nonce checking (should be done by the caller of this
  library post verification).
- Make attaching proofs more uniform (independent of suite).
- Use `verificationMethod`.
- Add external `signer` API.
  • Loading branch information
dlongley committed Jan 1, 2019
1 parent 125b869 commit 363eade
Show file tree
Hide file tree
Showing 30 changed files with 1,949 additions and 1,537 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# jsonld-signatures ChangeLog

## 3.0.0 - TBD

### Changed
- **BREAKING**: `sign` and `verify` APIs require suites and proof purpose
instances to be passed.

### Removed
- **BREAKING**: Removed API `wrap` and injector support.

## 2.3.1 - 2018-09-05

### Changed
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2014-2017, Digital Bazaar, Inc.
Copyright (c) 2014-2018, Digital Bazaar, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down
22 changes: 8 additions & 14 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
/**
* Karam configuration for jsonld-signatures.
*
* @author Dave Longley
* @author David I. Lehn
*
* Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2011-2018 Digital Bazaar, Inc. All rights reserved.
*/
const path = require('path');
const webpack = require('webpack');

module.exports = function(config) {
// bundler to test: webpack, browserify
var bundler = process.env.BUNDLER || 'webpack';
const bundler = process.env.BUNDLER || 'webpack';

var frameworks = ['mocha'];
const frameworks = ['mocha'];
// main bundle preprocessors
var preprocessors = ['babel'];
const preprocessors = ['babel'];

if(bundler === 'browserify') {
frameworks.push(bundler);
Expand All @@ -33,7 +27,7 @@ module.exports = function(config) {

// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: frameworks,
frameworks,

// list of files / patterns to load in the browser
files: [
Expand Down Expand Up @@ -63,7 +57,7 @@ module.exports = function(config) {
// exclude node_modules by default
exclude: /(node_modules)/
}, {
// include rdf-canonize
// include jsonld and rdf-canonize
include: /(node_modules\/jsonld)/,
include: /(node_modules\/rdf-canonize)/
}],
Expand All @@ -72,7 +66,7 @@ module.exports = function(config) {
options: {
presets: ['env'],
plugins: [
['transform-object-rest-spread', {useBuiltIns: true }]
['transform-object-rest-spread', {useBuiltIns: true}]
]
}
}
Expand Down Expand Up @@ -114,7 +108,7 @@ module.exports = function(config) {
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
//browsers: ['PhantomJS', 'Chrome', 'Firefox', 'Safari'],
browsers: ['PhantomJS'],
browsers: ['ChromeHeadless'],

customLaunchers: {
IE9: {
Expand Down
12 changes: 3 additions & 9 deletions lib/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
*/
'use strict';

// FIXME: remove this class

const constants = require('./constants');
const jsonld = require('jsonld');
const util = require('./util');

module.exports = class Helper {
constructor(injector) {
this.injector = injector;
}

/**
* Gets a remote public key.
*
Expand Down Expand Up @@ -86,7 +85,6 @@ module.exports = class Helper {

// find specific owner of key
let owner;
const jsonld = this.injector.use('jsonld');
for(let i = 0; i < framedOwners.length; ++i) {
let keys;
// direct access to public keys
Expand Down Expand Up @@ -133,7 +131,6 @@ module.exports = class Helper {
async getJsonLd(url, options) {
options = options || {};

const jsonld = this.injector.use('jsonld');
const remoteDoc = await jsonld.get(url, options);

// compact w/context URL from link header
Expand All @@ -160,7 +157,6 @@ module.exports = class Helper {
type: requiredKeyType,
owner: {'@embed': '@never'}
};
const jsonld = this.injector.use('jsonld');
const opts = {};
if(options.documentLoader) {
opts.documentLoader = options.documentLoader;
Expand Down Expand Up @@ -193,7 +189,6 @@ module.exports = class Helper {
publicKey: {'@embed': '@never'}
};
}
const jsonld = this.injector.use('jsonld');
const opts = {};
if(options.documentLoader) {
opts.documentLoader = options.documentLoader;
Expand All @@ -208,7 +203,6 @@ module.exports = class Helper {
'@requireAll': false,
[proofPurpose]: {'@embed': '@never'}
};
const jsonld = this.injector.use('jsonld');
const opts = {};
if(options.documentLoader) {
opts.documentLoader = options.documentLoader;
Expand Down
8 changes: 8 additions & 0 deletions lib/ProofChain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*!
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

module.exports = class ProofChain {
// TODO: implement
};
204 changes: 204 additions & 0 deletions lib/ProofSet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*!
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

const constants = require('./constants');
const jsonld = require('jsonld');
const {extendContextLoader, strictDocumentLoader} = require('./documentLoader');
const strictExpansionMap = require('./expansionMap');

module.exports = class ProofSet {
/**
* Adds a Linked Data proof to a document. If the document contains other
* proofs, the new proof will be appended to the existing set of proofs.
*
* Important note: This method assumes that the term `proof` in the given
* document has the same definition as the `https://w3id.org/security/v2`
* JSON-LD @context.
*
* @param document {object|string} Object to be signed, either a string URL
* (resolved via the given `documentLoader`) or a plain object (JSON-LD
* document).
* @param options {object} Options hashmap.
*
* A `suite` option is required:
*
* @param options.suite {LinkedDataSignature} a signature suite instance
* that will create the proof.
*
* A `purpose` option is required:
*
* @param options.purpose {ProofPurpose} a proof purpose instance that will
* augment the proof with information describing its intended purpose.
*
* Advanced optional parameters and overrides:
*
* @param [documentLoader] {function} a custom document loader,
* `Promise<RemoteDocument> documentLoader(url)`.
* @param [expansionMap] {function} A custom expansion map that is
* passed to the JSON-LD processor; by default a function that will throw
* an error when unmapped properties are detected in the input, use `false`
* to turn this off and allow unmapped properties to be dropped or use a
* custom function.
*
* @return {Promise<object>} resolves with the signed document, with
* the signature in the top-level `proof` property.
*/
async add(document, {
suite, purpose, documentLoader, expansionMap} = {}) {
if(!(suite && purpose)) {
throw new TypeError(
'"options.suite" and "options.purpose" must be given.');
}

if(documentLoader) {
documentLoader = extendContextLoader(documentLoader);
} else {
documentLoader = strictDocumentLoader;
}
if(expansionMap !== false) {
expansionMap = strictExpansionMap;
}

if(typeof document === 'string') {
// fetch document
document = await documentLoader(document);
}

// shallow clone the document, excluding any existing proof(s)
const input = {...document};
delete input.proof;

// create the new proof (suites MUST output a proof using the security-v2
// `@context`)
const proof = await suite.createProof(
input, {purpose, documentLoader, expansionMap});

// compact proof to match document's context
const expandedProof = {
'https://w3id.org/security#proof': {
'@graph': proof
}
};
const ctx = jsonld.getValues(document, '@context');
const options = {documentLoader, expansionMap};
const compactProof = await jsonld.compact(expandedProof, ctx, options);
delete compactProof['@context'];

// add proof to document
jsonld.addValue(document, 'proof', compactProof.proof);
return document;
}

/**
* Verify Linked Data proof(s) on a document. The proofs to be verified
* must match the given proof purpose.
*
* Important note: This method assumes that the term `proof` in the given
* document has the same definition as the `https://w3id.org/security/v2`
* JSON-LD @context.
*
* @param document {object|string} Object with one or more proofs to be
* verified, either a string URL (resolved to an object via the given
* `documentLoader`) or a plain object (JSON-LD document).
* @param options {object} Options hashmap.
*
* A `suites` option is required:
*
* @param options.suites {Array of LinkedDataSignature} acceptable signature
* suite instances for verifying the proof(s).
*
* A `purpose` option is required:
*
* @param options.purpose {ProofPurpose} a proof purpose instance that will
* match proofs to be verified and ensure they were created according to
* the appropriate purpose.
*
* Advanced optional parameters and overrides:
*
* @param [documentLoader] {function} a custom document loader,
* `Promise<RemoteDocument> documentLoader(url)`.
* @param [expansionMap] {function} A custom expansion map that is
* passed to the JSON-LD processor; by default a function that will throw
* an error when unmapped properties are detected in the input, use `false`
* to turn this off and allow unmapped properties to be dropped or use a
* custom function.
*
* @return {Promise<object>} resolves with an object with a `verified`
* boolean property that is `true` if at least one proof matching the
* given purpose and suite verifies and `false` otherwise; a `results`
* property with an array of detailed results; if `false` an `error`
* property will be present.
*/
async verify(document, {
suites, purpose, documentLoader, expansionMap} = {}) {
if(!(suites && purpose)) {
throw new TypeError(
'"options.suites" and "options.purpose" must be given.');
}

if(documentLoader) {
documentLoader = extendContextLoader(documentLoader);
} else {
documentLoader = strictDocumentLoader;
}
if(expansionMap !== false) {
expansionMap = strictExpansionMap;
}

try {
if(typeof document === 'string') {
// fetch document
document = await documentLoader(document);
}

const {proof: proofSet} = document;
if(!proofSet) {
throw new Error(
'Could not verify any proofs; the document has no "proof" property.');
}

// compact proofs to security-v2 context
const expanded = {
proof: proofSet
};
const ctx = jsonld.getValues(document, '@context');
expanded['@context'] = ctx;
const options = {documentLoader, expansionMap};
const compact = await jsonld.compact(
expanded, constants.SECURITY_CONTEXT_URL, options);

// filter out matching proofs
const matches = compact.proof.filter(proof => purpose.match(
proof, {document, documentLoader, expansionMap}));
if(matches.length === 0) {
throw new Error(
'Could not verify any proofs; no proofs matched the required ' +
'purpose.');
}

// verify each matching proof
const results = (await Promise.all(matches.map(proof => {
for(const suite of suites) {
if(suite.match(proof)) {
return suite.verifyProof(
proof, {document, purpose, documentLoader, expansionMap});
}
}
return {
verified: false,
error: new Error('No matching suite found.')
};
}))).map((r, i) => ({proof: matches[i], ...r}));
const verified = results.some(r => r.verified);
if(!verified) {
const errors = results.filter(r => !r.verified).map(r => r.error);
return {verified, results, error: [].concat(...errors)};
}
return {verified, results};
} catch(error) {
return {verified: false, error};
}
}
};
6 changes: 4 additions & 2 deletions lib/contexts/security-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ module.exports = {
"EquihashProof2018": "sec:EquihashProof2018",
"RsaSignature2018": "sec:RsaSignature2018",
"RsaVerificationKey2018": "sec:RsaVerificationKey2018",
"action": "sec:action",

"allowedAction": "sec:allowedAction",
"capability": {"@id": "sec:capability", "@type": "@id"},
"capabilityAction": "sec:capabilityAction",
"capabilityChain": {"@id": "sec:capabilityChain", "@type": "@id", "@container": "@list"},
"capabilityDelegation": {"@id": "sec:capabilityDelegationSuite", "@type": "@id", "@container": "@set"},
"capabilityInvocation": {"@id": "sec:capabilityInvocationSuite", "@type": "@id", "@container": "@set"},
"caveat": {"@id": "sec:caveat", "@type": "@id"},
"controller": {"@id": "sec:controller", "@type": "@id"},
"delegator": {"@id": "sec:delegator", "@type": "@id"},
"equihashParameterK": {"@id": "sec:equihashParameterK", "@type": "xsd:integer"},
"equihashParameterN": {"@id": "sec:equihashParameterN", "@type": "xsd:integer"},
Expand All @@ -24,6 +25,7 @@ module.exports = {
"parentCapability": {"@id": "sec:parentCapability", "@type": "@id"},
"proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"},
"proofPurpose": {"@id": "sec:proofPurpose", "@type": "@vocab"},
"proofValue": "sec:proofValue"
"proofValue": "sec:proofValue",
"verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"}
}]
};
Loading

0 comments on commit 363eade

Please sign in to comment.