oydid 0.8.2 → 0.9.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: 66decf23cb4e474d0703a8948e20474be619c05083bd8abd5ab8d45857ac4ac0
4
- data.tar.gz: db83f3c7ba0a20c5e4f8fd8ba8b9977b42b9b78fc56ad11df8a8bf12e9657704
3
+ metadata.gz: d3e561caa744ec1b341317d142b27f5b9d7d9dddf92b2d18aaf2117fff05cb9a
4
+ data.tar.gz: 6aa242687b1765148f79e9b6911d22e81339159f2f0c45445c0be1ce804fe0ad
5
5
  SHA512:
6
- metadata.gz: 8856635688f0dd47b246443d7fcc7a994ceef87e7d2594530da0f3489445a544f3c7cc9d20327fec7da3988e83872d1ddf3c854bd4db25b367b72fa917ba09c3
7
- data.tar.gz: '08956cf2da8ed63a35261a7ffa01cb58b11a177c77fe8b5f873f8ed4d59592695c0e80bd72d118c28f4266728b10e420b4eaab7189c406dd6c7731e52f40e38d'
6
+ metadata.gz: 35d735d7f16edf812645b91034b5c8824437144fb86b2dba19e2b165a1912279fc3e6b09916cde37004402bb309f5faad7248105b41de07f293892f7e6963d09
7
+ data.tar.gz: 0e333ede2a2b46e9c6264264f638cf3b54823cab9663a11af3bf776f796965210abb39b3c37555d0824f67d9b55506d3065803b1a94182097acdf0b11b9c670e
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.2
1
+ 0.9.0
data/lib/oydid.rb CHANGED
@@ -68,8 +68,15 @@ class Oydid
68
68
  end
69
69
 
70
70
  # setup
