@interop/did-web-resolver 0.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,17 @@
1
- 'use strict'
2
-
3
- // import axios from 'axios' // todo: consider 'apisauce' instead
4
- import { DidDocument } from 'did-io'
5
- import { URL } from 'whatwg-url'
1
+ import { httpClient } from '@digitalbazaar/http-client'
2
+ import * as didIo from '@digitalbazaar/did-io'
3
+ import ed25519Context from 'ed25519-signature-2020-context'
4
+ import x25519Context from 'x25519-key-agreement-2020-context'
6
5
  import didContext from 'did-context'
7
- const DID_CONTEXT_URL = didContext.constants.DID_CONTEXT_URL
6
+
7
+ const { VERIFICATION_RELATIONSHIPS } = didIo
8
8
 
9
9
  const DEFAULT_KEY_MAP = {
10
- capabilityInvocation: 'Ed25519VerificationKey2018',
11
- authentication: 'Ed25519VerificationKey2018',
12
- assertionMethod: 'Ed25519VerificationKey2018',
13
- capabilityDelegation: 'Ed25519VerificationKey2018'
14
- // keyAgreement: 'X25519KeyAgreementKey2019' // <- not yet supported
10
+ capabilityInvocation: 'Ed25519VerificationKey2020',
11
+ authentication: 'Ed25519VerificationKey2020',
12
+ assertionMethod: 'Ed25519VerificationKey2020',
13
+ capabilityDelegation: 'Ed25519VerificationKey2020',
14
+ keyAgreement: 'X25519KeyAgreementKey2020'
15
15
  }
16
16
 
