oydid 0.9.1 → 0.9.2

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: 9c74b5cd9e592efe1514c9110cff25a131ddc0a45727e035d48656bc0c20d2f8
4
- data.tar.gz: bd936eb03e018e15ae334907c7087629f48f17c2cc54bdeebb8a81542e2f2af9
3
+ metadata.gz: 072b6ccce2a1b7bf3c607aa01d3a3c97cfd37f8e41d52bb9db529d539ccb6eda
4
+ data.tar.gz: 75ccf1a0357d60e629dac7ea3a046cfe0933693a04b5179bf0db7b54929abc6b
5
5
  SHA512:
6
- metadata.gz: 8fddcdf2ce2328ba9329263b1de2826cec3c3d4947c7cd5e599972798cffb10887e8bf2205d208961494a7296a818c90a4fea679f2b9b1c01109645c8fde59bf
7
- data.tar.gz: fdaab3ac52a14b4313db2d7d52058a41bdd6b72b2ca580441f5620a246d73d2fdf753a0530aef76d44520d8347573852aa6e3ae27d173371d54b09eeb86c874c
6
+ metadata.gz: c936a18cf7106ae73e76fb30fc5ee789a50330a342dfa9624f29bd03574cde42b9c8a4b6a710e0f7e3ed9c6865fb8ec49aaac2cf7cd24c5d5330405e4f20cd81
7
+ data.tar.gz: 3d6c15c81d795cd470f75031722f211aac478dc239a1ece245a64e1b00144d7d37eadfe3c5db8dab4ebfd487e685011f50615f5c40d05ea502d59762cbb51a1f
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.1
1
+ 0.9.2
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(":").first rescue nil
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(":").first] rescue nil
370
+ keys = [did_document["doc"]["key"].split(":")[0]] rescue nil
371
371
  when "rev"
372
- keys = [did_document["doc"]["key"].split(":").last] rescue nil
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
data/lib/oydid/log.rb CHANGED
@@ -361,7 +361,7 @@ class Oydid
361
361
  currentDID["verification"] += JSON.pretty_generate(doc) + "\n"
362
362
  currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
363
363
  end
364
- current_public_doc_key = currentDID["doc"]["key"].split(":").first rescue ""
364
+ current_public_doc_key = currentDID["doc"]["key"].split(":")[0] rescue ""
365
365
 
366
366
  when 0 # TERMINATE
367
367
  currentDID["termination_log_id"] = i
@@ -506,7 +506,7 @@ class Oydid
506
506
  return currentDID
507
507
  end
508
508
  next_doc = next_doc.first["doc"]
509
- if pubKeys.include?(next_doc["key"].split(":").first)
509
+ if pubKeys.include?(next_doc["key"].split(":")[0])
510
510
  currentDID["verification"] += "⚠️ no key rotation in updated DID Document" + "\n"
511
511
  end
512
512
  currentDID["verification"] += "\n"
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(":").first.to_s
387
- old_publicRevKey = old_did_key.split(":").last.to_s
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(":").last.to_s
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
@@ -807,6 +807,51 @@ describe "OYDID handling" do
807
807
  # Which identifier a resolved DID document announces as its own. An update
808
808
  # mints a new did:oyd, but the identifier a relying party holds is the one it
809
809
  # asked for - that is the one the document has to carry.
