oydid 0.9.1 → 0.9.3
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 -10
- data/lib/oydid/log.rb +27 -13
- data/lib/oydid.rb +40 -3
- data/spec/oydid_spec.rb +203 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b941c83aec6492dfb37d272fd83a77f1b036902b70aab64634fed657d1ccaf6a
|
|
4
|
+
data.tar.gz: 0ba0f288c5c30e2cd7d7d831bd072ef0c4924082d70fba44634e1412578a8dc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddc6020c44371fee58f2e527513e4d94974ee5d4a3ebabd22a01b5cc039f5c91cfe1a0ab0f4046c50a356ab0784db6a626c971c5ee18e10b076e03625250605e
|
|
7
|
+
data.tar.gz: eaba43282881c1239cde7af7733707d5beff7670b27388327926b4e7a5c739e9c221c1f9b70565a629adb3e7b4ee43dd3ebb3cb977bf6b2dd885ff6f55a34e93
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.3
|
data/lib/oydid/basic.rb
CHANGED
|
@@ -338,7 +338,7 @@ class Oydid
|
|
|
338
338
|
return [nil, msg]
|
|
339
339
|
exit
|
|
340
340
|
end
|
|
341
|
-
pubKey = did_document["doc"]["key"].split(":")
|
|
341
|
+
pubKey = did_document["doc"]["key"].split(":")[0] rescue nil
|
|
342
342
|
if pubKey.nil?
|
|
343
343
|
return [nil, "cannot resolve " + did.to_s]
|
|
344
344
|
else
|
|
@@ -367,9 +367,9 @@ class Oydid
|
|
|
367
367
|
# get current public key
|
|
368
368
|
case key_type
|
|
369
369
|
when "doc"
|
|
370
|
-
keys = [did_document["doc"]["key"].split(":")
|
|
370
|
+
keys = [did_document["doc"]["key"].split(":")[0]] rescue nil
|
|
371
371
|
when "rev"
|
|
372
|
-
keys = [did_document["doc"]["key"].split(":")
|
|
372
|
+
keys = [did_document["doc"]["key"].split(":")[1]] rescue nil
|
|
373
373
|
else
|
|
374
374
|
return [nil, "invalid key type: " + key_type]
|
|
375
375
|
end
|
|
@@ -1248,6 +1248,30 @@ class Oydid
|
|
|
1248
1248
|
end
|
|
1249
1249
|
end
|
|
1250
1250
|
|
|
1251
|
+
# Marks a failure of the queried repository itself (HTTP 5xx) as opposed to
|
|
1252
|
+
# a DID that is simply not stored there. Without the distinction a caller
|
|
1253
|
+
# turns a broken upstream into "not found", which claims the identifier
|
|
1254
|
+
# never existed - see upstream_error?.
|
|
1255
|
+
UPSTREAM_ERROR_PREFIX = "upstream error"
|
|
1256
|
+
|
|
1257
|
+
def self.upstream_error?(message)
|
|
1258
|
+
message.to_s.start_with?(UPSTREAM_ERROR_PREFIX)
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
# Failure message for a non-200 response: the repository's own error field
|
|
1262
|
+
# when it sent one, otherwise a message naming status and URL. A 5xx gets
|
|
1263
|
+
# the prefix above so callers can tell it apart from a missing DID.
|
|
1264
|
+
def self.http_error_message(response, url)
|
|
1265
|
+
msg = (response.parsed_response["error"].to_s rescue "")
|
|
1266
|
+
return msg unless msg.to_s == ""
|
|
1267
|
+
code = response.code.to_i rescue 0
|
|
1268
|
+
if code >= 500
|
|
1269
|
+
UPSTREAM_ERROR_PREFIX + " " + code.to_s + " from " + url.to_s
|
|
1270
|
+
else
|
|
1271
|
+
"invalid response from " + url.to_s
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1251
1275
|
def self.retrieve_document(doc_identifier, doc_file, doc_location, options)
|
|
1252
1276
|
# in-process callers can supply the DID document directly (e.g. read from
|
|
1253
1277
|
# a local database) to avoid any HTTP/file lookup
|
|
@@ -1271,11 +1295,7 @@ class Oydid
|
|
|
1271
1295
|
end
|
|
1272
1296
|
retVal = HTTParty.get(doc_location + "/doc/" + doc_identifier + option_str)
|
|
1273
1297
|
if retVal.code != 200
|
|
1274
|
-
|
|
1275
|
-
if msg.to_s == ""
|
|
1276
|
-
msg = "invalid response from " + doc_location.to_s + "/doc/" + doc_identifier.to_s
|
|
1277
|
-
end
|
|
1278
|
-
return [nil, msg]
|
|
1298
|
+
return [nil, http_error_message(retVal, doc_location.to_s + "/doc/" + doc_identifier.to_s)]
|
|
1279
1299
|
end
|
|
1280
1300
|
if options.transform_keys(&:to_s)["trace"]
|
|
1281
1301
|
if options[:silent].nil? || !options[:silent]
|
|
@@ -1324,8 +1344,7 @@ class Oydid
|
|
|
1324
1344
|
doc_location = doc_location.sub("%3A%2F%2F","://").sub("%3A", ":")
|
|
1325
1345
|
retVal = HTTParty.get(doc_location + "/doc_raw/" + doc_hash)
|
|
1326
1346
|
if retVal.code != 200
|
|
1327
|
-
|
|
1328
|
-
return [nil, msg]
|
|
1347
|
+
return [nil, http_error_message(retVal, doc_location.to_s + "/doc_raw/" + doc_hash.to_s)]
|
|
1329
1348
|
end
|
|
1330
1349
|
if options.transform_keys(&:to_s)["trace"]
|
|
1331
1350
|
if options[:silent].nil? || !options[:silent]
|
data/lib/oydid/log.rb
CHANGED
|
@@ -44,10 +44,7 @@ class Oydid
|
|
|
44
44
|
log_location = log_location.gsub("%2F%2F","//")
|
|
45
45
|
retVal = HTTParty.get(log_location + "/log/" + did_hash)
|
|
46
46
|
if retVal.code != 200
|
|
47
|
-
|
|
48
|
-
"invalid response from " + log_location.to_s + "/log/" + did_hash.to_s
|
|
49
|
-
|
|
50
|
-
return [nil, msg]
|
|
47
|
+
return [nil, http_error_message(retVal, log_location.to_s + "/log/" + did_hash.to_s)]
|
|
51
48
|
end
|
|
52
49
|
if options.transform_keys(&:to_s)["trace"]
|
|
53
50
|
if options[:silent].nil? || !options[:silent]
|
|
@@ -79,9 +76,7 @@ class Oydid
|
|
|
79
76
|
log_location = log_location.gsub("%2F%2F","//")
|
|
80
77
|
retVal = HTTParty.get(log_location + "/log/" + log_hash + "/item")
|
|
81
78
|
if retVal.code != 200
|
|
82
|
-
|
|
83
|
-
"invalid response from " + log_location.to_s + "/log/" + log_hash.to_s + "/item"
|
|
84
|
-
return [nil, msg]
|
|
79
|
+
return [nil, http_error_message(retVal, log_location.to_s + "/log/" + log_hash.to_s + "/item")]
|
|
85
80
|
end
|
|
86
81
|
if options.transform_keys(&:to_s)["trace"]
|
|
87
82
|
if options[:silent].nil? || !options[:silent]
|
|
@@ -361,7 +356,7 @@ class Oydid
|
|
|
361
356
|
currentDID["verification"] += JSON.pretty_generate(doc) + "\n"
|
|
362
357
|
currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
|
|
363
358
|
end
|
|
364
|
-
current_public_doc_key = currentDID["doc"]["key"].split(":")
|
|
359
|
+
current_public_doc_key = currentDID["doc"]["key"].split(":")[0] rescue ""
|
|
365
360
|
|
|
366
361
|
when 0 # TERMINATE
|
|
367
362
|
currentDID["termination_log_id"] = i
|
|
@@ -506,7 +501,7 @@ class Oydid
|
|
|
506
501
|
return currentDID
|
|
507
502
|
end
|
|
508
503
|
next_doc = next_doc.first["doc"]
|
|
509
|
-
if pubKeys.include?(next_doc["key"].split(":")
|
|
504
|
+
if pubKeys.include?(next_doc["key"].split(":")[0])
|
|
510
505
|
currentDID["verification"] += "⚠️ no key rotation in updated DID Document" + "\n"
|
|
511
506
|
end
|
|
512
507
|
currentDID["verification"] += "\n"
|
|
@@ -558,9 +553,22 @@ class Oydid
|
|
|
558
553
|
# handle DID Rotation
|
|
559
554
|
if (i == (currentDID["log"].length-1))
|
|
560
555
|
if options[:followAlsoKnownAs]
|
|
556
|
+
# the payload of a DID Document is arbitrary JSON: an
|
|
557
|
+
# Array or a scalar is valid and simply carries no
|
|
558
|
+
# alsoKnownAs, so only a Hash can be inspected for it.
|
|
559
|
+
# Without this guard a revoked DID whose document holds
|
|
560
|
+
# e.g. [5] raised NoMethodError here, the repository
|
|
561
|
+
# answered 500 with an empty body, and the resolver
|
|
562
|
+
# degraded that to "not found" instead of "deactivated".
|
|
561
563
|
current_doc = currentDID["doc"]
|
|
562
|
-
|
|
563
|
-
|
|
564
|
+
doc_payload = current_doc.is_a?(Hash) ? current_doc["doc"] : nil
|
|
565
|
+
doc_payload = doc_payload.transform_keys(&:to_s) if doc_payload.is_a?(Hash)
|
|
566
|
+
if doc_payload.is_a?(Hash) && doc_payload.has_key?("alsoKnownAs")
|
|
567
|
+
# DID Core defines alsoKnownAs as a set; older
|
|
568
|
+
# documents wrote a bare String
|
|
569
|
+
rotate_DID = doc_payload["alsoKnownAs"]
|
|
570
|
+
rotate_DID = rotate_DID.first if rotate_DID.is_a?(Array)
|
|
571
|
+
rotate_DID = rotate_DID.to_s
|
|
564
572
|
if rotate_DID.start_with?("did:")
|
|
565
573
|
rotate_DID_method = rotate_DID.split(":").take(2).join(":")
|
|
566
574
|
did_orig = currentDID["did"]
|
|
@@ -586,8 +594,14 @@ class Oydid
|
|
|
586
594
|
currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#did_rotation)" + "\n\n"
|
|
587
595
|
end
|
|
588
596
|
when "did:oyd"
|
|
589
|
-
|
|
590
|
-
|
|
597
|
+
# DID Rotation to another did:oyd identifier
|
|
598
|
+
# is not implemented yet: resolving it needs
|
|
599
|
+
# a second pass through this resolver.
|
|
600
|
+
if verification_output
|
|
601
|
+
currentDID["verification"] += "DID rotation to: " + rotate_DID.to_s + "\n"
|
|
602
|
+
currentDID["verification"] += "⚠️ rotation to another did:oyd identifier is not supported yet\n"
|
|
603
|
+
currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#did_rotation)" + "\n\n"
|
|
604
|
+
end
|
|
591
605
|
else
|
|
592
606
|
# do nothing: DID Rotation is not supported for this DID method yet
|
|
593
607
|
end
|
data/lib/oydid.rb
CHANGED
|
@@ -383,8 +383,8 @@ class Oydid
|
|
|
383
383
|
old_privateKey = nil
|
|
384
384
|
old_revocationKey = nil
|
|
385
385
|
old_did_key = did_info["doc"]["key"].to_s
|
|
386
|
-
old_publicDocKey = old_did_key.split(":").
|
|
387
|
-
old_publicRevKey = old_did_key.split(":").
|
|
386
|
+
old_publicDocKey = old_did_key.split(":")[0].to_s
|
|
387
|
+
old_publicRevKey = old_did_key.split(":")[1].to_s
|
|
388
388
|
|
|
389
389
|
verified, vmsg = verify_revocation_log(did_info, options[:log_revoke_old],
|
|
390
390
|
"log_revoke_old", options)
|
|
@@ -460,6 +460,16 @@ class Oydid
|
|
|
460
460
|
publicKey = public_key(privateKey, options).first
|
|
461
461
|
pubRevoKey = public_key(revocationKey, options).first
|
|
462
462
|
end
|
|
463
|
+
# The "key" field is the control field of a did:oyd: it names who may
|
|
464
|
+
# update the document (slot 0) and who may revoke it (slot 1) - nothing
|
|
465
|
+
# else. Keys that are content rather than control (key agreement above
|
|
466
|
+
# all) belong in the payload as verification methods, where they are
|
|
467
|
+
# covered by the identifier hash just the same.
|
|
468
|
+
#
|
|
469
|
+
# The field is positional and has exactly two slots. Readers must use an
|
|
470
|
+
# explicit index; .last would silently return the wrong key if a third
|
|
471
|
+
# slot were ever appended, and the mistake would only surface at
|
|
472
|
+
# revocation time. A spec enforces this across the repository.
|
|
463
473
|
did_key = publicKey + ":" + pubRevoKey
|
|
464
474
|
|
|
465
475
|
# the document is assembled once, in the first phase of a CMSM flow -
|
|
@@ -512,6 +522,10 @@ class Oydid
|
|
|
512
522
|
end
|
|
513
523
|
|
|
514
524
|
if revocationKey.to_s == ""
|
|
525
|
+
if options[:cmsm]
|
|
526
|
+
err = cmsm_verify_signature(subDidHash, cmsm_sig_rev, pubRevoKey, "key-rev")
|
|
527
|
+
return [nil, nil, nil, err] if !err.nil?
|
|
528
|
+
end
|
|
515
529
|
signedSubDidHash = cmsm_sig_rev
|
|
516
530
|
else
|
|
517
531
|
signedSubDidHash = sign(subDidHash, revocationKey, LOG_HASH_OPTIONS).first
|
|
@@ -534,6 +548,8 @@ class Oydid
|
|
|
534
548
|
cmsm_sig_rev, cmsm_sig_doc, cmsm_sig_create,
|
|
535
549
|
did_old, cmsm_log_revoke_old), options)
|
|
536
550
|
end
|
|
551
|
+
err = cmsm_verify_signature(l2_doc, cmsm_sig_doc, publicKey, "key-doc")
|
|
552
|
+
return [nil, nil, nil, err] if !err.nil?
|
|
537
553
|
l2_sig = cmsm_sig_doc
|
|
538
554
|
else
|
|
539
555
|
l2_sig = sign(l2_doc, privateKey, options).first
|
|
@@ -595,6 +611,8 @@ class Oydid
|
|
|
595
611
|
cmsm_sig_rev, cmsm_sig_doc, cmsm_sig_create,
|
|
596
612
|
did_old, cmsm_log_revoke_old), options)
|
|
597
613
|
end
|
|
614
|
+
err = cmsm_verify_signature(l1_doc, cmsm_sig_create, old_publicDocKey, "key-doc-old")
|
|
615
|
+
return [nil, nil, nil, err] if !err.nil?
|
|
598
616
|
l1_sig = cmsm_sig_create
|
|
599
617
|
else
|
|
600
618
|
l1_sig = sign(l1_doc, old_privateKey, options).first
|
|
@@ -628,6 +646,8 @@ class Oydid
|
|
|
628
646
|
cmsm_sig_rev, cmsm_sig_doc, cmsm_sig_create,
|
|
629
647
|
did_old, cmsm_log_revoke_old), options)
|
|
630
648
|
end
|
|
649
|
+
err = cmsm_verify_signature(l1_doc, cmsm_sig_create, publicKey, "key-doc")
|
|
650
|
+
return [nil, nil, nil, err] if !err.nil?
|
|
631
651
|
l1_sig = cmsm_sig_create
|
|
632
652
|
else
|
|
633
653
|
l1_sig = sign(l1_doc, privateKey, options).first
|
|
@@ -736,7 +756,7 @@ class Oydid
|
|
|
736
756
|
end
|
|
737
757
|
|
|
738
758
|
did_key = did_info["doc"]["key"].to_s
|
|
739
|
-
pubRevKey = did_key.split(":").
|
|
759
|
+
pubRevKey = did_key.split(":")[1].to_s
|
|
740
760
|
subDid = {"doc": did_info["doc"]["doc"], "key": did_key}.to_json
|
|
741
761
|
subDidHash = multi_hash(canonical(subDid), LOG_HASH_OPTIONS).first
|
|
742
762
|
|
|
@@ -776,6 +796,23 @@ class Oydid
|
|
|
776
796
|
# state of a CMSM flow that has to survive between phases. Deliberately only
|
|
777
797
|
# inputs and collected signatures - no derived hashes: every phase recomputes
|
|
778
798
|
# r1 / l2_doc / the DID from these values, so there is nothing to keep in sync.
|
|
799
|
+
# A signature collected in a CMSM phase is only worth something if it was
|
|
800
|
+
# really made with the key the challenge named. Without this check the flow
|
|
801
|
+
# takes anything: the log entries are built from whatever the client sent and
|
|
802
|
+
# only fail on resolution - by which point the DID exists and has claimed the
|
|
803
|
+
# public key, which lets anyone permanently burn a key they do not hold.
|
|
804
|
+
#
|
|
805
|
+
# The message starts with "CMSM " so that the REST layers report it as a
|
|
806
|
+
# client error (400), not as a server fault.
|
|
807
|
+
def self.cmsm_verify_signature(value, signature, public_key, with)
|
|
808
|
+
if public_key.to_s == ""
|
|
809
|
+
return "CMSM cannot verify the " + with.to_s + " signature: no public key in session"
|
|
810
|
+
end
|
|
811
|
+
success, _msg = (verify(value, signature, public_key) rescue [false, ""])
|
|
812
|
+
return nil if success == true
|
|
813
|
+
"CMSM signature for " + with.to_s + " is invalid"
|
|
814
|
+
end
|
|
815
|
+
|
|
779
816
|
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)
|
|
780
817
|
{
|
|
781
818
|
publicKey: publicKey,
|
data/spec/oydid_spec.rb
CHANGED
|
@@ -519,6 +519,65 @@ describe "OYDID handling" do
|
|
|
519
519
|
# end
|
|
520
520
|
# end
|
|
521
521
|
|
|
522
|
+
# regression: the payload of a DID Document is arbitrary JSON, not
|
|
523
|
+
# necessarily an object. A revoked DID whose payload is an Array used to
|
|
524
|
+
# raise NoMethodError in the alsoKnownAs lookup - the repository then
|
|
525
|
+
# answered 500 with an empty body and the resolver reported "not found"
|
|
526
|
+
# instead of "deactivated".
|
|
527
|
+
describe "dag_update with a non-object DID Document payload" do
|
|
528
|
+
def revoked_state(payload)
|
|
529
|
+
{ "did" => "did:oyd:zQmPyjnkL52gQxBBhPuCFkf167MzpCKquqbu5Qt5ia2JGTr",
|
|
530
|
+
"doc" => { "doc" => payload, "key" => "", "log" => "" },
|
|
531
|
+
"log" => [{ "ts" => 1647732475, "op" => 1, "doc" => "zQmcVLwEtzU8By7TTeRFGvVUKm7HhZS6GgRTDFt51QNnans",
|
|
532
|
+
"sig" => "", "previous" => [] }] }
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it "does not raise for an Array payload" do
|
|
536
|
+
expect {
|
|
537
|
+
Oydid.dag_update(revoked_state([5]), { followAlsoKnownAs: true })
|
|
538
|
+
}.not_to raise_error
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
it "does not raise for a scalar payload" do
|
|
542
|
+
expect {
|
|
543
|
+
Oydid.dag_update(revoked_state("plain"), { followAlsoKnownAs: true })
|
|
544
|
+
}.not_to raise_error
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
it "accepts alsoKnownAs written as a set" do
|
|
548
|
+
state = revoked_state({ "alsoKnownAs" => ["did:oyd:zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh"] })
|
|
549
|
+
expect {
|
|
550
|
+
Oydid.dag_update(state, { followAlsoKnownAs: true })
|
|
551
|
+
}.not_to raise_error
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# a broken repository (5xx) has to stay distinguishable from a DID that is
|
|
556
|
+
# not stored there - otherwise the caller reports "not found" and thereby
|
|
557
|
+
# claims the identifier never existed
|
|
558
|
+
describe "failure messages for non-200 responses" do
|
|
559
|
+
it "marks a 5xx as an upstream error" do
|
|
560
|
+
reply = double(code: 500, parsed_response: "")
|
|
561
|
+
msg = Oydid.http_error_message(reply, "https://oydid.ownyourdata.eu/doc/zQmXY")
|
|
562
|
+
expect(Oydid.upstream_error?(msg)).to be true
|
|
563
|
+
expect(msg).to include("500")
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
it "passes the repository's own error through" do
|
|
567
|
+
reply = double(code: 410, parsed_response: { "error" => "revoked" })
|
|
568
|
+
msg = Oydid.http_error_message(reply, "https://oydid.ownyourdata.eu/doc/zQmXY")
|
|
569
|
+
expect(msg).to eq "revoked"
|
|
570
|
+
expect(Oydid.upstream_error?(msg)).to be false
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it "does not mark a 4xx without an error field as upstream error" do
|
|
574
|
+
reply = double(code: 404, parsed_response: nil)
|
|
575
|
+
msg = Oydid.http_error_message(reply, "https://oydid.ownyourdata.eu/doc/zQmXY")
|
|
576
|
+
expect(msg).to start_with("invalid response from")
|
|
577
|
+
expect(Oydid.upstream_error?(msg)).to be false
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
522
581
|
# main functionds
|
|
523
582
|
Dir.glob(File.expand_path("../input/main/*_read.doc", __FILE__)).each do |input|
|
|
524
583
|
it "execute read for #{input.split('/').last}" do
|
|
@@ -807,6 +866,51 @@ describe "OYDID handling" do
|
|
|
807
866
|
# Which identifier a resolved DID document announces as its own. An update
|
|
808
867
|
# mints a new did:oyd, but the identifier a relying party holds is the one it
|
|
809
868
|
# asked for - that is the one the document has to carry.
|
|
869
|
+
# The "key" field is positional: "<public document key>:<public revocation key>".
|
|
870
|
+
# Reading it with .first/.last works only as long as it has exactly two slots -
|
|
871
|
+
# the day a third one is added (e.g. a key agreement key), .last silently
|
|
872
|
+
# returns the wrong key and the error only surfaces at revocation time.
|
|
873
|
+
# Every read must therefore use an explicit index.
|
|
874
|
+
describe "positional access to the key field" do
|
|
875
|
+
repo_root = File.expand_path("../../..", __FILE__)
|
|
876
|
+
sources = %w[
|
|
877
|
+
ruby-gem/lib
|
|
878
|
+
cli
|
|
879
|
+
repository/app
|
|
880
|
+
uni-registrar-driver-did-oyd/app
|
|
881
|
+
].map { |d| File.join(repo_root, d) }.select { |d| File.directory?(d) }
|
|
882
|
+
|
|
883
|
+
ruby_files = sources.flat_map { |d| Dir.glob(File.join(d, "**", "*.rb")) }
|
|
884
|
+
|
|
885
|
+
it "finds sources to scan" do
|
|
886
|
+
skip "running outside the repository checkout" if ruby_files.empty?
|
|
887
|
+
expect(ruby_files).not_to be_empty
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
it "never reads a split key field with .last" do
|
|
891
|
+
skip "running outside the repository checkout" if ruby_files.empty?
|
|
892
|
+
offenders = ruby_files.flat_map do |file|
|
|
893
|
+
File.readlines(file).each_with_index.filter_map do |line, i|
|
|
894
|
+
"#{file.sub(repo_root + "/", "")}:#{i + 1}" if line =~ /split\((["'])\:\1\)\.last/
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
expect(offenders).to eq []
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
it "never reads a split key field with .first" do
|
|
901
|
+
skip "running outside the repository checkout" if ruby_files.empty?
|
|
902
|
+
offenders = ruby_files.flat_map do |file|
|
|
903
|
+
File.readlines(file).each_with_index.filter_map do |line, i|
|
|
904
|
+
next unless line =~ /split\((["'])\:\1\)\.first/
|
|
905
|
+
# the DID identifier itself is also colon-separated and legitimately
|
|
906
|
+
# read with .first - only the key field is at issue here
|
|
907
|
+
"#{file.sub(repo_root + "/", "")}:#{i + 1}" if line.include?('"key"') || line.include?("'key'") || line =~ /_key(s)?\.split/
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
expect(offenders).to eq []
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
810
914
|
describe "document_id" do
|
|
811
915
|
let(:first_did) { "zQmSE1hzumtZ7AoK1qhHf4t5kiKsujMsJSHqoXtWrdd7K7W" }
|
|
812
916
|
let(:second_did) { "zQmfEb3KgYZjZUPLTHPmFPdcV6peF5itB5NmJ9N6gaxxE8K" }
|
|
@@ -858,4 +962,103 @@ describe "OYDID handling" do
|
|
|
858
962
|
end
|
|
859
963
|
end
|
|
860
964
|
|
|
965
|
+
|
|
966
|
+
describe "CMSM signature verification" do
|
|
967
|
+
# An in-memory stand-in for the session store the repository provides, so
|
|
968
|
+
# these examples run without a database and without the network.
|
|
969
|
+
class MemoryCmsmStore
|
|
970
|
+
def initialize; @data = {}; end
|
|
971
|
+
def set(session, payload); @data[session] = payload; end
|
|
972
|
+
def get(session); @data[session]; end
|
|
973
|
+
def delete(session); @data.delete(session); end
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
let(:store) { MemoryCmsmStore.new }
|
|
977
|
+
let(:priv) { Oydid.generate_private_key("", "ed25519-priv", {}).first }
|
|
978
|
+
let(:pub) { Oydid.public_key(priv, {}).first }
|
|
979
|
+
|
|
980
|
+
def cmsm_options(extra = {})
|
|
981
|
+
{ cmsm: true, key_type: "ed25519", cmsm_store: store,
|
|
982
|
+
return_secrets: true, skip_publish: true }.merge(extra)
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
# phase 1: hand over the public key, receive session and value to sign
|
|
986
|
+
def start_flow
|
|
987
|
+
status, msg = Oydid.create({ "key" => pub }, cmsm_options)
|
|
988
|
+
expect(msg).to eq("cmsm")
|
|
989
|
+
status.transform_keys(&:to_s)
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
describe "cmsm_verify_signature" do
|
|
993
|
+
it "accepts a signature made with the named key" do
|
|
994
|
+
signature = Oydid.sign("hello", priv, {}).first
|
|
995
|
+
expect(Oydid.cmsm_verify_signature("hello", signature, pub, "key-doc")).to be_nil
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
it "rejects a signature over a different value" do
|
|
999
|
+
signature = Oydid.sign("hello", priv, {}).first
|
|
1000
|
+
msg = Oydid.cmsm_verify_signature("goodbye", signature, pub, "key-doc")
|
|
1001
|
+
expect(msg).to eq("CMSM signature for key-doc is invalid")
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
it "rejects a signature made with a different key" do
|
|
1005
|
+
other = Oydid.generate_private_key("", "ed25519-priv", {}).first
|
|
1006
|
+
signature = Oydid.sign("hello", other, {}).first
|
|
1007
|
+
expect(Oydid.cmsm_verify_signature("hello", signature, pub, "key-doc")).not_to be_nil
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
it "rejects nonsense instead of raising" do
|
|
1011
|
+
expect(Oydid.cmsm_verify_signature("hello", "not-a-signature", pub, "key-doc")).not_to be_nil
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
it "refuses to verify without a public key" do
|
|
1015
|
+
signature = Oydid.sign("hello", priv, {}).first
|
|
1016
|
+
msg = Oydid.cmsm_verify_signature("hello", signature, "", "key-doc")
|
|
1017
|
+
expect(msg).to include("no public key in session")
|
|
1018
|
+
end
|
|
1019
|
+
|
|
1020
|
+
# the message is what the REST layers match on to answer 400 instead of 500
|
|
1021
|
+
it "phrases failures as client errors" do
|
|
1022
|
+
msg = Oydid.cmsm_verify_signature("hello", "zBogus", pub, "key-rev")
|
|
1023
|
+
expect(msg).to start_with("CMSM ")
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
describe "a running flow" do
|
|
1028
|
+
it "advances when the signature is correct" do
|
|
1029
|
+
phase1 = start_flow
|
|
1030
|
+
signature = Oydid.sign(phase1["sign"], priv, {}).first
|
|
1031
|
+
|
|
1032
|
+
status, msg = Oydid.create({}, cmsm_options(cmsm_session: phase1["session"], sig: signature))
|
|
1033
|
+
|
|
1034
|
+
expect(msg).to eq("cmsm")
|
|
1035
|
+
expect(status.transform_keys(&:to_s)["session"]).to eq(phase1["session"])
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
# Before this check the flow took any signature, built the log entries from
|
|
1039
|
+
# it and answered 200. The DID did not resolve, but it had claimed the
|
|
1040
|
+
# public key - so anyone could burn a key they did not hold.
|
|
1041
|
+
it "refuses a signature the client cannot have made" do
|
|
1042
|
+
phase1 = start_flow
|
|
1043
|
+
forged = Oydid.sign(phase1["sign"],
|
|
1044
|
+
Oydid.generate_private_key("", "ed25519-priv", {}).first, {}).first
|
|
1045
|
+
|
|
1046
|
+
status, msg = Oydid.create({}, cmsm_options(cmsm_session: phase1["session"], sig: forged))
|
|
1047
|
+
|
|
1048
|
+
expect(status).to be_nil
|
|
1049
|
+
expect(msg).to eq("CMSM signature for key-doc is invalid")
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
it "refuses a signature over a value from another flow" do
|
|
1053
|
+
phase1 = start_flow
|
|
1054
|
+
other = start_flow
|
|
1055
|
+
signature = Oydid.sign(other["sign"], priv, {}).first
|
|
1056
|
+
|
|
1057
|
+
status, msg = Oydid.create({}, cmsm_options(cmsm_session: phase1["session"], sig: signature))
|
|
1058
|
+
|
|
1059
|
+
expect(status).to be_nil
|
|
1060
|
+
expect(msg).to start_with("CMSM ")
|
|
1061
|
+
end
|
|
1062
|
+
end
|
|
1063
|
+
end
|
|
861
1064
|
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.9.
|
|
4
|
+
version: 0.9.3
|
|
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-29 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: simple_dag
|