oydid 0.6.4 → 0.6.6

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: e048d33455d4b2b305d1905b1acea54c0f87f7a871998fa5a294b0669a245112
4
- data.tar.gz: 0ff2fabe31fe7352062e93ef8bce6c74dfa6afd7004db0f53ccfb13912b7e9d5
3
+ metadata.gz: 3e47064181aee5338fa3fde2af77f2955cbcb6e5a4b19f8586882183056f1070
4
+ data.tar.gz: d601a5b9151f57af747e1ea42d464549b115cd36eb25cfc42d77385f9c8222a3
5
5
  SHA512:
6
- metadata.gz: be4ef04a7f885cf2cbd870126a45fc97ec84444426dca4341102256dddbe17eddd3975e63ccd02687527fb43099cda7b1eba70e655cf5ecee4c64b21499237ca
7
- data.tar.gz: f4c305a56ce2952dc72632d9a3fdc13ee9282eefd40dc8fda65f5233bd8e82409a3ba90a64a4964503cb6ba3825cf89c24143311b6051630148fd21d51ef1a60
6
+ metadata.gz: f40852d750dd966b86b0c7cc7efac6c6a24feac83b9e581284adaa7ebc5231571f790e9c8fccc18c54316b48e7563d9b4e3ec4ee36442184fcafd6e22ac296b6
7
+ data.tar.gz: 0f145b61b4ddd8794316cfc4a5bb653212b1e5be1332dcd24726d30783fbd2448b726cf0cec3a0e2b8f9893d60a8f9605ea591350993ac609f80df6edc262518
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.4
1
+ 0.6.6
data/lib/oydid/basic.rb CHANGED
@@ -1012,14 +1012,23 @@ class Oydid
1012
1012
  end
1013
1013
  end
1014
1014
 
1015
- # build an OYDID multibase-encoded private key from a raw hex-encoded key
1016
- # (e.g. an externally generated P-256 or Ed25519 key). The key type is taken
1017
- # from options[:key_type] (default 'ed25519').
1018
- def self.private_key_from_hex(hex, options = {})
1015
+ # normalize and validate hex input shared by the hex-based converters below:
1016
+ # strips whitespace and an optional '0x' prefix and rejects anything that is
1017
+ # not an even number of hex digits. Returns [normalized_hex, ""] or [nil, msg].
1018
+ def self.normalize_hex(hex)
1019
1019
  hex = hex.to_s.strip.delete_prefix("0x").delete_prefix("0X")
1020
1020
  unless hex =~ /\A[0-9a-fA-F]+\z/ && hex.length.even?
1021
1021
  return [nil, "invalid hex input"]
1022
1022
  end
1023
+ return [hex, ""]
1024
+ end
1025
+
1026
+ # build an OYDID multibase-encoded private key from a raw hex-encoded key
1027
+ # (e.g. an externally generated P-256 or Ed25519 key). The key type is taken
1028
+ # from options[:key_type] (default 'ed25519').
1029
+ def self.private_key_from_hex(hex, options = {})
1030
+ hex, msg = normalize_hex(hex)
1031
+ return [nil, msg] if hex.nil?
1023
1032
  raw = [hex].pack("H*")
1024
1033
  key_type = options[:key_type].to_s
1025
1034
  key_type = "ed25519" if key_type == ""
@@ -1047,6 +1056,63 @@ class Oydid
1047
1056
  return multi_encode([code, length, raw].pack("SCa#{length}"), options)
1048
1057
  end
1049
1058
 
