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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc7743afc702237fde6c94c6b4deb6dfd667ec8e0671952dbc0262414b67f2ea
4
- data.tar.gz: a51344bf8e704bbe23a5e8c5f8a663000e701ce55803a171ff5b6f994a8089c2
3
+ metadata.gz: e119ee021cb7e09dd4c7ee6273a4ee778bae850e31b6360a31044c11e674df4c
4
+ data.tar.gz: 9be71c4690f1e6476c6a339a49172575eac79c11406c6a1ac8a49e66c804ee6e
5
5
  SHA512:
6
- metadata.gz: 1d2696a20122730603f44cad828e602f59791fad06a3f93fa70aca358445e35ac1af7cf38951cd3a4d0179dfdb47f2d38770d29c0cec162129f2730849431e46
7
- data.tar.gz: b3c13897c837fb7be6ede4b4ce2e12251c0e9ff1546bde6123b8437f179d6d758e57ee5939931da1f3a08278d60c74d55e326d5f26b9ec05a4e22847e1530dfa
6
+ metadata.gz: fa57e8c74580e2afa7e6adae48528fd8d43fd306176eb9039a1531c9db46c0090702eef2d036ddaee3aef09e568ef8ad2714c990e6745b2dff4ed080d5e18874
7
+ data.tar.gz: 4608b92f08da163850832144c89a9495c182a065155727a8d10bacb83ee00fd47951b442420e9ee24582a5804938349e55649b57fe0e699082aa3311fbc57729
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.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
- doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {})
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 for CREATE is optional (due to CMSM)
313
- if !el["sig"].nil?
314
- if !match_log_did?(el, doc)
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"] = "Signatures in log don't match"
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
- doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {})
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
- options[:cmsm2] = false
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
- if did_doc["key"].nil?
228
- return [nil, nil, nil, "CMSM requires public key"]
229
- end
230
- cmsm_keys = did_doc["key"].split(':')
231
- did_doc.delete("key")
232
- if did_doc == {}
233
- did_doc = nil
234
- end
235
- if cmsm_keys.count == 1
236
- publicKey = cmsm_keys.first
237
- revocationKey, msg = generate_private_key("", options[:key_type]+'-priv', options)
238
- pubRevoKey = public_key(revocationKey, options).first
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
- return [nil, nil, nil, "CMSM with multiple keys is not yet supported"]
241
- end
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
- # check if information for provided key already exists
244
- payload, msg = check_cmsm(publicKey, options)
245
- if !payload.nil? && !did_doc.nil? && !did_doc["opt"].nil?
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
- did_doc = JSON.parse(did_doc.to_json)
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 = payload["did_doc"]
270
- did_key = payload["did_key"]
271
- l2_doc = payload["l2_doc"]
272
- r1 = payload["r1"]
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[:cmsm2]
387
- if !options[:cmsm]
388
- publicKey = public_key(privateKey, options).first
389
- pubRevoKey = public_key(revocationKey, options).first
390
- end
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
- # build new revocation document
420
- subDid = {"doc": did_doc, "key": did_key}.to_json
421
- retVal = multi_hash(canonical(subDid), LOG_HASH_OPTIONS)
422
- if retVal.first.nil?
423
- return [nil, nil, nil, retVal.last]
424
- end
425
- subDidHash = retVal.first
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
- r1 = { "ts": ts,
428
- "op": 1, # REVOKE
429
- "doc": subDidHash,
430
- "sig": signedSubDidHash }.transform_keys(&:to_s)
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
- if options[:cmsm]
439
- # persist data
440
- payload = {
441
- revocationKey: revocationKey,
442
- did_doc: did_doc,
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
- cmsm_doc = {
450
- cmsm: true,
451
- pk: publicKey,
452
- sign: l2_doc
453
- }
454
- return [cmsm_doc, nil, r1, "cmsm"]
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 !options[:cmsm]
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 !options[:cmsm]
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
- def self.persist_cmsm(pubkey, payload, options)
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(pubkey, payload)) to persist CMSM data in a local database
820
+ # #set(session, payload)) to persist CMSM data in a local database
618
821
  if options[:cmsm_store]
619
- options[:cmsm_store].set(pubkey, payload)
620
- return [true, ""]
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
- pubkey: pubkey,
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 [false, err_msg]
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 [true, ""]
850
+ return [session, ""]
647
851
 
648
852
  end
649
853
 
650
- def self.check_cmsm(pubkey, options)
854
+ def self.check_cmsm(session, options)
651
855
  # in-process callers can supply a store object (responding to
652
- # #get(pubkey)) to read CMSM data from a local database
856
+ # #get(session)) to read CMSM data from a local database
653
857
  if options[:cmsm_store]
654
- payload = options[:cmsm_store].get(pubkey)
858
+ payload = options[:cmsm_store].get(session)
655
859
  if payload.nil?
656
- return [nil, "no persisted CMSM data for key"]
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/" + pubkey)
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/" + pubkey.to_s
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.7.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-21 00:00:00.000000000 Z
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.12
436
+ rubygems_version: 4.0.19
437
437
  specification_version: 4
438
438
  summary: Own Your Decentralized Identifier for Ruby.
439
439
  test_files: