oydid 0.6.1 → 0.6.5
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 +36 -2
- data/lib/oydid.rb +43 -1
- data/spec/oydid_spec.rb +71 -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: 7cd355cc4bdc04a3d149199467cbbe4263ace619676422c722485fc6d459c83b
|
|
4
|
+
data.tar.gz: 85f8930282e7cbee45997d65a5cca1be2bea78f853cce44152d6b93808e1a923
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e04e8cd58b7a29bb9e7dabffffcf613c9459c18ece1254b8cc1ad60b4d19d073276f18bd0f9c36b7cd6786340e1f81fb9be320f8964a182d5b861bfab9d5825f
|
|
7
|
+
data.tar.gz: 370d28f6c8614238b734a9a0dc8410de4007923c6ac7e464190ad36bd82bbba5f28def3e46d05c74d882b5ba5559f11cadf2a4a26b2ea0045ea87acd8a6503bb
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.5
|
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
|
|
@@ -136,6 +141,7 @@ class Oydid
|
|
|
136
141
|
terminate_entries = 0
|
|
137
142
|
terminate_overall = 0
|
|
138
143
|
terminate_index = nil
|
|
144
|
+
revoked_terminate_found = false
|
|
139
145
|
logs.each do |el|
|
|
140
146
|
if el["op"].to_i == 0 # TERMINATE
|
|
141
147
|
if dag.vertices[i].successors.length == 0
|
|
@@ -149,6 +155,7 @@ class Oydid
|
|
|
149
155
|
dag.vertices[i].predecessors.each do |l|
|
|
150
156
|
if logs[l[:id]]["op"].to_i == 0 # TERMINATE
|
|
151
157
|
terminate_index = l[:id]
|
|
158
|
+
revoked_terminate_found = true
|
|
152
159
|
end
|
|
153
160
|
end
|
|
154
161
|
end
|
|
@@ -156,11 +163,18 @@ class Oydid
|
|
|
156
163
|
i += 1
|
|
157
164
|
end unless logs.nil?
|
|
158
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.
|
|
159
169
|
if terminate_entries != 1 && !options[:log_complete] && !options[:followAlsoKnownAs]
|
|
160
|
-
|
|
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
|
|
161
175
|
return [nil, nil, nil, "cannot resolve DID" ]
|
|
162
176
|
end
|
|
163
|
-
end
|
|
177
|
+
end
|
|
164
178
|
|
|
165
179
|
# create actual edges between vertices (but only use last terminate index for delegates)
|
|
166
180
|
dag = DAG.new
|
|
@@ -249,8 +263,14 @@ class Oydid
|
|
|
249
263
|
result.uniq
|
|
250
264
|
end
|
|
251
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
|
+
|
|
252
270
|
def self.dag_update(currentDID, options)
|
|
253
271
|
i = 0
|
|
272
|
+
revoked = false
|
|
273
|
+
rotation_performed = false
|
|
254
274
|
doc_location = options[:doc_location].to_s
|
|
255
275
|
initial_did = currentDID["did"].to_s.dup
|
|
256
276
|
initial_did = initial_did.delete_prefix("did:oyd:")
|
|
@@ -496,6 +516,12 @@ class Oydid
|
|
|
496
516
|
end
|
|
497
517
|
end
|
|
498
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
|
|
499
525
|
else
|
|
500
526
|
if verification_output
|
|
501
527
|
currentDID["verification"] += "Revocation reference in log record: " + revoc_term.to_s + "\n"
|
|
@@ -533,6 +559,7 @@ class Oydid
|
|
|
533
559
|
# 2) das new DID reference back original DID
|
|
534
560
|
currentDID["did"] = rotate_DID
|
|
535
561
|
currentDID["doc"]["doc"] = rotate_ddoc
|
|
562
|
+
rotation_performed = true
|
|
536
563
|
if verification_output
|
|
537
564
|
currentDID["verification"] += "DID rotation to: " + rotate_DID.to_s + "\n"
|
|
538
565
|
currentDID["verification"] += "✅ original DID (" + did_orig + ") revoked and referenced in alsoKnownAs\n"
|
|
@@ -558,6 +585,13 @@ class Oydid
|
|
|
558
585
|
end
|
|
559
586
|
i += 1
|
|
560
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
|
|
561
595
|
return currentDID
|
|
562
596
|
end
|
|
563
597
|
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.5
|
|
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-18 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.15
|
|
426
426
|
specification_version: 4
|
|
427
427
|
summary: Own Your Decentralized Identifier for Ruby.
|
|
428
428
|
test_files:
|