oydid 0.6.7 → 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 +29 -1
- data/lib/oydid/log.rb +27 -7
- data/lib/oydid.rb +319 -92
- data/spec/fixtures/multi_hash_legacy.json +1766 -0
- data/spec/oydid_spec.rb +130 -0
- metadata +5 -3
data/lib/oydid.rb
CHANGED
|
@@ -26,7 +26,12 @@ class Oydid
|
|
|
26
26
|
LOCATION_PREFIX = "@"
|
|
27
27
|
DEFAULT_LOCATION = "https://oydid.ownyourdata.eu"
|
|
28
28
|
DEFAULT_DIGEST = "sha2-256"
|
|
29
|
-
|
|
29
|
+
# BLAKE2b digests are named by digest size in BYTES here (house convention,
|
|
30
|
+
# e.g. "blake2b-16" is a 16 byte / 128 bit digest). This deliberately
|
|
31
|
+
# deviates from the multicodec registry, where blake2b-N counts bits.
|
|
32
|
+
SUPPORTED_DIGESTS = ["sha2-256", "sha2-512", "sha3-224", "sha3-256", "sha3-384", "sha3-512",
|
|
33
|
+
"blake2b-16", "blake2b-17", "blake2b-18", "blake2b-19", "blake2b-20",
|
|
34
|
+
"blake2b-21", "blake2b-22", "blake2b-23", "blake2b-32", "blake2b-64"]
|
|
30
35
|
DEFAULT_ENCODING = "base58btc"
|
|
31
36
|
SUPPORTED_ENCODINGS = ["base16", "base32", "base58btc", "base64"]
|
|
32
37
|
LOG_HASH_OPTIONS = {:digest => "sha2-256", :encode => "base58btc"}
|
|
@@ -34,6 +39,24 @@ class Oydid
|
|
|
34
39
|
JWS_SECURITY_SUITE = "https://w3id.org/security/suites/jws-2020/v1"
|
|
35
40
|
DEFAULT_PUBLIC_RESOLVER = "https://dev.uniresolver.io/1.0/identifiers/"
|
|
36
41
|
|
|
42
|
+
# Single-byte multicodec codes for the intermediate BLAKE2b digest sizes
|
|
43
|
+
# (17-23 bytes), needed to keep a did:oyd identifier short enough for the
|
|
44
|
+
# 50 character URL limit of the EU Digital Product Passport registry.
|
|
45
|
+
#
|
|
46
|
+
# These sizes cannot take their code from Multicodecs[]: multi_hash packs
|
|
47
|
+
# the code into a single byte, which effectively truncates the registry's
|
|
48
|
+
# two-byte blake2b codes to their low byte - and those collide with the SHA
|
|
49
|
+
# codes already in use (0xb212 -> 0x12 = sha2-256, 0xb214 -> 0x14 =
|
|
50
|
+
# sha3-512, 0xb216 -> 0x16 = sha3-256, 0xb217 -> 0x17 = sha3-224).
|
|
51
|
+
#
|
|
52
|
+
# Instead the free, contiguous range 0x0B-0x11 is used (code = size - 6).
|
|
53
|
+
# Codes are kept low and single-byte on purpose: the value of the leading
|
|
54
|
+
# byte feeds into the base58btc length, and a low code saves a character.
|
|
55
|
+
BLAKE2B_EXTRA_CODES = {
|
|
56
|
+
17 => 0x0B, 18 => 0x0C, 19 => 0x0D, 20 => 0x0E,
|
|
57
|
+
21 => 0x0F, 22 => 0x10, 23 => 0x11
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
37
60
|
# full Multicodecs table: https://github.com/multiformats/multicodec/blob/master/table.csv
|
|
38
61
|
# Multicodecs.register(code: 0x1305, name: 'rsa-priv', tag: 'key')
|
|
39
62
|
# Multicodecs.register(code: 0x1205, name: 'rsa-pub', tag: 'key')
|
|
@@ -199,54 +222,98 @@ class Oydid
|
|
|
199
222
|
ts = options[:ts]
|
|
200
223
|
end
|
|
201
224
|
|
|
202
|
-
|
|
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
|
+
|
|
203
232
|
if options[:cmsm]
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
216
265
|
else
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
219
275
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
223
283
|
if payload.is_a?(String)
|
|
224
284
|
payload = JSON.parse(payload) rescue nil
|
|
225
285
|
end
|
|
226
286
|
if payload.nil?
|
|
227
287
|
return [nil, nil, nil, "invalid persisted data in CMSM flow"]
|
|
228
288
|
end
|
|
229
|
-
|
|
230
|
-
if did_doc["opt"].nil?
|
|
231
|
-
if options[:sig].nil?
|
|
232
|
-
return [nil, nil, nil, "1missing signature in CMSM flow (sig)"]
|
|
233
|
-
end
|
|
234
|
-
l2_sig = options[:sig]
|
|
235
|
-
else
|
|
236
|
-
if did_doc["opt"]["sig"].nil?
|
|
237
|
-
return [nil, nil, nil, "2missing signature in CMSM flow (sig)"]
|
|
238
|
-
end
|
|
239
|
-
l2_sig = did_doc["opt"]["sig"]
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
options[:cmsm2] = true
|
|
243
|
-
privateKey = nil
|
|
289
|
+
payload = payload.transform_keys(&:to_s)
|
|
244
290
|
|
|
291
|
+
privateKey = nil
|
|
292
|
+
publicKey = payload["publicKey"]
|
|
293
|
+
pubRevoKey = payload["pubRevoKey"]
|
|
245
294
|
revocationKey = payload["revocationKey"]
|
|
246
|
-
did_doc
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
250
317
|
end
|
|
251
318
|
else
|
|
252
319
|
# key management
|
|
@@ -299,6 +366,27 @@ class Oydid
|
|
|
299
366
|
log_old = did_info["log"]
|
|
300
367
|
|
|
301
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
|
|
302
390
|
tmp_old_doc_did10 = did10_old + "_private_key.enc" rescue ""
|
|
303
391
|
old_privateKey, msg = getPrivateKey(options[:old_doc_enc], options[:old_doc_pwd], options[:old_doc_key], tmp_old_doc_did10, options)
|
|
304
392
|
tmp_old_rev_did10 = did10_old + "_revocation_key.enc" rescue ""
|
|
@@ -352,6 +440,7 @@ class Oydid
|
|
|
352
440
|
return [nil, nil, nil, "cannot decrypt revocation log entry: " + msg]
|
|
353
441
|
end
|
|
354
442
|
end # compare old keys with existing DID Document
|
|
443
|
+
end # CMSM or self-managed
|
|
355
444
|
|
|
356
445
|
revoc_log = JSON.parse(revocationLog)
|
|
357
446
|
revoc_log["previous"] = [
|
|
@@ -360,13 +449,15 @@ class Oydid
|
|
|
360
449
|
]
|
|
361
450
|
prev_hash = [multi_hash(canonical(revoc_log), LOG_HASH_OPTIONS).first]
|
|
362
451
|
end
|
|
363
|
-
if !options[:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
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
|
|
369
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
|
|
370
461
|
if options[:keyAgreement]
|
|
371
462
|
if did_doc.nil?
|
|
372
463
|
did_doc = {}
|
|
@@ -392,44 +483,52 @@ class Oydid
|
|
|
392
483
|
did_doc[:authentication] = ["#key-doc"]
|
|
393
484
|
did_doc = did_doc.transform_keys(&:to_s)
|
|
394
485
|
end
|
|
486
|
+
end
|
|
395
487
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
403
510
|
signedSubDidHash = sign(subDidHash, revocationKey, LOG_HASH_OPTIONS).first
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
# build termination log entry
|
|
410
|
-
l2_doc = multi_hash(canonical(r1), LOG_HASH_OPTIONS).first
|
|
411
|
-
if !doc_location.nil?
|
|
412
|
-
l2_doc += LOCATION_PREFIX + doc_location.to_s
|
|
413
|
-
end
|
|
511
|
+
end
|
|
512
|
+
r1 = { "ts": ts,
|
|
513
|
+
"op": 1, # REVOKE
|
|
514
|
+
"doc": subDidHash,
|
|
515
|
+
"sig": signedSubDidHash }.transform_keys(&:to_s)
|
|
414
516
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
did_key: did_key,
|
|
421
|
-
l2_doc: l2_doc,
|
|
422
|
-
r1: r1
|
|
423
|
-
}
|
|
424
|
-
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
|
|
425
522
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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)
|
|
432
529
|
end
|
|
530
|
+
l2_sig = cmsm_sig_doc
|
|
531
|
+
else
|
|
433
532
|
l2_sig = sign(l2_doc, privateKey, options).first
|
|
434
533
|
end
|
|
435
534
|
|
|
@@ -480,7 +579,17 @@ class Oydid
|
|
|
480
579
|
log_revoke_encrypted_array = nil
|
|
481
580
|
l1_sig = nil
|
|
482
581
|
if operation_mode == 3 # UPDATE
|
|
483
|
-
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
|
|
484
593
|
l1_sig = sign(l1_doc, old_privateKey, options).first
|
|
485
594
|
end
|
|
486
595
|
l1 = { "ts": ts,
|
|
@@ -502,7 +611,18 @@ class Oydid
|
|
|
502
611
|
end
|
|
503
612
|
end unless options[:confirm_logs].nil?
|
|
504
613
|
else
|
|
505
|
-
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
|
|
506
626
|
l1_sig = sign(l1_doc, privateKey, options).first
|
|
507
627
|
end
|
|
508
628
|
l1 = { "ts": ts,
|
|
@@ -589,12 +709,118 @@ class Oydid
|
|
|
589
709
|
|
|
590
710
|
end
|
|
591
711
|
|
|
592
|
-
|
|
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
|
+
|
|
593
819
|
# in-process callers can supply a store object (responding to
|
|
594
|
-
# #set(
|
|
820
|
+
# #set(session, payload)) to persist CMSM data in a local database
|
|
595
821
|
if options[:cmsm_store]
|
|
596
|
-
options[:cmsm_store].set(
|
|
597
|
-
return [
|
|
822
|
+
options[:cmsm_store].set(session, payload)
|
|
823
|
+
return [session, ""]
|
|
598
824
|
end
|
|
599
825
|
|
|
600
826
|
doc_location = options[:doc_location]
|
|
@@ -604,7 +830,8 @@ class Oydid
|
|
|
604
830
|
doc_location = doc_location.sub("%3A%2F%2F","://").sub("%3A", ":")
|
|
605
831
|
|
|
606
832
|
my_body = {
|
|
607
|
-
|
|
833
|
+
session: session,
|
|
834
|
+
pubkey: payload[:publicKey] || payload["publicKey"],
|
|
608
835
|
payload: payload.to_json
|
|
609
836
|
}
|
|
610
837
|
case doc_location.to_s
|
|
@@ -615,22 +842,22 @@ class Oydid
|
|
|
615
842
|
body: my_body.to_json )
|
|
616
843
|
if retVal.code != 200
|
|
617
844
|
err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/cmsm"
|
|
618
|
-
return [
|
|
845
|
+
return [nil, err_msg]
|
|
619
846
|
end
|
|
620
847
|
else
|
|
621
848
|
return [nil, "location not supported for persisting data in cmsm-flow"]
|
|
622
849
|
end
|
|
623
|
-
return [
|
|
850
|
+
return [session, ""]
|
|
624
851
|
|
|
625
852
|
end
|
|
626
853
|
|
|
627
|
-
def self.check_cmsm(
|
|
854
|
+
def self.check_cmsm(session, options)
|
|
628
855
|
# in-process callers can supply a store object (responding to
|
|
629
|
-
# #get(
|
|
856
|
+
# #get(session)) to read CMSM data from a local database
|
|
630
857
|
if options[:cmsm_store]
|
|
631
|
-
payload = options[:cmsm_store].get(
|
|
858
|
+
payload = options[:cmsm_store].get(session)
|
|
632
859
|
if payload.nil?
|
|
633
|
-
return [nil, "
|
|
860
|
+
return [nil, "unknown or expired CMSM session"]
|
|
634
861
|
end
|
|
635
862
|
payload = payload.transform_keys(&:to_s) if payload.is_a?(Hash)
|
|
636
863
|
return [payload, ""]
|
|
@@ -644,11 +871,11 @@ class Oydid
|
|
|
644
871
|
|
|
645
872
|
case doc_location.to_s
|
|
646
873
|
when /^http/
|
|
647
|
-
retVal = HTTParty.get(doc_location + "/cmsm/" +
|
|
874
|
+
retVal = HTTParty.get(doc_location + "/cmsm/" + session.to_s)
|
|
648
875
|
if retVal.code != 200
|
|
649
876
|
msg = retVal.parsed_response["error"].to_s rescue ""
|
|
650
877
|
if msg.to_s == ""
|
|
651
|
-
msg = "invalid response from " + doc_location.to_s + "/cmsm/" +
|
|
878
|
+
msg = "invalid response from " + doc_location.to_s + "/cmsm/" + session.to_s
|
|
652
879
|
end
|
|
653
880
|
return [nil, msg]
|
|
654
881
|
end
|