oydid 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/oydid/basic.rb +13 -0
- data/lib/oydid/log.rb +27 -7
- data/lib/oydid.rb +295 -91
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e119ee021cb7e09dd4c7ee6273a4ee778bae850e31b6360a31044c11e674df4c
|
|
4
|
+
data.tar.gz: 9be71c4690f1e6476c6a339a49172575eac79c11406c6a1ac8a49e66c804ee6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa57e8c74580e2afa7e6adae48528fd8d43fd306176eb9039a1531c9db46c0090702eef2d036ddaee3aef09e568ef8ad2714c990e6745b2dff4ed080d5e18874
|
|
7
|
+
data.tar.gz: 4608b92f08da163850832144c89a9495c182a065155727a8d10bacb83ee00fd47951b442420e9ee24582a5804938349e55649b57fe0e699082aa3311fbc57729
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.8.0
|
data/lib/oydid/basic.rb
CHANGED
|
@@ -1279,6 +1279,19 @@ class Oydid
|
|
|
1279
1279
|
doc_hash = doc_hash.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first rescue doc_hash
|
|
1280
1280
|
doc_hash = doc_hash.delete_prefix("did:oyd:")
|
|
1281
1281
|
|
|
1282
|
+
# in-process callers (e.g. a repository holding DIDs in its own database)
|
|
1283
|
+
# can supply a store object responding to #get(doc_hash) that returns
|
|
1284
|
+
# {"doc" => <did document>, "log" => [<log entries>]} or nil. Unlike
|
|
1285
|
+
# options[:local_doc] this resolves *per hash*: dag_update walks several
|
|
1286
|
+
# different documents along an update chain, so a single document must
|
|
1287
|
+
# not be handed back for every hash it asks for.
|
|
1288
|
+
if options[:local_store]
|
|
1289
|
+
local_obj = options[:local_store].get(doc_hash)
|
|
1290
|
+
if !local_obj.nil?
|
|
1291
|
+
return [local_obj, ""]
|
|
1292
|
+
end
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1282
1295
|
if doc_location == ""
|
|
1283
1296
|
doc_location = DEFAULT_LOCATION
|
|
1284
1297
|
end
|
data/lib/oydid/log.rb
CHANGED
|
@@ -297,7 +297,10 @@ class Oydid
|
|
|
297
297
|
did_hash = did_hash.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first
|
|
298
298
|
did10 = did_hash[0,10]
|
|
299
299
|
|
|
300
|
-
|
|
300
|
+
# pass options through: without them options[:local_store] is lost
|
|
301
|
+
# and the lookup falls back to HTTP against DEFAULT_LOCATION, which
|
|
302
|
+
# breaks every in-process caller that keeps its DIDs locally
|
|
303
|
+
doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, options)
|
|
301
304
|
if doc.first.nil?
|
|
302
305
|
currentDID["error"] = 2
|
|
303
306
|
msg = doc.last.to_s
|
|
@@ -309,13 +312,27 @@ class Oydid
|
|
|
309
312
|
end
|
|
310
313
|
doc = doc.first["doc"]
|
|
311
314
|
if el["op"] == 2 # CREATE
|
|
312
|
-
# signature
|
|
313
|
-
|
|
314
|
-
|
|
315
|
+
# The CREATE signature used to be unobtainable in the
|
|
316
|
+
# Client-Managed-Secret-Mode: its payload l1_doc is the hash
|
|
317
|
+
# of a DID document that already contains the TERMINATE
|
|
318
|
+
# signature, so it can only be collected in a phase of its
|
|
319
|
+
# own. CMSM has that phase now and new DIDs always carry the
|
|
320
|
+
# signature - but DIDs created before it do not, and
|
|
321
|
+
# rejecting them would make them unresolvable.
|
|
322
|
+
#
|
|
323
|
+
# The check is therefore tolerant by default and strict on
|
|
324
|
+
# request: a relying party that only accepts DIDs created
|
|
325
|
+
# with a signed CREATE entry sets options[:strict_create_sig].
|
|
326
|
+
if el["sig"].nil?
|
|
327
|
+
if options[:strict_create_sig]
|
|
315
328
|
currentDID["error"] = 1
|
|
316
|
-
currentDID["message"] = "
|
|
329
|
+
currentDID["message"] = "missing signature in CREATE log entry"
|
|
317
330
|
return currentDID
|
|
318
331
|
end
|
|
332
|
+
elsif !match_log_did?(el, doc)
|
|
333
|
+
currentDID["error"] = 1
|
|
334
|
+
currentDID["message"] = "Signatures in log don't match"
|
|
335
|
+
return currentDID
|
|
319
336
|
end
|
|
320
337
|
end
|
|
321
338
|
currentDID["did"] = doc_did
|
|
@@ -353,7 +370,10 @@ class Oydid
|
|
|
353
370
|
did_hash = doc_did.delete_prefix("did:oyd:")
|
|
354
371
|
did_hash = did_hash.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first
|
|
355
372
|
did10 = did_hash[0,10]
|
|
356
|
-
|
|
373
|
+
# pass options through: without them options[:local_store] is lost
|
|
374
|
+
# and the lookup falls back to HTTP against DEFAULT_LOCATION, which
|
|
375
|
+
# breaks every in-process caller that keeps its DIDs locally
|
|
376
|
+
doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, options)
|
|
357
377
|
doc = doc.first["doc"]
|
|
358
378
|
|
|
359
379
|
if !Oydid.match_log_did?(el, doc)
|
|
@@ -479,7 +499,7 @@ class Oydid
|
|
|
479
499
|
next_did_hash = next_doc_did.delete_prefix("did:oyd:")
|
|
480
500
|
next_did_hash = next_did_hash.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first
|
|
481
501
|
next_did10 = next_did_hash[0,10]
|
|
482
|
-
next_doc = retrieve_document_raw(next_doc_did, next_did10 + ".doc", next_doc_location,
|
|
502
|
+
next_doc = retrieve_document_raw(next_doc_did, next_did10 + ".doc", next_doc_location, options)
|
|
483
503
|
if next_doc.first.nil?
|
|
484
504
|
currentDID["error"] = 2
|
|
485
505
|
currentDID["message"] = next_doc.last
|
data/lib/oydid.rb
CHANGED
|
@@ -222,54 +222,98 @@ class Oydid
|
|
|
222
222
|
ts = options[:ts]
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
# signatures collected from the client so far (CMSM only)
|
|
226
|
+
cmsm_sig_rev = nil
|
|
227
|
+
cmsm_sig_doc = nil
|
|
228
|
+
cmsm_sig_create = nil
|
|
229
|
+
cmsm_log_revoke_old = nil
|
|
230
|
+
cmsm_resume = options[:cmsm] && options[:cmsm_session].to_s != ""
|
|
231
|
+
|
|
226
232
|
if options[:cmsm]
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
233
|
+
# A CMSM flow is identified by a session handle, not by the public
|
|
234
|
+
# key: the same key may legitimately be reused across flows, and
|
|
235
|
+
# keying the intermediate data on it made a second flow overwrite
|
|
236
|
+
# the first one. Absence of a session means "start a new flow".
|
|
237
|
+
if !cmsm_resume
|
|
238
|
+
if did_doc["key"].nil?
|
|
239
|
+
return [nil, nil, nil, "CMSM requires public key"]
|
|
240
|
+
end
|
|
241
|
+
cmsm_keys = did_doc["key"].split(':')
|
|
242
|
+
did_doc.delete("key")
|
|
243
|
+
if did_doc == {}
|
|
244
|
+
did_doc = nil
|
|
245
|
+
end
|
|
246
|
+
case cmsm_keys.count
|
|
247
|
+
when 2
|
|
248
|
+
# both keys are managed by the client - the revocation record
|
|
249
|
+
# of the new document has to be signed by the client as well
|
|
250
|
+
publicKey = cmsm_keys[0]
|
|
251
|
+
pubRevoKey = cmsm_keys[1]
|
|
252
|
+
revocationKey = nil
|
|
253
|
+
when 1
|
|
254
|
+
# only the document key is client-managed; the revocation key
|
|
255
|
+
# is generated here and stays with this process
|
|
256
|
+
publicKey = cmsm_keys[0]
|
|
257
|
+
revocationKey, msg = generate_private_key("", options[:key_type]+'-priv', options)
|
|
258
|
+
if revocationKey.nil?
|
|
259
|
+
return [nil, nil, nil, "cannot generate revocation key: " + msg.to_s]
|
|
260
|
+
end
|
|
261
|
+
pubRevoKey = public_key(revocationKey, options).first
|
|
262
|
+
else
|
|
263
|
+
return [nil, nil, nil, "CMSM accepts at most two public keys"]
|
|
264
|
+
end
|
|
239
265
|
else
|
|
240
|
-
|
|
241
|
-
|
|
266
|
+
# continue a persisted flow: the request carries the session and
|
|
267
|
+
# exactly one new signature, which fills the next open slot
|
|
268
|
+
incoming_sig = options[:sig]
|
|
269
|
+
if incoming_sig.to_s == ""
|
|
270
|
+
incoming_sig = did_doc["opt"]["sig"] rescue nil
|
|
271
|
+
end
|
|
272
|
+
if incoming_sig.to_s == ""
|
|
273
|
+
return [nil, nil, nil, "missing signature in CMSM flow (sig)"]
|
|
274
|
+
end
|
|
242
275
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
276
|
+
payload, msg = check_cmsm(options[:cmsm_session], options)
|
|
277
|
+
if payload.nil?
|
|
278
|
+
if msg.to_s == ""
|
|
279
|
+
msg = "unknown or expired CMSM session"
|
|
280
|
+
end
|
|
281
|
+
return [nil, nil, nil, msg]
|
|
282
|
+
end
|
|
246
283
|
if payload.is_a?(String)
|
|
247
284
|
payload = JSON.parse(payload) rescue nil
|
|
248
285
|
end
|
|
249
286
|
if payload.nil?
|
|
250
287
|
return [nil, nil, nil, "invalid persisted data in CMSM flow"]
|
|
251
288
|
end
|
|
252
|
-
|
|
253
|
-
if did_doc["opt"].nil?
|
|
254
|
-
if options[:sig].nil?
|
|
255
|
-
return [nil, nil, nil, "1missing signature in CMSM flow (sig)"]
|
|
256
|
-
end
|
|
257
|
-
l2_sig = options[:sig]
|
|
258
|
-
else
|
|
259
|
-
if did_doc["opt"]["sig"].nil?
|
|
260
|
-
return [nil, nil, nil, "2missing signature in CMSM flow (sig)"]
|
|
261
|
-
end
|
|
262
|
-
l2_sig = did_doc["opt"]["sig"]
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
options[:cmsm2] = true
|
|
266
|
-
privateKey = nil
|
|
289
|
+
payload = payload.transform_keys(&:to_s)
|
|
267
290
|
|
|
291
|
+
privateKey = nil
|
|
292
|
+
publicKey = payload["publicKey"]
|
|
293
|
+
pubRevoKey = payload["pubRevoKey"]
|
|
268
294
|
revocationKey = payload["revocationKey"]
|
|
269
|
-
did_doc
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
295
|
+
did_doc = payload["did_doc"]
|
|
296
|
+
cmsm_sig_rev = payload["sig_rev"]
|
|
297
|
+
cmsm_sig_doc = payload["sig_doc"]
|
|
298
|
+
cmsm_sig_create = payload["sig_create"]
|
|
299
|
+
# the update context is only sent in phase 1
|
|
300
|
+
if !payload["log_revoke_old"].nil?
|
|
301
|
+
options[:log_revoke_old] = payload["log_revoke_old"]
|
|
302
|
+
end
|
|
303
|
+
# the timestamp and the location are part of every hash in the
|
|
304
|
+
# flow and must not be recomputed per phase
|
|
305
|
+
ts = payload["ts"].to_i
|
|
306
|
+
doc_location = payload["doc_location"]
|
|
307
|
+
|
|
308
|
+
if revocationKey.to_s == "" && cmsm_sig_rev.to_s == ""
|
|
309
|
+
cmsm_sig_rev = incoming_sig
|
|
310
|
+
elsif cmsm_sig_doc.to_s == ""
|
|
311
|
+
cmsm_sig_doc = incoming_sig
|
|
312
|
+
elsif cmsm_sig_create.to_s == ""
|
|
313
|
+
cmsm_sig_create = incoming_sig
|
|
314
|
+
else
|
|
315
|
+
return [nil, nil, nil, "CMSM session is already complete"]
|
|
316
|
+
end
|
|
273
317
|
end
|
|
274
318
|
else
|
|
275
319
|
# key management
|
|
@@ -322,6 +366,27 @@ class Oydid
|
|
|
322
366
|
log_old = did_info["log"]
|
|
323
367
|
|
|
324
368
|
# check if provided old keys are native DID keys or delegates ==================
|
|
369
|
+
if options[:cmsm]
|
|
370
|
+
# The old private keys are with the client, so nothing can be
|
|
371
|
+
# derived from them here. The old public keys come from the DID
|
|
372
|
+
# document just resolved, and the revocation record of the current
|
|
373
|
+
# document is supplied by the client - it received that record when
|
|
374
|
+
# the DID was created or last updated. Rebuilding it here is not
|
|
375
|
+
# possible: it needs a signature by the old revocation key.
|
|
376
|
+
old_privateKey = nil
|
|
377
|
+
old_revocationKey = nil
|
|
378
|
+
old_did_key = did_info["doc"]["key"].to_s
|
|
379
|
+
old_publicDocKey = old_did_key.split(":").first.to_s
|
|
380
|
+
old_publicRevKey = old_did_key.split(":").last.to_s
|
|
381
|
+
|
|
382
|
+
verified, vmsg = verify_revocation_log(did_info, options[:log_revoke_old],
|
|
383
|
+
"log_revoke_old", options)
|
|
384
|
+
if verified.nil?
|
|
385
|
+
return [nil, nil, nil, vmsg]
|
|
386
|
+
end
|
|
387
|
+
revocationLog = verified.to_json
|
|
388
|
+
cmsm_log_revoke_old = verified
|
|
389
|
+
else
|
|
325
390
|
tmp_old_doc_did10 = did10_old + "_private_key.enc" rescue ""
|
|
326
391
|
old_privateKey, msg = getPrivateKey(options[:old_doc_enc], options[:old_doc_pwd], options[:old_doc_key], tmp_old_doc_did10, options)
|
|
327
392
|
tmp_old_rev_did10 = did10_old + "_revocation_key.enc" rescue ""
|
|
@@ -375,6 +440,7 @@ class Oydid
|
|
|
375
440
|
return [nil, nil, nil, "cannot decrypt revocation log entry: " + msg]
|
|
376
441
|
end
|
|
377
442
|
end # compare old keys with existing DID Document
|
|
443
|
+
end # CMSM or self-managed
|
|
378
444
|
|
|
379
445
|
revoc_log = JSON.parse(revocationLog)
|
|
380
446
|
revoc_log["previous"] = [
|
|
@@ -383,13 +449,15 @@ class Oydid
|
|
|
383
449
|
]
|
|
384
450
|
prev_hash = [multi_hash(canonical(revoc_log), LOG_HASH_OPTIONS).first]
|
|
385
451
|
end
|
|
386
|
-
if !options[:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
did_key = publicKey + ":" + pubRevoKey
|
|
452
|
+
if !options[:cmsm]
|
|
453
|
+
publicKey = public_key(privateKey, options).first
|
|
454
|
+
pubRevoKey = public_key(revocationKey, options).first
|
|
455
|
+
end
|
|
456
|
+
did_key = publicKey + ":" + pubRevoKey
|
|
392
457
|
|
|
458
|
+
# the document is assembled once, in the first phase of a CMSM flow -
|
|
459
|
+
# afterwards it comes back from the session unchanged
|
|
460
|
+
if !cmsm_resume
|
|
393
461
|
if options[:keyAgreement]
|
|
394
462
|
if did_doc.nil?
|
|
395
463
|
did_doc = {}
|
|
@@ -415,44 +483,52 @@ class Oydid
|
|
|
415
483
|
did_doc[:authentication] = ["#key-doc"]
|
|
416
484
|
did_doc = did_doc.transform_keys(&:to_s)
|
|
417
485
|
end
|
|
486
|
+
end
|
|
418
487
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
488
|
+
# build new revocation document
|
|
489
|
+
subDid = {"doc": did_doc, "key": did_key}.to_json
|
|
490
|
+
retVal = multi_hash(canonical(subDid), LOG_HASH_OPTIONS)
|
|
491
|
+
if retVal.first.nil?
|
|
492
|
+
return [nil, nil, nil, retVal.last]
|
|
493
|
+
end
|
|
494
|
+
subDidHash = retVal.first
|
|
495
|
+
|
|
496
|
+
if options[:cmsm] && revocationKey.to_s == "" && cmsm_sig_rev.to_s == ""
|
|
497
|
+
# the revocation key is managed by the client, so the revocation
|
|
498
|
+
# record of the new document has to be signed there. Its signature
|
|
499
|
+
# feeds into r1 and thus into every later hash, which is why this
|
|
500
|
+
# cannot be asked for together with the document-key signature.
|
|
501
|
+
return cmsm_challenge(subDidHash, "key-rev", cmsm_state(publicKey,
|
|
502
|
+
pubRevoKey, revocationKey, did_doc, ts, doc_location,
|
|
503
|
+
cmsm_sig_rev, cmsm_sig_doc, cmsm_sig_create,
|
|
504
|
+
did_old, cmsm_log_revoke_old), options)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
if revocationKey.to_s == ""
|
|
508
|
+
signedSubDidHash = cmsm_sig_rev
|
|
509
|
+
else
|
|
426
510
|
signedSubDidHash = sign(subDidHash, revocationKey, LOG_HASH_OPTIONS).first
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
# build termination log entry
|
|
433
|
-
l2_doc = multi_hash(canonical(r1), LOG_HASH_OPTIONS).first
|
|
434
|
-
if !doc_location.nil?
|
|
435
|
-
l2_doc += LOCATION_PREFIX + doc_location.to_s
|
|
436
|
-
end
|
|
511
|
+
end
|
|
512
|
+
r1 = { "ts": ts,
|
|
513
|
+
"op": 1, # REVOKE
|
|
514
|
+
"doc": subDidHash,
|
|
515
|
+
"sig": signedSubDidHash }.transform_keys(&:to_s)
|
|
437
516
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
did_key: did_key,
|
|
444
|
-
l2_doc: l2_doc,
|
|
445
|
-
r1: r1
|
|
446
|
-
}
|
|
447
|
-
success, msg = persist_cmsm(publicKey, payload, options)
|
|
517
|
+
# build termination log entry
|
|
518
|
+
l2_doc = multi_hash(canonical(r1), LOG_HASH_OPTIONS).first
|
|
519
|
+
if !doc_location.nil?
|
|
520
|
+
l2_doc += LOCATION_PREFIX + doc_location.to_s
|
|
521
|
+
end
|
|
448
522
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
523
|
+
if options[:cmsm]
|
|
524
|
+
if cmsm_sig_doc.to_s == ""
|
|
525
|
+
return cmsm_challenge(l2_doc, "key-doc", cmsm_state(publicKey,
|
|
526
|
+
pubRevoKey, revocationKey, did_doc, ts, doc_location,
|
|
527
|
+
cmsm_sig_rev, cmsm_sig_doc, cmsm_sig_create,
|
|
528
|
+
did_old, cmsm_log_revoke_old), options)
|
|
455
529
|
end
|
|
530
|
+
l2_sig = cmsm_sig_doc
|
|
531
|
+
else
|
|
456
532
|
l2_sig = sign(l2_doc, privateKey, options).first
|
|
457
533
|
end
|
|
458
534
|
|
|
@@ -503,7 +579,17 @@ class Oydid
|
|
|
503
579
|
log_revoke_encrypted_array = nil
|
|
504
580
|
l1_sig = nil
|
|
505
581
|
if operation_mode == 3 # UPDATE
|
|
506
|
-
if
|
|
582
|
+
if options[:cmsm]
|
|
583
|
+
if cmsm_sig_create.to_s == ""
|
|
584
|
+
# the transition has to be authorised by the key of the
|
|
585
|
+
# document being replaced, not by the new one
|
|
586
|
+
return cmsm_challenge(l1_doc, "key-doc-old", cmsm_state(publicKey,
|
|
587
|
+
pubRevoKey, revocationKey, did_doc, ts, doc_location,
|
|
588
|
+
cmsm_sig_rev, cmsm_sig_doc, cmsm_sig_create,
|
|
589
|
+
did_old, cmsm_log_revoke_old), options)
|
|
590
|
+
end
|
|
591
|
+
l1_sig = cmsm_sig_create
|
|
592
|
+
else
|
|
507
593
|
l1_sig = sign(l1_doc, old_privateKey, options).first
|
|
508
594
|
end
|
|
509
595
|
l1 = { "ts": ts,
|
|
@@ -525,7 +611,18 @@ class Oydid
|
|
|
525
611
|
end
|
|
526
612
|
end unless options[:confirm_logs].nil?
|
|
527
613
|
else
|
|
528
|
-
if
|
|
614
|
+
if options[:cmsm]
|
|
615
|
+
if cmsm_sig_create.to_s == ""
|
|
616
|
+
# l1_doc is only known once the TERMINATE entry is signed, so
|
|
617
|
+
# the CREATE signature cannot be collected any earlier. Without
|
|
618
|
+
# this phase the CREATE log entry would stay unsigned.
|
|
619
|
+
return cmsm_challenge(l1_doc, "key-doc", cmsm_state(publicKey,
|
|
620
|
+
pubRevoKey, revocationKey, did_doc, ts, doc_location,
|
|
621
|
+
cmsm_sig_rev, cmsm_sig_doc, cmsm_sig_create,
|
|
622
|
+
did_old, cmsm_log_revoke_old), options)
|
|
623
|
+
end
|
|
624
|
+
l1_sig = cmsm_sig_create
|
|
625
|
+
else
|
|
529
626
|
l1_sig = sign(l1_doc, privateKey, options).first
|
|
530
627
|
end
|
|
531
628
|
l1 = { "ts": ts,
|
|
@@ -612,12 +709,118 @@ class Oydid
|
|
|
612
709
|
|
|
613
710
|
end
|
|
614
711
|
|
|
615
|
-
|
|
712
|
+
# Verify a revocation record supplied by a client against the DID it claims
|
|
713
|
+
# to revoke. Used wherever the private revocation key is not available here:
|
|
714
|
+
# a CMSM update (revoking the document being replaced) and a client-managed
|
|
715
|
+
# deactivation.
|
|
716
|
+
#
|
|
717
|
+
# Three things have to line up, and the third is the strict one: the hash of
|
|
718
|
+
# the record is committed in the TERMINATE entry of the current document, so
|
|
719
|
+
# a record from any other DID - or a re-signed one that happens to differ -
|
|
720
|
+
# cannot pass.
|
|
721
|
+
#
|
|
722
|
+
# Returns the normalised record (without "previous") or [nil, message].
|
|
723
|
+
def self.verify_revocation_log(did_info, log_revoke, field, options = {})
|
|
724
|
+
record = log_revoke
|
|
725
|
+
record = (JSON.parse(record) rescue nil) if record.is_a?(String)
|
|
726
|
+
record = record.transform_keys(&:to_s) if record.is_a?(Hash)
|
|
727
|
+
if !record.is_a?(Hash)
|
|
728
|
+
return [nil, "missing revocation record (" + field + ")"]
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
did_key = did_info["doc"]["key"].to_s
|
|
732
|
+
pubRevKey = did_key.split(":").last.to_s
|
|
733
|
+
subDid = {"doc": did_info["doc"]["doc"], "key": did_key}.to_json
|
|
734
|
+
subDidHash = multi_hash(canonical(subDid), LOG_HASH_OPTIONS).first
|
|
735
|
+
|
|
736
|
+
if record["doc"].to_s != subDidHash.to_s
|
|
737
|
+
return [nil, field + " does not match the current DID document"]
|
|
738
|
+
end
|
|
739
|
+
sig_ok, _msg = verify(subDidHash, record["sig"].to_s, pubRevKey)
|
|
740
|
+
if !sig_ok
|
|
741
|
+
return [nil, "invalid signature in " + field]
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
normalised = { "ts" => record["ts"],
|
|
745
|
+
"op" => 1, # REVOKE
|
|
746
|
+
"doc" => subDidHash,
|
|
747
|
+
"sig" => record["sig"] }
|
|
748
|
+
|
|
749
|
+
termination_doc = did_info["log"][did_info["termination_log_id"].to_i]["doc"].to_s
|
|
750
|
+
termination_doc = termination_doc.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first
|
|
751
|
+
if termination_doc != multi_hash(canonical(normalised.to_json), LOG_HASH_OPTIONS).first
|
|
752
|
+
return [nil, field + " does not match the TERMINATE entry of the current document"]
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
[normalised, ""]
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# link a verified revocation record into the log of the DID it revokes
|
|
759
|
+
def self.link_revocation_log(record, did_info)
|
|
760
|
+
log_old = did_info["log"]
|
|
761
|
+
record = record.dup
|
|
762
|
+
record["previous"] = [
|
|
763
|
+
multi_hash(canonical(log_old[did_info["doc_log_id"].to_i]), LOG_HASH_OPTIONS).first,
|
|
764
|
+
multi_hash(canonical(log_old[did_info["termination_log_id"].to_i]), LOG_HASH_OPTIONS).first
|
|
765
|
+
]
|
|
766
|
+
record
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# state of a CMSM flow that has to survive between phases. Deliberately only
|
|
770
|
+
# inputs and collected signatures - no derived hashes: every phase recomputes
|
|
771
|
+
# r1 / l2_doc / the DID from these values, so there is nothing to keep in sync.
|
|
772
|
+
def self.cmsm_state(publicKey, pubRevoKey, revocationKey, did_doc, ts, doc_location, sig_rev, sig_doc, sig_create, did_old = nil, log_revoke_old = nil)
|
|
773
|
+
{
|
|
774
|
+
publicKey: publicKey,
|
|
775
|
+
pubRevoKey: pubRevoKey,
|
|
776
|
+
revocationKey: revocationKey,
|
|
777
|
+
did_doc: did_doc,
|
|
778
|
+
ts: ts,
|
|
779
|
+
doc_location: doc_location,
|
|
780
|
+
sig_rev: sig_rev,
|
|
781
|
+
sig_doc: sig_doc,
|
|
782
|
+
# signature over l1_doc: the CREATE entry on create, the UPDATE entry
|
|
783
|
+
# on update - the latter is signed with the OLD document key
|
|
784
|
+
sig_create: sig_create,
|
|
785
|
+
# update only: which DID is being replaced, and the revocation record
|
|
786
|
+
# of its current document (supplied by the client in phase 1)
|
|
787
|
+
did_old: did_old,
|
|
788
|
+
log_revoke_old: log_revoke_old
|
|
789
|
+
}
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# persist the state of a CMSM flow and return the next challenge: the value
|
|
793
|
+
# the client has to sign, and which of its keys it has to sign it with
|
|
794
|
+
def self.cmsm_challenge(value, with, state, options)
|
|
795
|
+
session, msg = persist_cmsm(options[:cmsm_session], state, options)
|
|
796
|
+
if session.nil?
|
|
797
|
+
return [nil, nil, nil, msg]
|
|
798
|
+
end
|
|
799
|
+
collected = [state[:sig_rev], state[:sig_doc], state[:sig_create]].count { |sig| sig.to_s != "" }
|
|
800
|
+
cmsm_doc = {
|
|
801
|
+
cmsm: true,
|
|
802
|
+
session: session,
|
|
803
|
+
phase: collected + 1,
|
|
804
|
+
pk: state[:publicKey],
|
|
805
|
+
sign: value,
|
|
806
|
+
with: with
|
|
807
|
+
}
|
|
808
|
+
return [cmsm_doc, nil, nil, "cmsm"]
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# persist the intermediate data of a CMSM flow and return the session handle
|
|
812
|
+
# it is stored under. A blank session starts a new flow; the handle is
|
|
813
|
+
# generated here so that the local and the remote store behave identically.
|
|
814
|
+
def self.persist_cmsm(session, payload, options)
|
|
815
|
+
if session.to_s == ""
|
|
816
|
+
session = "cmsm-" + SecureRandom.hex(8)
|
|
817
|
+
end
|
|
818
|
+
|
|
616
819
|
# in-process callers can supply a store object (responding to
|
|
617
|
-
# #set(
|
|
820
|
+
# #set(session, payload)) to persist CMSM data in a local database
|
|
618
821
|
if options[:cmsm_store]
|
|
619
|
-
options[:cmsm_store].set(
|
|
620
|
-
return [
|
|
822
|
+
options[:cmsm_store].set(session, payload)
|
|
823
|
+
return [session, ""]
|
|
621
824
|
end
|
|
622
825
|
|
|
623
826
|
doc_location = options[:doc_location]
|
|
@@ -627,7 +830,8 @@ class Oydid
|
|
|
627
830
|
doc_location = doc_location.sub("%3A%2F%2F","://").sub("%3A", ":")
|
|
628
831
|
|
|
629
832
|
my_body = {
|
|
630
|
-
|
|
833
|
+
session: session,
|
|
834
|
+
pubkey: payload[:publicKey] || payload["publicKey"],
|
|
631
835
|
payload: payload.to_json
|
|
632
836
|
}
|
|
633
837
|
case doc_location.to_s
|
|
@@ -638,22 +842,22 @@ class Oydid
|
|
|
638
842
|
body: my_body.to_json )
|
|
639
843
|
if retVal.code != 200
|
|
640
844
|
err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/cmsm"
|
|
641
|
-
return [
|
|
845
|
+
return [nil, err_msg]
|
|
642
846
|
end
|
|
643
847
|
else
|
|
644
848
|
return [nil, "location not supported for persisting data in cmsm-flow"]
|
|
645
849
|
end
|
|
646
|
-
return [
|
|
850
|
+
return [session, ""]
|
|
647
851
|
|
|
648
852
|
end
|
|
649
853
|
|
|
650
|
-
def self.check_cmsm(
|
|
854
|
+
def self.check_cmsm(session, options)
|
|
651
855
|
# in-process callers can supply a store object (responding to
|
|
652
|
-
# #get(
|
|
856
|
+
# #get(session)) to read CMSM data from a local database
|
|
653
857
|
if options[:cmsm_store]
|
|
654
|
-
payload = options[:cmsm_store].get(
|
|
858
|
+
payload = options[:cmsm_store].get(session)
|
|
655
859
|
if payload.nil?
|
|
656
|
-
return [nil, "
|
|
860
|
+
return [nil, "unknown or expired CMSM session"]
|
|
657
861
|
end
|
|
658
862
|
payload = payload.transform_keys(&:to_s) if payload.is_a?(Hash)
|
|
659
863
|
return [payload, ""]
|
|
@@ -667,11 +871,11 @@ class Oydid
|
|
|
667
871
|
|
|
668
872
|
case doc_location.to_s
|
|
669
873
|
when /^http/
|
|
670
|
-
retVal = HTTParty.get(doc_location + "/cmsm/" +
|
|
874
|
+
retVal = HTTParty.get(doc_location + "/cmsm/" + session.to_s)
|
|
671
875
|
if retVal.code != 200
|
|
672
876
|
msg = retVal.parsed_response["error"].to_s rescue ""
|
|
673
877
|
if msg.to_s == ""
|
|
674
|
-
msg = "invalid response from " + doc_location.to_s + "/cmsm/" +
|
|
878
|
+
msg = "invalid response from " + doc_location.to_s + "/cmsm/" + session.to_s
|
|
675
879
|
end
|
|
676
880
|
return [nil, msg]
|
|
677
881
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: oydid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Christoph Fabianek
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: simple_dag
|
|
@@ -433,7 +433,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
433
433
|
- !ruby/object:Gem::Version
|
|
434
434
|
version: '0'
|
|
435
435
|
requirements: []
|
|
436
|
-
rubygems_version: 4.0.
|
|
436
|
+
rubygems_version: 4.0.19
|
|
437
437
|
specification_version: 4
|
|
438
438
|
summary: Own Your Decentralized Identifier for Ruby.
|
|
439
439
|
test_files:
|