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 +4 -4
- data/VERSION +1 -1
- data/lib/oydid/basic.rb +3 -3
- data/lib/oydid/log.rb +2 -2
- data/lib/oydid.rb +40 -3
- data/spec/oydid_spec.rb +144 -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: 072b6ccce2a1b7bf3c607aa01d3a3c97cfd37f8e41d52bb9db529d539ccb6eda
|
|
4
|
+
data.tar.gz: 75ccf1a0357d60e629dac7ea3a046cfe0933693a04b5179bf0db7b54929abc6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c936a18cf7106ae73e76fb30fc5ee789a50330a342dfa9624f29bd03574cde42b9c8a4b6a710e0f7e3ed9c6865fb8ec49aaac2cf7cd24c5d5330405e4f20cd81
|
|
7
|
+
data.tar.gz: 3d6c15c81d795cd470f75031722f211aac478dc239a1ece245a64e1b00144d7d37eadfe3c5db8dab4ebfd487e685011f50615f5c40d05ea502d59762cbb51a1f
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
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(":")
|
|
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
|
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(":")
|
|
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(":")
|
|
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(":").
|
|
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
|
@@ -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.
|
|
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-
|
|
10
|
+
date: 2026-08-28 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: simple_dag
|