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 +4 -4
- data/VERSION +1 -1
- data/lib/oydid/basic.rb +70 -4
- data/lib/oydid/log.rb +31 -2
- data/spec/oydid_spec.rb +76 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e47064181aee5338fa3fde2af77f2955cbcb6e5a4b19f8586882183056f1070
|
|
4
|
+
data.tar.gz: d601a5b9151f57af747e1ea42d464549b115cd36eb25cfc42d77385f9c8222a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f40852d750dd966b86b0c7cc7efac6c6a24feac83b9e581284adaa7ebc5231571f790e9c8fccc18c54316b48e7563d9b4e3ec4ee36442184fcafd6e22ac296b6
|
|
7
|
+
data.tar.gz: 0f145b61b4ddd8794316cfc4a5bb653212b1e5be1332dcd24726d30783fbd2448b726cf0cec3a0e2b8f9893d60a8f9605ea591350993ac609f80df6edc262518
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
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
|
-
#
|
|
1016
|
-
#
|
|
1017
|
-
#
|
|
1018
|
-
def self.
|
|
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
|
-
|
|
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
|
+
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-
|
|
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.
|
|
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.
|
|
425
|
+
rubygems_version: 4.0.12
|
|
426
426
|
specification_version: 4
|
|
427
427
|
summary: Own Your Decentralized Identifier for Ruby.
|
|
428
428
|
test_files:
|