810
+ # The "key" field is positional: "<public document key>:<public revocation key>".
811
+ # Reading it with .first/.last works only as long as it has exactly two slots -
812
+ # the day a third one is added (e.g. a key agreement key), .last silently
813
+ # returns the wrong key and the error only surfaces at revocation time.
814
+ # Every read must therefore use an explicit index.
815
+ describe "positional access to the key field" do
816
+ repo_root = File.expand_path("../../..", __FILE__)
817
+ sources = %w[
818
+ ruby-gem/lib
819
+ cli
820
+ repository/app
821
+ uni-registrar-driver-did-oyd/app
822
+ ].map { |d| File.join(repo_root, d) }.select { |d| File.directory?(d) }
823
+
824
+ ruby_files = sources.flat_map { |d| Dir.glob(File.join(d, "**", "*.rb")) }
825
+
826
+ it "finds sources to scan" do
827
+ skip "running outside the repository checkout" if ruby_files.empty?
828
+ expect(ruby_files).not_to be_empty
829
+ end
830
+
831
+ it "never reads a split key field with .last" do
832
+ skip "running outside the repository checkout" if ruby_files.empty?
833
+ offenders = ruby_files.flat_map do |file|
834
+ File.readlines(file).each_with_index.filter_map do |line, i|
835
+ "#{file.sub(repo_root + "/", "")}:#{i + 1}" if line =~ /split\((["'])\:\1\)\.last/
836
+ end
837
+ end
838
+ expect(offenders).to eq []
839
+ end
840
+
841
+ it "never reads a split key field with .first" do
842
+ skip "running outside the repository checkout" if ruby_files.empty?
843
+ offenders = ruby_files.flat_map do |file|
844
+ File.readlines(file).each_with_index.filter_map do |line, i|
845
+ next unless line =~ /split\((["'])\:\1\)\.first/
846
+ # the DID identifier itself is also colon-separated and legitimately
847
+ # read with .first - only the key field is at issue here
848
+ "#{file.sub(repo_root + "/", "")}:#{i + 1}" if line.include?('"key"') || line.include?("'key'") || line =~ /_key(s)?\.split/
849
+ end
850
+ end
851
+ expect(offenders).to eq []
852
+ end
853
+ end
854
+
810
855
  describe "document_id" do
811
856
  let(:first_did) { "zQmSE1hzumtZ7AoK1qhHf4t5kiKsujMsJSHqoXtWrdd7K7W" }
812
857
  let(:second_did) { "zQmfEb3KgYZjZUPLTHPmFPdcV6peF5itB5NmJ9N6gaxxE8K" }
@@ -858,4 +903,103 @@ describe "OYDID handling" do
858
903
  end
859
904
  end
860
905
 
906
+
907
+ describe "CMSM signature verification" do
908
+ # An in-memory stand-in for the session store the repository provides, so
909
+ # these examples run without a database and without the network.
910
+ class MemoryCmsmStore
911
+ def initialize; @data = {}; end
912
+ def set(session, payload); @data[session] = payload; end
913
+ def get(session); @data[session]; end
914
+ def delete(session); @data.delete(session); end
915
+ end
916
+
917
+ let(:store) { MemoryCmsmStore.new }
918
+ let(:priv) { Oydid.generate_private_key("", "ed25519-priv", {}).first }
919
+ let(:pub) { Oydid.public_key(priv, {}).first }
920
+
921
+ def cmsm_options(extra = {})
922
+ { cmsm: true, key_type: "ed25519", cmsm_store: store,
923
+ return_secrets: true, skip_publish: true }.merge(extra)
924
+ end
925
+
926
+ # phase 1: hand over the public key, receive session and value to sign
927
+ def start_flow
928
+ status, msg = Oydid.create({ "key" => pub }, cmsm_options)
929
+ expect(msg).to eq("cmsm")
930
+ status.transform_keys(&:to_s)
931
+ end
932
+
933
+ describe "cmsm_verify_signature" do
934
+ it "accepts a signature made with the named key" do
935
+ signature = Oydid.sign("hello", priv, {}).first
936
+ expect(Oydid.cmsm_verify_signature("hello", signature, pub, "key-doc")).to be_nil
937
+ end
938
+
939
+ it "rejects a signature over a different value" do
940
+ signature = Oydid.sign("hello", priv, {}).first
941
+ msg = Oydid.cmsm_verify_signature("goodbye", signature, pub, "key-doc")
942
+ expect(msg).to eq("CMSM signature for key-doc is invalid")
943
+ end
944
+
945
+ it "rejects a signature made with a different key" do
946
+ other = Oydid.generate_private_key("", "ed25519-priv", {}).first
947
+ signature = Oydid.sign("hello", other, {}).first
948
+ expect(Oydid.cmsm_verify_signature("hello", signature, pub, "key-doc")).not_to be_nil
949
+ end
950
+
951
+ it "rejects nonsense instead of raising" do
952
+ expect(Oydid.cmsm_verify_signature("hello", "not-a-signature", pub, "key-doc")).not_to be_nil
953
+ end
954
+
955
+ it "refuses to verify without a public key" do
956
+ signature = Oydid.sign("hello", priv, {}).first
957
+ msg = Oydid.cmsm_verify_signature("hello", signature, "", "key-doc")
958
+ expect(msg).to include("no public key in session")
959
+ end
960
+
961
+ # the message is what the REST layers match on to answer 400 instead of 500
962
+ it "phrases failures as client errors" do
963
+ msg = Oydid.cmsm_verify_signature("hello", "zBogus", pub, "key-rev")
964
+ expect(msg).to start_with("CMSM ")
965
+ end
966
+ end
967
+
968
+ describe "a running flow" do
969
+ it "advances when the signature is correct" do
970
+ phase1 = start_flow
971
+ signature = Oydid.sign(phase1["sign"], priv, {}).first
972
+
973
+ status, msg = Oydid.create({}, cmsm_options(cmsm_session: phase1["session"], sig: signature))
974
+
975
+ expect(msg).to eq("cmsm")
976
+ expect(status.transform_keys(&:to_s)["session"]).to eq(phase1["session"])
977
+ end
978
+
979
+ # Before this check the flow took any signature, built the log entries from
980
+ # it and answered 200. The DID did not resolve, but it had claimed the
981
+ # public key - so anyone could burn a key they did not hold.
982
+ it "refuses a signature the client cannot have made" do
983
+ phase1 = start_flow
984
+ forged = Oydid.sign(phase1["sign"],
985
+ Oydid.generate_private_key("", "ed25519-priv", {}).first, {}).first
986
+
987
+ status, msg = Oydid.create({}, cmsm_options(cmsm_session: phase1["session"], sig: forged))
988
+
989
+ expect(status).to be_nil
990
+ expect(msg).to eq("CMSM signature for key-doc is invalid")
991
+ end
992
+
993
+ it "refuses a signature over a value from another flow" do
994
+ phase1 = start_flow
995
+ other = start_flow
996
+ signature = Oydid.sign(other["sign"], priv, {}).first
997
+
998
+ status, msg = Oydid.create({}, cmsm_options(cmsm_session: phase1["session"], sig: signature))
999
+
1000
+ expect(status).to be_nil
1001
+ expect(msg).to start_with("CMSM ")
1002
+ end
1003
+ end
1004
+ end
861
1005
  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.1
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christoph Fabianek
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-08-26 00:00:00.000000000 Z
10
+ date: 2026-08-28 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: simple_dag