1059
+ # counterpart to private_key_from_hex for public keys.
1060
+ #
1061
+ # NOTE on the p256 length check: multicodec 0x1200 ('p256-pub') is specified
1062
+ # for compressed points, but Oydid.public_key() emits uncompressed ones
1063
+ # (OpenSSL's default point_conversion_form), so both forms are accepted here
1064
+ # to stay compatible with keys this library produces itself.
1065
+ def self.public_key_from_hex(hex, options = {})
1066
+ hex, msg = normalize_hex(hex)
1067
+ return [nil, msg] if hex.nil?
1068
+ raw = [hex].pack("H*")
1069
+ key_type = options[:key_type].to_s
1070
+ key_type = "ed25519" if key_type == ""
1071
+ case key_type
1072
+ when "p256"
1073
+ unless [33, 65].include?(raw.bytesize)
1074
+ return [nil, "p256 public key must be 33 (compressed) or 65 (uncompressed) bytes"]
1075
+ end
1076
+ # the length alone says nothing: verify that the input actually
1077
+ # decodes to a point on prime256v1 (this also rejects a wrong
1078
+ # leading byte), otherwise an unusable key would be encoded happily
1079
+ begin
1080
+ OpenSSL::PKey::EC::Point.new(
1081
+ OpenSSL::PKey::EC::Group.new("prime256v1"),
1082
+ OpenSSL::BN.new(hex, 16))
1083
+ rescue StandardError
1084
+ return [nil, "p256 public key is not a valid point on the curve"]
1085
+ end
1086
+ code = Multicodecs["p256-pub"].code
1087
+ when "ed25519"
1088
+ unless raw.bytesize == 32
1089
+ return [nil, "ed25519 public key must be 32 bytes (64 hex characters)"]
1090
+ end
1091
+ # there is no cheap point validation for ed25519 (Ed25519::VerifyKey
1092
+ # accepts any 32 bytes), and a private key has the same length - so
1093
+ # passing a *private* key with --public silently yields a public key
1094
+ # multibase. Callers have to know what they hand in.
1095
+ code = Multicodecs["ed25519-pub"].code
1096
+ else
1097
+ return [nil, "unsupported key type"]
1098
+ end
1099
+ return multi_encode(Multibases::DecodedByteArray.new((to_varint(code) << raw.bytes).flatten).to_s(Encoding::BINARY), options)
1100
+ end
1101
+
1102
+ # convert raw hex data (no multicodec prefix, e.g. a signature) to Multibase
1103
+ def self.hex_to_multibase(hex, options = {})
1104
+ hex, msg = normalize_hex(hex)
1105
+ return [nil, msg] if hex.nil?
1106
+ # the multibases gem cannot represent an all-zero byte string: packing
1107
+ # raises NoMethodError and unpacking the correct base58btc form ("z1111")
1108
+ # raises RangeError. Leading zero bytes in otherwise non-zero input are
1109
+ # handled correctly, so only the all-zero case has to be rejected.
1110
+ if hex.delete("0") == ""
1111
+ return [nil, "all-zero input is not supported by the multibase encoder"]
1112
+ end
1113
+ return multi_encode([hex].pack("H*"), options)
1114
+ end
1115
+
1050
1116
  # reverse of private_key_from_hex / public_key encoding:
1051
1117
  # decode an OYDID Multibase-encoded key (private or public) back to raw hex.
1052
1118
  def self.key_to_hex(key_encoded, options = {})
data/lib/oydid/log.rb CHANGED
@@ -141,6 +141,7 @@ class Oydid
141
141
  terminate_entries = 0
142
142
  terminate_overall = 0
143
143
  terminate_index = nil
144
+ revoked_terminate_found = false
144
145
  logs.each do |el|
145
146
  if el["op"].to_i == 0 # TERMINATE
146
147
  if dag.vertices[i].successors.length == 0
@@ -154,6 +155,7 @@ class Oydid
154
155
  dag.vertices[i].predecessors.each do |l|
155
156
  if logs[l[:id]]["op"].to_i == 0 # TERMINATE
156
157
  terminate_index = l[:id]
158
+ revoked_terminate_found = true
157
159
  end
158
160
  end
159
161
  end
@@ -161,11 +163,18 @@ class Oydid
161
163
  i += 1
162
164
  end unless logs.nil?
163
165
 
166
+ # NOTE: this is a structural check on the log and must not depend on how
167
+ # verbose the caller wants to be. Suppressing it under :silent returned a
168
+ # DID document for logs that cannot be resolved.
164
169
  if terminate_entries != 1 && !options[:log_complete] && !options[:followAlsoKnownAs]
165
- if options[:silent].nil? || !options[:silent]
170
+ # a revoked DID has no tangling TERMINATE entry (its TERMINATE is
171
+ # followed by the REVOKE record). That is not a broken log: continue
172
+ # so that dag_update can report the revocation - or apply DID
173
+ # Rotation - instead of the unspecific "cannot resolve DID".
174
+ unless terminate_entries == 0 && revoked_terminate_found
166
175
  return [nil, nil, nil, "cannot resolve DID" ]
167
176
  end
168
- end
177
+ end
169
178
 
170
179
  # create actual edges between vertices (but only use last terminate index for delegates)
