oydid 0.9.3 → 0.9.4
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/log.rb +17 -7
- data/spec/oydid_spec.rb +94 -0
- 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: 0f96270f4b2465c3dadb499841094e20c27c1e6512e18f69c2e2f7dbb2c6559f
|
|
4
|
+
data.tar.gz: 0aff913041f3105b4569c41c88918439b50754431e24c85e51197f851fd436d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2505aeb0d16fc4a7786b24bbd1dcd607e7ac71122738ddc0bff9c6880d813813c5e6f2c134977a4bbf92b765565b8899822ae5d66dfe1977f214b89960ed6733
|
|
7
|
+
data.tar.gz: 49b7a640acf6793d33a504b45c4f69b30767a86d95fe4f7fd3236c26e4494456ccf4e799effe12c856b85a2ab675fa9887664c934bdcdd93710381466af27145
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.4
|
data/lib/oydid/log.rb
CHANGED
|
@@ -127,7 +127,7 @@ class Oydid
|
|
|
127
127
|
if !position.nil?
|
|
128
128
|
dag.add_edge from: dag_log[position], to: dag_log[i]
|
|
129
129
|
end
|
|
130
|
-
end unless el["previous"] == []
|
|
130
|
+
end unless el["previous"].nil? || el["previous"] == []
|
|
131
131
|
i += 1
|
|
132
132
|
end unless logs.nil?
|
|
133
133
|
|
|
@@ -207,7 +207,7 @@ class Oydid
|
|
|
207
207
|
dag.add_edge from: dag_log[position], to: dag_log[i]
|
|
208
208
|
end
|
|
209
209
|
end
|
|
210
|
-
end unless el["previous"] == []
|
|
210
|
+
end unless el["previous"].nil? || el["previous"] == []
|
|
211
211
|
i += 1
|
|
212
212
|
end unless logs.nil?
|
|
213
213
|
|
|
@@ -467,10 +467,20 @@ class Oydid
|
|
|
467
467
|
message = log_el["doc"].to_s
|
|
468
468
|
signature = log_el["sig"]
|
|
469
469
|
# public_key = current_public_doc_key.to_s
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
#
|
|
473
|
-
|
|
470
|
+
# SECURITY (fail-closed on delegation): an UPDATE
|
|
471
|
+
# is authorised only by the document key of the
|
|
472
|
+
# version it supersedes - the key continuity that
|
|
473
|
+
# anchors the chain. Delegated keys (op=5) are NOT
|
|
474
|
+
# honoured at resolution: they were collected from
|
|
475
|
+
# the raw log with no authentication whatsoever, so
|
|
476
|
+
# honouring them let anyone able to write a log
|
|
477
|
+
# entry take over a revoked DID by injecting a
|
|
478
|
+
# DELEGATE plus a self-signed UPDATE. No production
|
|
479
|
+
# DID relies on a delegated key to resolve
|
|
480
|
+
# (audit 2026-09-01), so this closes the hole at
|
|
481
|
+
# zero cost.
|
|
482
|
+
pubKeys = [current_public_doc_key.to_s]
|
|
483
|
+
msg = ""
|
|
474
484
|
signature_verification = false
|
|
475
485
|
used_pubkey = ""
|
|
476
486
|
pubKeys.each do |key|
|
|
@@ -515,7 +525,7 @@ class Oydid
|
|
|
515
525
|
new_did_hash = new_doc_did.delete_prefix("did:oyd:")
|
|
516
526
|
new_did_hash = new_did_hash.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first
|
|
517
527
|
new_did10 = new_did_hash[0,10]
|
|
518
|
-
new_doc = retrieve_document(new_doc_did, new_did10 + ".doc", new_doc_location,
|
|
528
|
+
new_doc = retrieve_document(new_doc_did, new_did10 + ".doc", new_doc_location, options).first
|
|
519
529
|
currentDID["verification"] += "found UPDATE log record:" + "\n"
|
|
520
530
|
currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
|
|
521
531
|
currentDID["verification"] += "⛔ none of available public keys (" + pubKeys.join(", ") + ")\n"
|
data/spec/oydid_spec.rb
CHANGED
|
@@ -552,6 +552,100 @@ describe "OYDID handling" do
|
|
|
552
552
|
end
|
|
553
553
|
end
|
|
554
554
|
|
|
555
|
+
# regression: a log entry with a missing "previous" field (e.g. junk op=5
|
|
556
|
+
# entries POSTed to the unauthenticated /log endpoint) made dag_did call
|
|
557
|
+
# nil.each - the repository then answered 500 with an empty body for every
|
|
558
|
+
# request to that DID. previous is optional; a missing one means no
|
|
559
|
+
# back-reference, exactly like an empty array.
|
|
560
|
+
describe "dag_did with a log entry missing 'previous'" do
|
|
561
|
+
let(:log) do
|
|
562
|
+
[ { "ts" => 0, "op" => 5, "doc" => "doc:123", "sig" => "z6M234" }, # no "previous"
|
|
563
|
+
{ "ts" => 1, "op" => 2, "doc" => "zQmSVzALxVDs2Tqf5gh9XDWSL4L57YLi8H1XpvpizQPd79c", "sig" => "z", "previous" => [] },
|
|
564
|
+
{ "ts" => 1, "op" => 0, "doc" => "zQmXRVzdjMnTz1WQVkVdJU1ZF9DPjcK5LM2GynpGzxbNAz6", "sig" => "z", "previous" => [] } ]
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
it "does not raise on a missing previous field" do
|
|
568
|
+
expect { Oydid.dag_did(log, { silent: true }) }.not_to raise_error
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
it "still resolves the DAG and finds the single CREATE entry" do
|
|
572
|
+
dag, create_index, _terminate_index, msg = Oydid.dag_did(log, { silent: true })
|
|
573
|
+
expect(msg).to eq ""
|
|
574
|
+
expect(dag).not_to be_nil
|
|
575
|
+
expect(create_index).to eq 1
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# SECURITY regression (fail-closed on delegation): a DELEGATE entry (op=5) is
|
|
580
|
+
# collected from the raw log with no authentication. Honouring it at resolution
|
|
581
|
+
# let anyone able to write a log entry take over a revoked DID by injecting a
|
|
582
|
+
# DELEGATE plus a self-signed UPDATE. Resolution must authorise an UPDATE only
|
|
583
|
+
# by the superseded version's own document key - and must keep resolving a
|
|
584
|
+
# legitimate owner-signed update.
|
|
585
|
+
describe "DELEGATE keys are not honoured at resolution" do
|
|
586
|
+
HOPTS = { digest: "sha2-256", encode: "base58btc" }
|
|
587
|
+
class MemStore
|
|
588
|
+
def initialize(h); @h = h; end
|
|
589
|
+
def get(k); @h[k]; end
|
|
590
|
+
end
|
|
591
|
+
def h(e); Oydid.multi_hash(Oydid.canonical(e.slice("ts","op","doc","sig","previous")), HOPTS).first; end
|
|
592
|
+
def sub(e); Oydid.multi_hash(Oydid.canonical(e.slice("ts","op","doc","sig")), HOPTS).first; end
|
|
593
|
+
|
|
594
|
+
# generate_base builds the CREATE/TERMINATE/REVOKE records and the keys
|
|
595
|
+
# without the network round-trip that Oydid.create makes via w3c (which
|
|
596
|
+
# WebMock blocks in the test env). doc_location "local" keeps the log
|
|
597
|
+
# reference free of an @location suffix.
|
|
598
|
+
def build_revoked_victim
|
|
599
|
+
dd, dk, dl, = Oydid.generate_base({ "purpose" => "victim" }, nil, "create",
|
|
600
|
+
{ key_type: "ed25519", doc_location: "local" })
|
|
601
|
+
create = dl[:l1]
|
|
602
|
+
term1 = dl[:l2]
|
|
603
|
+
revoke = dl[:r1].merge("previous" => [h(create), h(term1)])
|
|
604
|
+
{ vdid: dd[:did].delete_prefix("did:oyd:"), vdoc: dd[:didDocument], vpriv: dk[:privateKey],
|
|
605
|
+
create: create, term1: term1, revoke: revoke }
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def build_v2
|
|
609
|
+
np = { "service" => [{ "id" => "#x", "serviceEndpoint" => "https://new.example" }] }
|
|
610
|
+
apriv = Oydid.generate_private_key("", "ed25519-priv", {}).first
|
|
611
|
+
apub = Oydid.public_key(apriv, {}).first
|
|
612
|
+
arpub = Oydid.public_key(Oydid.generate_private_key("", "ed25519-priv", {}).first, {}).first
|
|
613
|
+
nkey = "#{apub}:#{arpub}"
|
|
614
|
+
r2 = { "ts" => 3, "op" => 1, "sig" => "z",
|
|
615
|
+
"doc" => Oydid.multi_hash(Oydid.canonical({ "doc" => np, "key" => nkey }.to_json), HOPTS).first }
|
|
616
|
+
term2 = { "ts" => 3, "op" => 0, "doc" => sub(r2), "sig" => Oydid.sign(sub(r2), apriv, HOPTS).first, "previous" => [] }
|
|
617
|
+
ndoc = { "doc" => np, "key" => nkey, "log" => h(term2) }
|
|
618
|
+
{ np: np, apriv: apriv, apub: apub, term2: term2, ndoc: ndoc,
|
|
619
|
+
ndid: Oydid.multi_hash(Oydid.canonical(ndoc.to_json), HOPTS).first }
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def resolve(vic, v2, log)
|
|
623
|
+
store = MemStore.new(vic[:vdid] => { "doc" => vic[:vdoc], "log" => log },
|
|
624
|
+
v2[:ndid] => { "doc" => v2[:ndoc], "log" => log })
|
|
625
|
+
r, = Oydid.read("did:oyd:#{vic[:vdid]}",
|
|
626
|
+
{ local_doc: vic[:vdoc], local_log: log, local_store: store, silent: true })
|
|
627
|
+
[r["error"].to_i, r.dig("doc", "doc")]
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
it "rejects a takeover via an injected DELEGATE + self-signed UPDATE" do
|
|
631
|
+
vic = build_revoked_victim; v2 = build_v2
|
|
632
|
+
deleg = { "ts" => 2, "op" => 5, "doc" => "doc:#{v2[:apub]}", "sig" => "", "previous" => [h(vic[:term1])] }
|
|
633
|
+
upd = { "ts" => 3, "op" => 3, "doc" => v2[:ndid],
|
|
634
|
+
"sig" => Oydid.sign(v2[:ndid], v2[:apriv], HOPTS).first, "previous" => [h(vic[:revoke])] }
|
|
635
|
+
_err, doc = resolve(vic, v2, [vic[:create], vic[:term1], vic[:revoke], deleg, upd, v2[:term2]])
|
|
636
|
+
expect(doc).not_to eq v2[:np]
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
it "still resolves a legitimate owner-signed UPDATE" do
|
|
640
|
+
vic = build_revoked_victim; v2 = build_v2
|
|
641
|
+
upd = { "ts" => 3, "op" => 3, "doc" => v2[:ndid],
|
|
642
|
+
"sig" => Oydid.sign(v2[:ndid], vic[:vpriv], HOPTS).first, "previous" => [h(vic[:revoke])] }
|
|
643
|
+
err, doc = resolve(vic, v2, [vic[:create], vic[:term1], vic[:revoke], upd, v2[:term2]])
|
|
644
|
+
expect(err).to eq 0
|
|
645
|
+
expect(doc).to eq v2[:np]
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
555
649
|
# a broken repository (5xx) has to stay distinguishable from a DID that is
|
|
556
650
|
# not stored there - otherwise the caller reports "not found" and thereby
|
|
557
651
|
# claims the identifier never existed
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Christoph Fabianek
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-09-01 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.15
|
|
437
437
|
specification_version: 4
|
|
438
438
|
summary: Own Your Decentralized Identifier for Ruby.
|
|
439
439
|
test_files:
|