71
+ #
72
+ # did_requested is the identifier the caller asked for. dag_update
73
+ # overwrites "did" with every version it walks through, so by the time a
74
+ # DID document is rendered the requested identifier would be lost - and
75
+ # after an update that is exactly the one a relying party holds, e.g.
76
+ # printed on a product's data carrier.
71
77
  currentDID = {
72
78
  "did": did,
79
+ "did_requested": did,
73
80
  "doc": "",
74
81
  "log": [],
75
82
  "doc_log_id": nil,
@@ -1426,6 +1433,61 @@ class Oydid
1426
1433
  wd
1427
1434
  end
1428
1435
 
1436
+ # Identifiers of every published version of a DID, oldest first.
1437
+ #
1438
+ # A did:oyd is the hash over its own DID document, so every update mints a
1439
+ # new identifier while the earlier ones stay resolvable through the log.
1440
+ # The version reached by traversing the log is the canonical identifier,
1441
+ # every other version identifier is logically equivalent to it - which is
1442
+ # what DID Core expresses as didDocumentMetadata canonicalId/equivalentId.
1443
+ #
1444
+ # This lives in the gem rather than in the resolving controllers because
1445
+ # there are three of them (repository resolve and resolve_full, plus the
1446
+ # uniresolver plugin) and they had drifted into computing this list with
1447
+ # different rules.
1448
+ #
1449
+ # Returns [canonicalId, equivalentIds]; equivalentIds never contains the
1450
+ # identifier the DID document itself carries as `id` (see document_id).
1451
+ def self.version_ids(did_info)
1452
+ canonical = percent_encode(did_info["did"].to_s)
1453
+ if !canonical.start_with?("did:oyd:")
1454
+ canonical = "did:oyd:" + canonical
1455
+ end
1456
+ own = document_id(did_info)
1457
+ equivalentIds = []
1458
+ did_info["log"].each do |log|
1459
+ if log["op"] == 2 || log["op"] == 3
1460
+ eid = percent_encode("did:oyd:" + log["doc"].to_s)
1461
+ if eid != own
1462
+ equivalentIds << eid
1463
+ end
1464
+ end
1465
+ end unless did_info["log"].nil?
1466
+ [canonical, equivalentIds]
1467
+ end
1468
+
1469
+ # The identifier a resolved DID document carries as `id`.
1470
+ #
1471
+ # A did:oyd is the hash over its own document, so an update mints a new one
1472
+ # and the resolution walks the log to the newest version. Answering with that
1473
+ # newest identifier means a relying party never sees the DID it asked for -
1474
+ # the one on the data carrier, in the credential, in the database. So the
1475
+ # requested identifier is what goes into the document; which version it is
1476
+ # remains readable from didDocumentMetadata canonicalId.
1477
+ #
1478
+ # Callers that build a did_info by hand - the registrar endpoints do, for a
1479
+ # DID that was just created or updated - carry no did_requested and keep
1480
+ # getting the identifier they passed in.
1481
+ def self.document_id(did_info)
1482
+ did = did_info["did_requested"].to_s
1483
+ did = did_info["did"].to_s if did == ""
1484
+ did = percent_encode(did)
1485
+ if !did.start_with?("did:oyd:")
1486
+ did = "did:oyd:" + did
1487
+ end
1488
+ did
1489
+ end
1490
+
1429
1491
  def self.w3c(did_info, options)
1430
1492
  # check if doc is already W3C DID
1431
1493
  is_already_w3c_did = (did_info.transform_keys(&:to_s)["doc"]["doc"].has_key?("@context") &&
@@ -1434,10 +1496,7 @@ class Oydid
1434
1496
  if is_already_w3c_did
1435
1497
  return did_info.transform_keys(&:to_s)["doc"]["doc"]
1436
1498
  end
1437
- did = percent_encode(did_info["did"])
1438
- if !did.start_with?("did:oyd:")
1439
- did = "did:oyd:" + did
1440
- end
1499
+ did = document_id(did_info)
1441
1500
 
1442
1501
  didDoc = did_info.dup.transform_keys(&:to_s)["doc"]
1443
1502
  pubDocKey = didDoc["key"].split(":")[0] rescue ""
@@ -1568,15 +1627,13 @@ class Oydid
1568
1627
  end
1569
1628
  end
1570
1629
 
1571
- equivalentIds = []
1572
- did_info["log"].each do |log|
1573
- if log["op"] == 2 || log["op"] == 3
1574
- eid = percent_encode("did:oyd:" + log["doc"])
1575
- if eid != did
1576
- equivalentIds << eid
1577
- end
1578
- end
1579
- end unless did_info["log"].nil?
1630
+ # alsoKnownAs is a statement about the DID *subject*, and DID Core calls
1631
+ # it best practice not to read it as equivalence unless the relationship
1632
+ # is reciprocated - which it cannot be here, because all versions resolve
1633
+ # to the same document. It stays for backwards compatibility; the
1634
+ # authoritative statement is didDocumentMetadata canonicalId/equivalentId,
1635
+ # built from the same list.
1636
+ equivalentIds = version_ids(did_info).last
1580
1637
  if equivalentIds.length > 0
1581
1638
  wd["alsoKnownAs"] = equivalentIds
1582
1639
  end
@@ -1 +1 @@
1
- [{"did":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","doc":{"doc":{"simple":"example"},"key":"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB","log":"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe"},"log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}],"doc_log_id":0,"termination_log_id":1,"error":0,"message":"","verification":"identifier: zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh\n✅ is hash of DID Document:\n{\n \"doc\": {\n \"simple\": \"example\"\n },\n \"key\": \"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB\",\n \"log\": \"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\"\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\n'log' reference in DID Document: zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\n✅ is hash of TERMINATE log record:\n{\n \"ts\": 1641224736,\n \"op\": 0,\n \"doc\": \"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\",\n \"sig\": \"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU\",\n \"previous\": []\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\nRevocation reference in log record: zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\n✅ cannot find revocation record searching at\n- https://oydid.ownyourdata.eu\n(Details: https://ownyourdata.github.io/oydid/#retrieve_log)\n\n","full_log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}]},""]
1
+ [{"did":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","did_requested":"did:oyd:zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","doc":{"doc":{"simple":"example"},"key":"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB","log":"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe"},"log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}],"doc_log_id":0,"termination_log_id":1,"error":0,"message":"","verification":"identifier: zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh\n✅ is hash of DID Document:\n{\n \"doc\": {\n \"simple\": \"example\"\n },\n \"key\": \"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB\",\n \"log\": \"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\"\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\n'log' reference in DID Document: zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\n✅ is hash of TERMINATE log record:\n{\n \"ts\": 1641224736,\n \"op\": 0,\n \"doc\": \"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\",\n \"sig\": \"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU\",\n \"previous\": []\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\nRevocation reference in log record: zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\n✅ cannot find revocation record searching at\n- https://oydid.ownyourdata.eu\n(Details: https://ownyourdata.github.io/oydid/#retrieve_log)\n\n","full_log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}]},""]
data/spec/oydid_spec.rb CHANGED
@@ -584,4 +584,139 @@ describe "OYDID handling" do
584
584
  end
585
585
  end
586
586
 
587
+ # Identifiers of the published versions of a DID. Every update mints a new
588
+ # one, so a resolver has to be able to say which is current and which are
589
+ # earlier - didDocumentMetadata canonicalId and equivalentId.
590
+ describe "version_ids" do
591
+ let(:first_did) { "zQmSE1hzumtZ7AoK1qhHf4t5kiKsujMsJSHqoXtWrdd7K7W" }
592
+ let(:second_did) { "zQmfEb3KgYZjZUPLTHPmFPdcV6peF5itB5NmJ9N6gaxxE8K" }
593
+
594
+ it "reports a freshly created DID as canonical without equivalents" do
595
+ canonical, equivalent = Oydid.version_ids(
596
+ "did" => "did:oyd:" + first_did,
597
+ "log" => [{ "op" => 2, "doc" => first_did },
598
+ { "op" => 0, "doc" => "terminate" }])
599
+ expect(canonical).to eq "did:oyd:" + first_did
600
+ expect(equivalent).to eq []
601
+ end
602
+
603
+ it "names the current version canonical and the previous one equivalent" do
604
+ canonical, equivalent = Oydid.version_ids(
605
+ "did" => "did:oyd:" + second_did,
606
+ "log" => [{ "op" => 1, "doc" => "revoke" },
607
+ { "op" => 2, "doc" => first_did },
608
+ { "op" => 3, "doc" => second_did },
609
+ { "op" => 0, "doc" => "terminate" }])
610
+ expect(canonical).to eq "did:oyd:" + second_did
611
+ expect(equivalent).to eq ["did:oyd:" + first_did]
612
+ end
613
+
614
+ it "keeps every earlier version, oldest first" do
615
+ canonical, equivalent = Oydid.version_ids(
616
+ "did" => "did:oyd:zC",
617
+ "log" => [{ "op" => 2, "doc" => "zA" },
618
+ { "op" => 3, "doc" => "zB" },
619
+ { "op" => 3, "doc" => "zC" }])
620
+ expect(canonical).to eq "did:oyd:zC"
621
+ expect(equivalent).to eq ["did:oyd:zA", "did:oyd:zB"]
622
+ end
623
+
624
+ it "adds the method prefix when the resolved DID carries none" do
625
+ canonical, = Oydid.version_ids("did" => second_did, "log" => [])
626
+ expect(canonical).to eq "did:oyd:" + second_did
627
+ end
628
+
629
+ it "percent-encodes a location suffix" do
630
+ canonical, equivalent = Oydid.version_ids(
631
+ "did" => "did:oyd:" + second_did + "@https://example.org",
632
+ "log" => [{ "op" => 2, "doc" => first_did + "@https://example.org" },
633
+ { "op" => 3, "doc" => second_did + "@https://example.org" }])
634
+ expect(canonical).to eq "did:oyd:" + second_did + "%40example.org"
635
+ expect(equivalent).to eq ["did:oyd:" + first_did + "%40example.org"]
636
+ end
637
+
638
+ it "tolerates a missing log" do
639
+ canonical, equivalent = Oydid.version_ids("did" => "did:oyd:" + second_did)
640
+ expect(canonical).to eq "did:oyd:" + second_did
641
+ expect(equivalent).to eq []
642
+ end
643
+
644
+ # w3c builds alsoKnownAs from the same list. The two used to be computed
645
+ # separately in three places and drifted apart; this guards the merge.
646
+ #
647
+ # The canonical DID has to be one this suite stubs: w3c looks up delegation
648
+ # keys for it, and WebMock::NetConnectNotAllowedError descends from Exception
649
+ # rather than StandardError, so the inline `rescue []` around
650
+ # getDelegatedPubKeysFromDID does not catch it. Only the canonical DID is
651
+ # fetched - log entries are read, not resolved.
652
+ it "agrees with the alsoKnownAs the DID document carries" do
653
+ stubbed_did = "zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh"
654
+ did_info = {
655
+ "did" => "did:oyd:" + stubbed_did,
656
+ "doc" => { "doc" => { "hello" => "world" },
657
+ "key" => "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S:" \
658
+ "z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK" },
659
+ "log" => [{ "op" => 2, "doc" => first_did },
660
+ { "op" => 3, "doc" => stubbed_did }]
661
+ }
662
+ wd = Oydid.w3c(Marshal.load(Marshal.dump(did_info)), {})
663
+ expect(wd["alsoKnownAs"]).to eq ["did:oyd:" + first_did]
664
+ expect(wd["alsoKnownAs"]).to eq Oydid.version_ids(did_info).last
665
+ end
666
+ end
667
+
668
+ # Which identifier a resolved DID document announces as its own. An update
669
+ # mints a new did:oyd, but the identifier a relying party holds is the one it
670
+ # asked for - that is the one the document has to carry.
671
+ describe "document_id" do
672
+ let(:first_did) { "zQmSE1hzumtZ7AoK1qhHf4t5kiKsujMsJSHqoXtWrdd7K7W" }
673
+ let(:second_did) { "zQmfEb3KgYZjZUPLTHPmFPdcV6peF5itB5NmJ9N6gaxxE8K" }
674
+ # a DID this suite stubs - w3c looks up delegation keys for whatever it
675
+ # emits as id, see the note on the alsoKnownAs example above
676
+ let(:stubbed_did) { "zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh" }
677
+
678
+ it "answers with the requested DID when the resolver kept it" do
679
+ expect(Oydid.document_id("did" => "did:oyd:" + second_did,
680
+ "did_requested" => "did:oyd:" + first_did))
681
+ .to eq "did:oyd:" + first_did
682
+ end
683
+
684
+ it "falls back to the resolved DID for a did_info built by hand" do
685
+ expect(Oydid.document_id("did" => "did:oyd:" + second_did))
686
+ .to eq "did:oyd:" + second_did
687
+ end
688
+
689
+ it "adds the method prefix and percent-encodes a location suffix" do
690
+ expect(Oydid.document_id("did" => "", "did_requested" => first_did + "@https://example.org"))
691
+ .to eq "did:oyd:" + first_did + "%40example.org"
692
+ end
693
+
694
+ # the point of the exercise: a DID printed on a data carrier keeps naming
695
+ # itself after an update, while the current version stays readable
696
+ it "keeps the requested DID as id and moves the current one to alsoKnownAs" do
697
+ did_info = {
698
+ "did" => "did:oyd:" + second_did,
699
+ "did_requested" => "did:oyd:" + stubbed_did,
700
+ "doc" => { "doc" => { "hello" => "world" },
701
+ "key" => "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S:" \
702
+ "z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK" },
703
+ "log" => [{ "op" => 2, "doc" => stubbed_did },
704
+ { "op" => 3, "doc" => second_did }]
705
+ }
706
+ wd = Oydid.w3c(Marshal.load(Marshal.dump(did_info)), {})
707
+ expect(wd["id"]).to eq "did:oyd:" + stubbed_did
708
+ expect(wd["alsoKnownAs"]).to eq ["did:oyd:" + second_did]
709
+ # the entries of verificationMethod are built as hash literals with
710
+ # "id": - Ruby makes that a symbol key, the surrounding keys are strings
711
+ expect(wd["verificationMethod"].first[:id]).to start_with("did:oyd:" + stubbed_did + "#")
712
+ expect(wd["verificationMethod"].first[:controller]).to eq "did:oyd:" + stubbed_did
713
+
714
+ # canonicalId still names the version the log resolves to, and the
715
+ # equivalent list is everything the document does not call itself
716
+ canonical, equivalent = Oydid.version_ids(did_info)
717
+ expect(canonical).to eq "did:oyd:" + second_did
718
+ expect(equivalent).to eq ["did:oyd:" + second_did]
719
+ end
720
+ end
721
+
587
722
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oydid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christoph Fabianek