171
180
  dag = DAG.new
@@ -254,8 +263,14 @@ class Oydid
254
263
  result.uniq
255
264
  end
256
265
 
266
+ # a DID is revoked when the REVOCATION record referenced by the current
267
+ # TERMINATE record has been published and no UPDATE record follows it
268
+ REVOKED_ERROR_CODE = 410
269
+
257
270
  def self.dag_update(currentDID, options)
258
271
  i = 0
272
+ revoked = false
273
+ rotation_performed = false
259
274
  doc_location = options[:doc_location].to_s
260
275
  initial_did = currentDID["did"].to_s.dup
261
276
  initial_did = initial_did.delete_prefix("did:oyd:")
@@ -501,6 +516,12 @@ class Oydid
501
516
  end
502
517
  end
503
518
 
519
+ # the revocation record is published and no UPDATE record
520
+ # builds on it: this DID is revoked. The verdict is only
521
+ # acted upon after the log has been walked completely, so
522
+ # that DID Rotation (handled in the REVOKE entry below) can
523
+ # still take precedence.
524
+ revoked = !update_term_found
504
525
  else
505
526
  if verification_output
506
527
  currentDID["verification"] += "Revocation reference in log record: " + revoc_term.to_s + "\n"
@@ -538,6 +559,7 @@ class Oydid
538
559
  # 2) das new DID reference back original DID
539
560
  currentDID["did"] = rotate_DID
540
561
  currentDID["doc"]["doc"] = rotate_ddoc
562
+ rotation_performed = true
541
563
  if verification_output
542
564
  currentDID["verification"] += "DID rotation to: " + rotate_DID.to_s + "\n"
543
565
  currentDID["verification"] += "✅ original DID (" + did_orig + ") revoked and referenced in alsoKnownAs\n"
@@ -563,6 +585,13 @@ class Oydid
563
585
  end
564
586
  i += 1
565
587
  end unless currentDID["log"].nil?
588
+
589
+ # fail closed: a revoked DID has no resolvable DID Document unless the
590
+ # controller rotated it to another DID via alsoKnownAs
591
+ if revoked && !rotation_performed
592
+ currentDID["error"] = REVOKED_ERROR_CODE
593
+ currentDID["message"] = "revoked"
594
+ end
566
595
  return currentDID
567
596
  end
568
597
  end
data/spec/oydid_spec.rb CHANGED
@@ -238,6 +238,82 @@ describe "OYDID handling" do
238
238
  expect(Oydid.key_to_hex("not-a-key", {}).first).to be_nil
239
239
  end
240
240
 