17
17
  export function didFromUrl ({ url } = {}) {
@@ -47,59 +47,256 @@ export function urlFromDid ({ did } = {}) {
47
47
  throw new TypeError(`DID Method not supported: "${did}".`)
48
48
  }
49
49
 
50
+ const [didUrl, hashFragment] = did.split('#')
51
+ // eslint-disable-next-line no-unused-vars
52
+ // const [didResource, query] = didUrl.split('?')
53
+
50
54
  // eslint-disable-next-line no-unused-vars
51
- const [_did, _web, host, ...pathFragments] = did.split(':')
55
+ const [_did, _web, urlNoProtocol] = didUrl.split(':')
56
+
57
+ let parsedUrl
58
+ try {
59
+ // URI-decode the url (in case it contained a port number,
60
+ // for example, `did:web:localhost%3A8080`
61
+ parsedUrl = new URL('https://' + decodeURIComponent(urlNoProtocol))
62
+ } catch (error) {
63
+ throw new TypeError(`Cannot construct url from did: "${did}".`)
64
+ }
52
65
 
53
- let pathname = ''
54
- if (pathFragments.length === 0) {
55
- pathname = '/.well-known/did.json'
66
+ if (!parsedUrl.pathname || parsedUrl.pathname === '/') {
67
+ parsedUrl.pathname = '/.well-known/did.json'
56
68
  } else {
57
- pathname = '/' + pathFragments.map(decodeURIComponent).join('/')
69
+ const pathFragments = parsedUrl.pathname.split('/')
70
+ parsedUrl.pathname = pathFragments.map(decodeURIComponent).join('/')
71
+ }
72
+
73
+ if (hashFragment) {
74
+ parsedUrl.hash = hashFragment
75
+ }
76
+ return parsedUrl.toString()
77
+ }
78
+
79
+ /**
80
+ * Initializes the DID Document's keys/proof methods.
81
+ *
82
+ * @example
83
+ * didDocument.id = 'did:ex:123';
84
+ * const {didDocument, keyPairs} = await initKeys({
85
+ * didDocument,
86
+ * cryptoLd,
87
+ * keyMap: {
88
+ * capabilityInvocation: someExistingKey,
89
+ * authentication: 'Ed25519VerificationKey2020',
90
+ * assertionMethod: 'Ed25519VerificationKey2020',
91
+ * keyAgreement: 'X25519KeyAgreementKey2019'
92
+ * }
93
+ * });.
94
+ *
95
+ * @param {object} options - Options hashmap.
96
+ * @param {object} options.didDocument - DID Document.
97
+ * @typedef {object} CryptoLD
98
+ * @param {CryptoLD} [options.cryptoLd] - CryptoLD driver instance,
99
+ * initialized with the key types this DID Document intends to support.
100
+ * @param {object} [options.keyMap] - Map of keys (or key types) by purpose.
101
+ *
102
+ * @returns {Promise<{didDocument: object, keyPairs: Map}>} Resolves with the
103
+ * DID Document initialized with keys, as well as the map of the corresponding
104
+ * key pairs (by key id).
105
+ */
106
+ export async function initKeys ({ didDocument, cryptoLd, keyMap = {} } = {}) {
107
+ const doc = { ...didDocument }
108
+ if (!doc.id) {
109
+ throw new TypeError(
110
+ 'DID Document "id" property is required to initialize keys.')
111
+ }
112
+
113
+ const keyPairs = new Map()
114
+
115
+ // Set the defaults for the created keys (if needed)
116
+ const options = { controller: doc.id }
117
+
118
+ for (const purpose in keyMap) {
119
+ if (!VERIFICATION_RELATIONSHIPS.has(purpose)) {
120
+ throw new Error(`Unsupported key purpose: "${purpose}".`)
121
+ }
122
+
123
+ let key
124
+ if (typeof keyMap[purpose] === 'string') {
125
+ if (!cryptoLd) {
126
+ throw new Error('Please provide an initialized CryptoLD instance.')
127
+ }
128
+ key = await cryptoLd.generate({ type: keyMap[purpose], ...options })
129
+ } else {
130
+ // An existing key has been provided
131
+ key = keyMap[purpose]
132
+ }
133
+
134
+ doc[purpose] = [key.export({ publicKey: true })]
135
+ keyPairs.set(key.id, key)
58
136
  }
59
137
 
60
- return 'https://' + decodeURIComponent(host) + pathname
138
+ return { didDocument: doc, keyPairs }
61
139
  }
62
140
 
63
141
  export class DidWebResolver {
64
- constructor ({ cryptoLd, keyMap = DEFAULT_KEY_MAP } = {}) {
65
- this.method = 'web' // did:web:...
142
+ /**
143
+ * @param cryptoLd {CryptoLD}
144
+ * @param keyMap {object}
145
+ * @param [logger] {object} Logger object (with .log, .error, .warn,
146
+ * etc methods).
147
+ */
148
+ constructor ({ cryptoLd, keyMap = DEFAULT_KEY_MAP, logger = console } = {}) {
149
+ this.method = 'web' // did:web:... (used for didIo resolver harness)
66
150
  this.cryptoLd = cryptoLd
67
151
  this.keyMap = keyMap
152
+ this.logger = logger
68
153
  }
69
154
 
70
155
  /**
71
156
  * Generates a new DID Document and initializes various authentication
72
157
  * and authorization proof purpose keys.
73
158
  *
74
- * Usage:
75
- * ```
159
+ * @example
76
160
  * const url = 'https://example.com'
77
161
  * const { didDocument, didKeys } = await didWeb.generate({url})
78
162
  * didDocument.id
79
163
  * // -> 'did:web:example.com'
80
- * ```
164
+ *
81
165
  *
82
166
  * Either an `id` or a `url` is required:
83
167
  * @param [id] {string} - A did:web DID. If absent, will be converted from url
84
168
  * @param [url] {string}
85
169
  *
86
- * @throws {Error}
170
+ * @param [keyMap=DEFAULT_KEY_MAP] {object} A hashmap of key types by purpose.
87
171
  *
88
- * @returns {Promise<{didDocument: DidDocument, didKeys: object}>}
172
+ * @parma [cryptoLd] {object} CryptoLD instance with support for supported
173
+ * crypto suites installed.
174
+ *
175
+ * @returns {Promise<{didDocument: object, keyPairs: Map,
176
+ * methodFor: Function}>} Resolves with the generated DID Document, along
177
+ * with the corresponding key pairs used to generate it (for storage in a
178
+ * KMS).
89
179
  */
90
180
  async generate ({ id, url, keyMap = this.keyMap, cryptoLd = this.cryptoLd } = {}) {
91
- const didDocument = new DidDocument({ id: id || didFromUrl({ url }) })
92
- didDocument['@context'] = [DID_CONTEXT_URL]
181
+ const did = id || didFromUrl({ url })
182
+
183
+ // Compose the DID Document
184
+ let didDocument = {
185
+ '@context': [
186
+ didContext.constants.DID_CONTEXT_URL,
187
+ ed25519Context.constants.CONTEXT_URL,
188
+ x25519Context.constants.CONTEXT_URL
189
+ ],
190
+ id: did
191
+ }
192
+
193
+ const result = await initKeys({ didDocument, cryptoLd, keyMap })
194
+ const keyPairs = result.keyPairs
195
+ didDocument = result.didDocument
93
196
 
94
- const { didKeys } = await didDocument.initKeys({ cryptoLd, keyMap })
95
- return { didDocument, didKeys }
197
+ // Convenience function that returns the public/private key pair instance
198
+ // for a given purpose (authentication, assertionMethod, keyAgreement, etc).
199
+ const methodFor = ({ purpose }) => {
200
+ const { id: methodId } = didIo.findVerificationMethod({
201
+ doc: didDocument, purpose
202
+ })
203
+ return keyPairs.get(methodId)
204
+ }
205
+
206
+ return { didDocument, keyPairs, methodFor }
96
207
  }
97
208
 
98
209
  /**
99
210
  * Fetches a DID Document for a given DID.
100
- * @param {string} did
101
211
  *
102
- * @returns {Promise<DidDocument>}
212
+ * @example
213
+ * // In Node.js tests, use an agent to avoid self-signed certificate errors
214
+ * const agent = new https.agent({rejectUnauthorized: false});
215
+ *
216
+ * @param {string} [did] For example, 'did:web:example.com'
217
+ * @param {string} [url]
218
+ * @param {https.Agent} [agent] Optional agent used to customize network
219
+ * behavior in Node.js (such as `rejectUnauthorized: false`).
220
+ * @param {object} [logger] Logger object (with .log, .error, .warn,
221
+ * etc methods).
222
+ *
223
+ * @throws {Error}
224
+ *
225
+ * @returns {Promise<object>} Plain parsed JSON object of the DID Document.
103
226
  */
104
- async get () {}
227
+ async get ({ did, url, agent, logger = this.logger }) {
228
+ const didUrl = url || urlFromDid({ did })
229
+ if (!didUrl) {
230
+ throw new TypeError('A DID or a URL is required.')
231
+ }
232
+
233
+ const [urlAuthority, keyIdFragment] = didUrl.split('#')
234
+
235
+ let didDocument
236
+ try {
237
+ logger.info(`Fetching "${urlAuthority}" via http client.`)
238
+ const result = await httpClient.get(urlAuthority, { agent })
239
+ didDocument = result.data
240
+ } catch (e) {
241
+ // status is HTTP status code
242
+ // data is JSON error from the server if available
243
+ const { data, status } = e
244
+ logger.error(`Http ${status} error:`, data)
245
+ throw e
246
+ }
247
+ if (didDocument && keyIdFragment) {
248
+ // resolve an individual key
249
+ // Keys are expected to have format: <did:web:...>#<keyIdFragment>
250
+ const didAuthority = didFromUrl({ url: urlAuthority })
251
+ const methodId = `${didAuthority}#${keyIdFragment}`
252
+
253
+ const key = didIo.findVerificationMethod({ doc: didDocument, methodId })
254
+ if (!key) {
255
+ throw new Error(`Key id ${methodId} not found.`)
256
+ }
257
+
258
+ const keyPair = await this.cryptoLd.from(key)
259
+
260
+ return keyPair.export({ publicKey: true, includeContext: true })
261
+ }
262
+
263
+ return didDocument
264
+ }
265
+
266
+ /**
267
+ * Returns the public key (verification method) object for a given DID
268
+ * Document and purpose. Useful in conjunction with a `.get()` call.
269
+ *
270
+ * @example
271
+ * const didDocument = await didKeyDriver.get({did});
272
+ * const authKeyData = didDriver.publicMethodFor({
273
+ * didDocument, purpose: 'authentication'
274
+ * });
275
+ * // You can then create a suite instance object to verify signatures etc.
276
+ * const authPublicKey = await cryptoLd.from(authKeyData);
277
+ * const {verify} = authPublicKey.verifier();
278
+ *
279
+ * @param {object} options - Options hashmap.
280
+ * @param {object} options.didDocument - DID Document (retrieved via a
281
+ * `.get()` or from some other source).
282
+ * @param {string} options.purpose - Verification method purpose, such as
283
+ * 'authentication', 'assertionMethod', 'keyAgreement' and so on.
284
+ *
285
+ * @returns {object} Returns the public key object (obtained from the DID
286
+ * Document), without a `@context`.
287
+ */
288
+ publicMethodFor ({ didDocument, purpose } = {}) {
289
+ if (!didDocument) {
290
+ throw new TypeError('The "didDocument" parameter is required.')
291
+ }
292
+ if (!purpose) {
293
+ throw new TypeError('The "purpose" parameter is required.')
294
+ }
295
+
296
+ const method = didIo.findVerificationMethod({ doc: didDocument, purpose })
297
+ if (!method) {
298
+ throw new Error(`No verification method found for purpose "${purpose}"`)
299
+ }
300
+ return method
301
+ }
105
302
  }
package/src/index.js CHANGED
@@ -1,3 +1,8 @@
1
- 'use strict'
2
1
 
3
- export { DidWebResolver, didFromUrl, urlFromDid } from './DidWebResolver.js'
2
+ import { DidWebResolver, didFromUrl, urlFromDid } from './DidWebResolver.js'
3
+
4
+ const driver = options => {
5
+ return new DidWebResolver(options)
6
+ }
7
+
8
+ export { driver, DidWebResolver, didFromUrl, urlFromDid }
@@ -1,79 +0,0 @@
1
- <component name="ProjectCodeStyleConfiguration">
2
- <code_scheme name="Project" version="173">
3
- <option name="LINE_SEPARATOR" value="&#10;" />
4
- <option name="RIGHT_MARGIN" value="80" />
5
- <JSCodeStyleSettings version="0">
6
- <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
7
- <option name="FORCE_SEMICOLON_STYLE" value="true" />
8
- <option name="SPACE_BEFORE_GENERATOR_MULT" value="true" />
9
- <option name="USE_DOUBLE_QUOTES" value="false" />
10
- <option name="FORCE_QUOTE_STYlE" value="true" />
11
- <option name="OBJECT_LITERAL_WRAP" value="1" />
12
- <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
13
- <option name="SPACES_WITHIN_IMPORTS" value="true" />
14
- </JSCodeStyleSettings>
15
- <DBN-PSQL>
16
- <case-options enabled="true">
17
- <option name="KEYWORD_CASE" value="lower" />
18
- <option name="FUNCTION_CASE" value="lower" />
19
- <option name="PARAMETER_CASE" value="lower" />
20
- <option name="DATATYPE_CASE" value="lower" />
21
- <option name="OBJECT_CASE" value="preserve" />
22
- </case-options>
23
- <formatting-settings enabled="false" />
24
- </DBN-PSQL>
25
- <DBN-SQL>
26
- <case-options enabled="true">
27
- <option name="KEYWORD_CASE" value="lower" />
28
- <option name="FUNCTION_CASE" value="lower" />
29
- <option name="PARAMETER_CASE" value="lower" />
30
- <option name="DATATYPE_CASE" value="lower" />
31
- <option name="OBJECT_CASE" value="preserve" />
32
- </case-options>
33
- <formatting-settings enabled="false">
34
- <option name="STATEMENT_SPACING" value="one_line" />
35
- <option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
36
- <option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
37
- </formatting-settings>
38
- </DBN-SQL>
39
- <XML>
40
- <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
41
- </XML>
42
- <codeStyleSettings language="CSS">
43
- <indentOptions>
44
- <option name="INDENT_SIZE" value="2" />
45
- <option name="CONTINUATION_INDENT_SIZE" value="4" />
46
- <option name="TAB_SIZE" value="2" />
47
- </indentOptions>
48
- </codeStyleSettings>
49
- <codeStyleSettings language="HTML">
50
- <indentOptions>
51
- <option name="INDENT_SIZE" value="2" />
52
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
53
- <option name="TAB_SIZE" value="2" />
54
- </indentOptions>
55
- </codeStyleSettings>
56
- <codeStyleSettings language="Jade">
57
- <indentOptions>
58
- <option name="INDENT_SIZE" value="2" />
59
- <option name="TAB_SIZE" value="2" />
60
- </indentOptions>
61
- </codeStyleSettings>
62
- <codeStyleSettings language="JavaScript">
63
- <option name="RIGHT_MARGIN" value="80" />
64
- <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
65
- <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
66
- <option name="ALIGN_MULTILINE_FOR" value="false" />
67
- <option name="SPACE_WITHIN_BRACKETS" value="true" />
68
- <option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" />
69
- <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
70
- <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
71
- <option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
72
- <indentOptions>
73
- <option name="INDENT_SIZE" value="2" />
74
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
75
- <option name="TAB_SIZE" value="2" />
76
- </indentOptions>
77
- </codeStyleSettings>
78
- </code_scheme>
79
- </component>
@@ -1,5 +0,0 @@
1
- <component name="ProjectCodeStyleConfiguration">
2
- <state>
3
- <option name="USE_PER_PROJECT_SETTINGS" value="true" />
4
- </state>
5
- </component>