oydid 0.6.1 → 0.6.4
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 +114 -6
- data/lib/oydid/log.rb +5 -0
- data/lib/oydid.rb +43 -1
- data/spec/oydid_spec.rb +71 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e048d33455d4b2b305d1905b1acea54c0f87f7a871998fa5a294b0669a245112
|
|
4
|
+
data.tar.gz: 0ff2fabe31fe7352062e93ef8bce6c74dfa6afd7004db0f53ccfb13912b7e9d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be4ef04a7f885cf2cbd870126a45fc97ec84444426dca4341102256dddbe17eddd3975e63ccd02687527fb43099cda7b1eba70e655cf5ecee4c64b21499237ca
|
|
7
|
+
data.tar.gz: f4c305a56ce2952dc72632d9a3fdc13ee9282eefd40dc8fda65f5233bd8e82409a3ba90a64a4964503cb6ba3825cf89c24143311b6051630148fd21d51ef1a60
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.4
|
data/lib/oydid/basic.rb
CHANGED
|
@@ -193,18 +193,31 @@ class Oydid
|
|
|
193
193
|
end
|
|
194
194
|
raw_key = raw_key.to_bytes
|
|
195
195
|
when 'p256-priv'
|
|
196
|
-
key = OpenSSL::PKey::EC.new('prime256v1')
|
|
197
196
|
if input == ""
|
|
197
|
+
# no seed provided => generate a random key
|
|
198
198
|
key = OpenSSL::PKey::EC.generate('prime256v1')
|
|
199
|
+
raw_key = key.private_key.to_s(2)
|
|
199
200
|
else
|
|
200
|
-
#
|
|
201
|
+
# try to interpret input as a base64-encoded existing private key first;
|
|
202
|
+
# otherwise derive the key deterministically from input (seed/password),
|
|
203
|
+
# mirroring the ed25519-priv behaviour above
|
|
204
|
+
key = nil
|
|
201
205
|
begin
|
|
202
|
-
|
|
206
|
+
candidate = OpenSSL::PKey.read(Base64.decode64(input))
|
|
207
|
+
key = candidate if candidate.is_a?(OpenSSL::PKey::EC)
|
|
203
208
|
rescue
|
|
204
|
-
|
|
209
|
+
key = nil
|
|
210
|
+
end
|
|
211
|
+
if key.nil?
|
|
212
|
+
# deterministic derivation: hash seed to a scalar in [1, n-1]
|
|
213
|
+
order = OpenSSL::PKey::EC::Group.new('prime256v1').order
|
|
214
|
+
scalar = (OpenSSL::BN.new(RbNaCl::Hash.sha256(input), 2) % (order - OpenSSL::BN.new(1))) + OpenSSL::BN.new(1)
|
|
215
|
+
raw_key = scalar.to_s(2)
|
|
216
|
+
raw_key = ("\x00".b * (32 - raw_key.bytesize)) + raw_key if raw_key.bytesize < 32
|
|
217
|
+
else
|
|
218
|
+
raw_key = key.private_key.to_s(2)
|
|
205
219
|
end
|
|
206
220
|
end
|
|
207
|
-
raw_key = key.private_key.to_s(2)
|
|
208
221
|
else
|
|
209
222
|
return [nil, "unsupported key codec"]
|
|
210
223
|
end
|
|
@@ -284,7 +297,9 @@ class Oydid
|
|
|
284
297
|
privateKey, msg = read_private_key(dsk.to_s, options)
|
|
285
298
|
end
|
|
286
299
|
else
|
|
287
|
-
|
|
300
|
+
kt = options[:key_type].to_s
|
|
301
|
+
kt = 'ed25519' if kt == ''
|
|
302
|
+
privateKey, msg = generate_private_key(pwd, kt + '-priv', options)
|
|
288
303
|
end
|
|
289
304
|
else
|
|
290
305
|
privateKey, msg = decode_private_key(enc.to_s, options)
|
|
@@ -971,6 +986,94 @@ class Oydid
|
|
|
971
986
|
Base64.urlsafe_decode64(str + '=' * (4 - str.length % 4))
|
|
972
987
|
end
|
|
973
988
|
|
|
989
|
+
def self.private_key_from_jwk(jwk, options = {})
|
|
990
|
+
begin
|
|
991
|
+
if jwk.is_a?(String)
|
|
992
|
+
jwk = JSON.parse(jwk)
|
|
993
|
+
end
|
|
994
|
+
rescue
|
|
995
|
+
return [nil, "invalid input"]
|
|
996
|
+
end
|
|
997
|
+
jwk = jwk.transform_keys(&:to_s)
|
|
998
|
+
if jwk["kty"] == "EC" && jwk["crv"] == "P-256"
|
|
999
|
+
digest = base64_url_decode(jwk["d"])
|
|
1000
|
+
group = OpenSSL::PKey::EC::Group.new("prime256v1")
|
|
1001
|
+
public_key = group.generator.mul(OpenSSL::BN.new(digest, 2))
|
|
1002
|
+
point = public_key.to_bn.to_s(2)
|
|
1003
|
+
x_calc = Base64.urlsafe_encode64(point[1, 32], padding: false)
|
|
1004
|
+
y_calc = Base64.urlsafe_encode64(point[33, 32], padding: false)
|
|
1005
|
+
return [nil, "x/y do not match d"] unless x_calc == jwk["x"] && y_calc == jwk["y"]
|
|
1006
|
+
|
|
1007
|
+
code = Multicodecs["p256-priv"].code
|
|
1008
|
+
length = digest.bytesize
|
|
1009
|
+
return multi_encode([code, length, digest].pack("SCa#{length}"), options)
|
|
1010
|
+
else
|
|
1011
|
+
return [nil, "unsupported key codec"]
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
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 = {})
|
|
1019
|
+
hex = hex.to_s.strip.delete_prefix("0x").delete_prefix("0X")
|
|
1020
|
+
unless hex =~ /\A[0-9a-fA-F]+\z/ && hex.length.even?
|
|
1021
|
+
return [nil, "invalid hex input"]
|
|
1022
|
+
end
|
|
1023
|
+
raw = [hex].pack("H*")
|
|
1024
|
+
key_type = options[:key_type].to_s
|
|
1025
|
+
key_type = "ed25519" if key_type == ""
|
|
1026
|
+
case key_type
|
|
1027
|
+
when "p256"
|
|
1028
|
+
unless raw.bytesize == 32
|
|
1029
|
+
return [nil, "p256 private key must be 32 bytes (64 hex characters)"]
|
|
1030
|
+
end
|
|
1031
|
+
# scalar must be a valid private key in [1, n-1]
|
|
1032
|
+
order = OpenSSL::PKey::EC::Group.new("prime256v1").order
|
|
1033
|
+
scalar = OpenSSL::BN.new(raw, 2)
|
|
1034
|
+
if scalar < OpenSSL::BN.new(1) || scalar >= order
|
|
1035
|
+
return [nil, "p256 private key out of range"]
|
|
1036
|
+
end
|
|
1037
|
+
code = Multicodecs["p256-priv"].code
|
|
1038
|
+
when "ed25519"
|
|
1039
|
+
unless raw.bytesize == 32
|
|
1040
|
+
return [nil, "ed25519 private key must be 32 bytes (64 hex characters)"]
|
|
1041
|
+
end
|
|
1042
|
+
code = Multicodecs["ed25519-priv"].code
|
|
1043
|
+
else
|
|
1044
|
+
return [nil, "unsupported key type"]
|
|
1045
|
+
end
|
|
1046
|
+
length = raw.bytesize
|
|
1047
|
+
return multi_encode([code, length, raw].pack("SCa#{length}"), options)
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
# reverse of private_key_from_hex / public_key encoding:
|
|
1051
|
+
# decode an OYDID Multibase-encoded key (private or public) back to raw hex.
|
|
1052
|
+
def self.key_to_hex(key_encoded, options = {})
|
|
1053
|
+
key_encoded = key_encoded.to_s.strip
|
|
1054
|
+
key_type = get_keytype(key_encoded) rescue nil
|
|
1055
|
+
if key_type.nil?
|
|
1056
|
+
return [nil, "unsupported or invalid key"]
|
|
1057
|
+
end
|
|
1058
|
+
begin
|
|
1059
|
+
raw = multi_decode(key_encoded).first
|
|
1060
|
+
case key_type
|
|
1061
|
+
when 'ed25519-priv', 'p256-priv'
|
|
1062
|
+
# custom layout: [uint16 codec][uint8 length][raw key bytes]
|
|
1063
|
+
_code, _length, digest = raw.unpack('SCa*')
|
|
1064
|
+
hex = digest.unpack1('H*')
|
|
1065
|
+
when 'ed25519-pub', 'p256-pub'
|
|
1066
|
+
# multicodec varint prefix (2 bytes) followed by raw key material
|
|
1067
|
+
hex = raw[2..].unpack1('H*')
|
|
1068
|
+
else
|
|
1069
|
+
return [nil, "unsupported key codec"]
|
|
1070
|
+
end
|
|
1071
|
+
rescue
|
|
1072
|
+
return [nil, "invalid key"]
|
|
1073
|
+
end
|
|
1074
|
+
return [hex, ""]
|
|
1075
|
+
end
|
|
1076
|
+
|
|
974
1077
|
def self.public_key_from_jwk(jwk, options = {})
|
|
975
1078
|
begin
|
|
976
1079
|
if jwk.is_a?(String)
|
|
@@ -1043,6 +1146,11 @@ class Oydid
|
|
|
1043
1146
|
end
|
|
1044
1147
|
|
|
1045
1148
|
def self.retrieve_document(doc_identifier, doc_file, doc_location, options)
|
|
1149
|
+
# in-process callers can supply the DID document directly (e.g. read from
|
|
1150
|
+
# a local database) to avoid any HTTP/file lookup
|
|
1151
|
+
if options[:local_doc].is_a?(Hash) && !options[:local_doc].empty?
|
|
1152
|
+
return [options[:local_doc], ""]
|
|
1153
|
+
end
|
|
1046
1154
|
if doc_location == ""
|
|
1047
1155
|
doc_location = DEFAULT_LOCATION
|
|
1048
1156
|
end
|
data/lib/oydid/log.rb
CHANGED
|
@@ -24,6 +24,11 @@ class Oydid
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def self.retrieve_log(did_hash, log_file, log_location, options)
|
|
27
|
+
# in-process callers can supply the raw log array directly (e.g. read from
|
|
28
|
+
# a local database) to avoid any HTTP/file lookup
|
|
29
|
+
if options[:local_log].is_a?(Array)
|
|
30
|
+
return [options[:local_log], ""]
|
|
31
|
+
end
|
|
27
32
|
if log_location == ""
|
|
28
33
|
log_location = DEFAULT_LOCATION
|
|
29
34
|
end
|
data/lib/oydid.rb
CHANGED
|
@@ -541,6 +541,12 @@ class Oydid
|
|
|
541
541
|
end
|
|
542
542
|
|
|
543
543
|
def self.publish(did, didDocument, logs, options)
|
|
544
|
+
# in-process callers (e.g. a repository storing DIDs in its own database)
|
|
545
|
+
# can skip remote/file publishing and persist the returned logs themselves
|
|
546
|
+
if options[:skip_publish]
|
|
547
|
+
return [true, ""]
|
|
548
|
+
end
|
|
549
|
+
|
|
544
550
|
did_hash = did.delete_prefix("did:oyd:")
|
|
545
551
|
did10 = did_hash[0,10]
|
|
546
552
|
|
|
@@ -584,6 +590,13 @@ class Oydid
|
|
|
584
590
|
end
|
|
585
591
|
|
|
586
592
|
def self.persist_cmsm(pubkey, payload, options)
|
|
593
|
+
# in-process callers can supply a store object (responding to
|
|
594
|
+
# #set(pubkey, payload)) to persist CMSM data in a local database
|
|
595
|
+
if options[:cmsm_store]
|
|
596
|
+
options[:cmsm_store].set(pubkey, payload)
|
|
597
|
+
return [true, ""]
|
|
598
|
+
end
|
|
599
|
+
|
|
587
600
|
doc_location = options[:doc_location]
|
|
588
601
|
if doc_location.to_s == ""
|
|
589
602
|
doc_location = DEFAULT_LOCATION
|
|
@@ -612,6 +625,17 @@ class Oydid
|
|
|
612
625
|
end
|
|
613
626
|
|
|
614
627
|
def self.check_cmsm(pubkey, options)
|
|
628
|
+
# in-process callers can supply a store object (responding to
|
|
629
|
+
# #get(pubkey)) to read CMSM data from a local database
|
|
630
|
+
if options[:cmsm_store]
|
|
631
|
+
payload = options[:cmsm_store].get(pubkey)
|
|
632
|
+
if payload.nil?
|
|
633
|
+
return [nil, "no persisted CMSM data for key"]
|
|
634
|
+
end
|
|
635
|
+
payload = payload.transform_keys(&:to_s) if payload.is_a?(Hash)
|
|
636
|
+
return [payload, ""]
|
|
637
|
+
end
|
|
638
|
+
|
|
615
639
|
doc_location = options[:doc_location]
|
|
616
640
|
if doc_location.to_s == ""
|
|
617
641
|
doc_location = DEFAULT_LOCATION
|
|
@@ -937,7 +961,12 @@ class Oydid
|
|
|
937
961
|
end
|
|
938
962
|
|
|
939
963
|
# check if REVOCATION hash matches hash in TERMINATION
|
|
940
|
-
|
|
964
|
+
# the "doc" field of the TERMINATE log entry may carry a location suffix
|
|
965
|
+
# (e.g. "<hash>@http://localhost:3000") when the DID was created with -l,
|
|
966
|
+
# so strip the location prefix before comparing with the bare revocation hash
|
|
967
|
+
termination_doc = did_info["log"][did_info["termination_log_id"]]["doc"]
|
|
968
|
+
termination_doc = termination_doc.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first
|
|
969
|
+
if termination_doc != multi_hash(canonical(revocationLog), LOG_HASH_OPTIONS).first
|
|
941
970
|
return [nil, "invalid revocation information"]
|
|
942
971
|
end
|
|
943
972
|
revoc_log = JSON.parse(revocationLog)
|
|
@@ -957,9 +986,17 @@ class Oydid
|
|
|
957
986
|
did_hash = hash_split[0]
|
|
958
987
|
doc_location = hash_split[1]
|
|
959
988
|
end
|
|
989
|
+
if did_hash.include?(CGI.escape LOCATION_PREFIX)
|
|
990
|
+
hash_split = did_hash.split(CGI.escape LOCATION_PREFIX)
|
|
991
|
+
did_hash = hash_split[0]
|
|
992
|
+
doc_location = hash_split[1]
|
|
993
|
+
end
|
|
960
994
|
if doc_location.to_s == ""
|
|
961
995
|
doc_location = DEFAULT_LOCATION
|
|
962
996
|
end
|
|
997
|
+
# the location extracted from the DID may be percent-encoded
|
|
998
|
+
# (e.g. "http%3A%2F%2Flocalhost%3A3000"); decode before using as URL
|
|
999
|
+
doc_location = doc_location.to_s.gsub("%3A", ":").gsub("%2F", "/")
|
|
963
1000
|
|
|
964
1001
|
# publish revocation log based on location
|
|
965
1002
|
case doc_location.to_s
|
|
@@ -986,6 +1023,11 @@ class Oydid
|
|
|
986
1023
|
if revoc_log.nil?
|
|
987
1024
|
return [nil, msg]
|
|
988
1025
|
end
|
|
1026
|
+
# in-process callers persist the revocation log themselves; return it
|
|
1027
|
+
# instead of publishing to a remote location / file
|
|
1028
|
+
if options[:skip_publish]
|
|
1029
|
+
return [revoc_log, ""]
|
|
1030
|
+
end
|
|
989
1031
|
success, msg = revoke_publish(did, revoc_log, options)
|
|
990
1032
|
end
|
|
991
1033
|
|
data/spec/oydid_spec.rb
CHANGED
|
@@ -167,6 +167,77 @@ describe "OYDID handling" do
|
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
# P-256 predefined / deterministic key handling
|
|
171
|
+
it "derives a deterministic p256 private key from a seed/password" do
|
|
172
|
+
key1, _ = Oydid.generate_private_key("my-fixed-seed", "p256-priv", {key_type: "p256"})
|
|
173
|
+
key2, _ = Oydid.generate_private_key("my-fixed-seed", "p256-priv", {key_type: "p256"})
|
|
174
|
+
expect(key1).not_to be_nil
|
|
175
|
+
expect(key1).to eq key2
|
|
176
|
+
expect(Oydid.get_keytype(key1)).to eq "p256-priv"
|
|
177
|
+
end
|
|
178
|
+
it "derives different p256 keys for different seeds" do
|
|
179
|
+
key1, _ = Oydid.generate_private_key("seed-a", "p256-priv", {})
|
|
180
|
+
key2, _ = Oydid.generate_private_key("seed-b", "p256-priv", {})
|
|
181
|
+
expect(key1).not_to eq key2
|
|
182
|
+
end
|
|
183
|
+
it "produces a usable p256 keypair from a derived key (sign/verify roundtrip)" do
|
|
184
|
+
privkey, _ = Oydid.generate_private_key("roundtrip-seed", "p256-priv", {})
|
|
185
|
+
pubkey, _ = Oydid.public_key(privkey, {})
|
|
186
|
+
expect(pubkey).not_to be_nil
|
|
187
|
+
message = "hello oydid"
|
|
188
|
+
signature, _ = Oydid.sign(message, privkey, {})
|
|
189
|
+
expect(signature).not_to be_nil
|
|
190
|
+
expect(Oydid.verify(message, signature, pubkey).first).to eq true
|
|
191
|
+
end
|
|
192
|
+
it "preserves a predefined base64-encoded p256 private key" do
|
|
193
|
+
ec = OpenSSL::PKey::EC.generate('prime256v1')
|
|
194
|
+
b64 = Base64.encode64(ec.to_pem)
|
|
195
|
+
encoded, _ = Oydid.generate_private_key(b64, "p256-priv", {})
|
|
196
|
+
expect(encoded).not_to be_nil
|
|
197
|
+
decoded = Oydid.decode_private_key(encoded).first
|
|
198
|
+
expect(decoded.private_key.to_s(2)).to eq ec.private_key.to_s(2)
|
|
199
|
+
end
|
|
200
|
+
it "getPrivateKey honors key_type for password-derived keys" do
|
|
201
|
+
p256_key, _ = Oydid.getPrivateKey(nil, "shared-pwd", nil, nil, {key_type: "p256"})
|
|
202
|
+
ed_key, _ = Oydid.getPrivateKey(nil, "shared-pwd", nil, nil, {key_type: "ed25519"})
|
|
203
|
+
expect(Oydid.get_keytype(p256_key)).to eq "p256-priv"
|
|
204
|
+
expect(Oydid.get_keytype(ed_key)).to eq "ed25519-priv"
|
|
205
|
+
end
|
|
206
|
+
it "imports a hex-encoded p256 private key (matching the JWK conversion)" do
|
|
207
|
+
hex = "96fe0f41947d645c7a1858c48c7a0560e7e5bd3d45125b57a611a3a9a103626b"
|
|
208
|
+
jwk = {kty: "EC", crv: "P-256",
|
|
209
|
+
d: "lv4PQZR9ZFx6GFjEjHoFYOflvT1FEltXphGjqaEDYms",
|
|
210
|
+
x: "vK0MQ6yFnQVS2VtjkVYHP5wcT7GqlJDzY5qM8KKqrao",
|
|
211
|
+
y: "R3AQWDZ-AAdwQ3sys1UwhIA5MX2WNnmSerQRKDKxg48"}
|
|
212
|
+
from_hex, _ = Oydid.private_key_from_hex(hex, {key_type: "p256"})
|
|
213
|
+
from_jwk, _ = Oydid.private_key_from_jwk(jwk.to_json, {})
|
|
214
|
+
expect(from_hex).not_to be_nil
|
|
215
|
+
expect(Oydid.get_keytype(from_hex)).to eq "p256-priv"
|
|
216
|
+
expect(from_hex).to eq from_jwk
|
|
217
|
+
end
|
|
218
|
+
it "rejects malformed hex private keys" do
|
|
219
|
+
expect(Oydid.private_key_from_hex("xyz", {key_type: "p256"}).first).to be_nil
|
|
220
|
+
expect(Oydid.private_key_from_hex("96fe", {key_type: "p256"}).first).to be_nil
|
|
221
|
+
expect(Oydid.private_key_from_hex("00" * 32, {key_type: "p256"}).first).to be_nil
|
|
222
|
+
end
|
|
223
|
+
it "round-trips a p256 private key hex -> mb -> hex" do
|
|
224
|
+
hex = "96fe0f41947d645c7a1858c48c7a0560e7e5bd3d45125b57a611a3a9a103626b"
|
|
225
|
+
mb, _ = Oydid.private_key_from_hex(hex, {key_type: "p256"})
|
|
226
|
+
back, _ = Oydid.key_to_hex(mb, {})
|
|
227
|
+
expect(back).to eq hex
|
|
228
|
+
end
|
|
229
|
+
it "decodes a p256 public key from mb to uncompressed hex" do
|
|
230
|
+
privkey, _ = Oydid.private_key_from_hex(
|
|
231
|
+
"96fe0f41947d645c7a1858c48c7a0560e7e5bd3d45125b57a611a3a9a103626b",
|
|
232
|
+
{key_type: "p256"})
|
|
233
|
+
pub_mb, _ = Oydid.public_key(privkey, {})
|
|
234
|
+
pub_hex, _ = Oydid.key_to_hex(pub_mb, {})
|
|
235
|
+
expect(pub_hex).to eq "04bcad0c43ac859d0552d95b639156073f9c1c4fb1aa9490f3639a8cf0a2aaadaa47701058367e000770437b32b35530848039317d963679927ab4112832b1838f"
|
|
236
|
+
end
|
|
237
|
+
it "rejects an invalid multibase key in key_to_hex" do
|
|
238
|
+
expect(Oydid.key_to_hex("not-a-key", {}).first).to be_nil
|
|
239
|
+
end
|
|
240
|
+
|
|
170
241
|
# storage functions
|
|
171
242
|
it "should create 'filename' and put/read 'text'" do
|
|
172
243
|
@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.4
|
|
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-06-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: simple_dag
|
|
@@ -422,7 +422,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
422
422
|
- !ruby/object:Gem::Version
|
|
423
423
|
version: '0'
|
|
424
424
|
requirements: []
|
|
425
|
-
rubygems_version: 4.0.
|
|
425
|
+
rubygems_version: 4.0.15
|
|
426
426
|
specification_version: 4
|
|
427
427
|
summary: Own Your Decentralized Identifier for Ruby.
|
|
428
428
|
test_files:
|