241
+ # hex2mb: public keys (--public)
242
+ it "encodes an ed25519 public key from hex identically to public_key()" do
243
+ privkey, _ = Oydid.private_key_from_hex("aa" * 32, {key_type: "ed25519"})
244
+ pub_mb, _ = Oydid.public_key(privkey, {})
245
+ pub_hex, _ = Oydid.key_to_hex(pub_mb, {})
246
+ from_hex, msg = Oydid.public_key_from_hex(pub_hex, {key_type: "ed25519"})
247
+ expect(msg).to eq ""
248
+ expect(from_hex).to eq pub_mb
249
+ expect(Oydid.get_keytype(from_hex)).to eq "ed25519-pub"
250
+ end
251
+ it "encodes a p256 public key from hex identically to public_key()" do
252
+ privkey, _ = Oydid.private_key_from_hex(
253
+ "96fe0f41947d645c7a1858c48c7a0560e7e5bd3d45125b57a611a3a9a103626b",
254
+ {key_type: "p256"})
255
+ pub_mb, _ = Oydid.public_key(privkey, {})
256
+ pub_hex, _ = Oydid.key_to_hex(pub_mb, {})
257
+ from_hex, msg = Oydid.public_key_from_hex(pub_hex, {key_type: "p256"})
258
+ expect(msg).to eq ""
259
+ expect(from_hex).to eq pub_mb
260
+ expect(Oydid.get_keytype(from_hex)).to eq "p256-pub"
261
+ end
262
+ it "round-trips a public key hex -> mb -> hex" do
263
+ hex = "e734ea6c2b6257de72355e472aa05a4c487e6b463c029ed306df2f01b5636b58"
264
+ mb, _ = Oydid.public_key_from_hex(hex, {key_type: "ed25519"})
265
+ back, _ = Oydid.key_to_hex(mb, {})
266
+ expect(back).to eq hex
267
+ end
268
+ it "accepts a compressed p256 public key" do
269
+ hex = "03bcad0c43ac859d0552d95b639156073f9c1c4fb1aa9490f3639a8cf0a2aaadaa"
270
+ mb, msg = Oydid.public_key_from_hex(hex, {key_type: "p256"})
271
+ expect(msg).to eq ""
272
+ expect(Oydid.get_keytype(mb)).to eq "p256-pub"
273
+ end
274
+ it "rejects a p256 public key that is not a point on the curve" do
275
+ mb, msg = Oydid.public_key_from_hex("04" + "11" * 64, {key_type: "p256"})
276
+ expect(mb).to be_nil
277
+ expect(msg).to match(/not a valid point/)
278
+ end
279
+ it "rejects malformed public key hex" do
280
+ expect(Oydid.public_key_from_hex("xyz", {key_type: "ed25519"}).first).to be_nil
281
+ expect(Oydid.public_key_from_hex("aa" * 31, {key_type: "ed25519"}).first).to be_nil
282
+ expect(Oydid.public_key_from_hex("aa" * 32, {key_type: "p256"}).first).to be_nil
283
+ expect(Oydid.public_key_from_hex("aa" * 32, {key_type: "secp256k1"}).first).to be_nil
284
+ end
285
+ it "defaults to ed25519 when no key type is given for a public key" do
286
+ hex = "e734ea6c2b6257de72355e472aa05a4c487e6b463c029ed306df2f01b5636b58"
287
+ expect(Oydid.get_keytype(Oydid.public_key_from_hex(hex, {}).first)).to eq "ed25519-pub"
288
+ end
289
+
290
+ # hex2mb: raw data (--raw)
291
+ it "encodes raw hex data to multibase without a multicodec prefix" do
292
+ mb, msg = Oydid.hex_to_multibase("deadbeef", {})
293
+ expect(msg).to eq ""
294
+ expect(Oydid.multi_decode(mb).first.unpack1("H*")).to eq "deadbeef"
295
+ # no multicodec prefix -> deliberately not readable back as a key
296
+ expect(Oydid.key_to_hex(mb, {}).first).to be_nil
297
+ end
298
+ it "preserves leading zero bytes in raw hex data" do
299
+ mb, _ = Oydid.hex_to_multibase("00deadbeef", {})
300
+ expect(Oydid.multi_decode(mb).first.unpack1("H*")).to eq "00deadbeef"
301
+ end
302
+ it "accepts a 0x prefix and surrounding whitespace in raw hex data" do
303
+ expect(Oydid.hex_to_multibase(" 0xdeadbeef\n", {}).first).to \
304
+ eq Oydid.hex_to_multibase("deadbeef", {}).first
305
+ end
306
+ it "rejects all-zero raw hex data with a specific message" do
307
+ mb, msg = Oydid.hex_to_multibase("00" * 4, {})
308
+ expect(mb).to be_nil
309
+ expect(msg).to match(/all-zero/)
310
+ end
311
+ it "rejects malformed raw hex data" do
312
+ expect(Oydid.hex_to_multibase("xyz", {}).first).to be_nil
313
+ expect(Oydid.hex_to_multibase("abc", {}).first).to be_nil
314
+ expect(Oydid.hex_to_multibase("", {}).first).to be_nil
315
+ end
316
+
241
317
  # storage functions
242
318
  it "should create 'filename' and put/read 'text'" do
243
319
  @buffer = StringIO.new()
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.6.4
4
+ version: 0.6.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christoph Fabianek
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-06-27 00:00:00.000000000 Z
10
+ date: 2026-08-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: simple_dag
@@ -415,14 +415,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
415
415
  requirements:
416
416
  - - ">="
417
417
  - !ruby/object:Gem::Version
418
- version: 3.2.8
418
+ version: 3.0.0
419
419
  required_rubygems_version: !ruby/object:Gem::Requirement
420
420
  requirements:
421
421
  - - ">="
422
422
  - !ruby/object:Gem::Version
423
423
  version: '0'
424
424
  requirements: []
425
- rubygems_version: 4.0.15
425
+ rubygems_version: 4.0.12
426
426
  specification_version: 4
427
427
  summary: Own Your Decentralized Identifier for Ruby.
428
428
  test_files: