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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53df9e3939e5a1e5a6f1ccede456130a6160b08c3e89cb0dd4a05efbb8210ebc
4
- data.tar.gz: bce7ea71e480908e1565f2783a709a3a67e3f2b5e43492bc4a535f9750cb738d
3
+ metadata.gz: e048d33455d4b2b305d1905b1acea54c0f87f7a871998fa5a294b0669a245112
4
+ data.tar.gz: 0ff2fabe31fe7352062e93ef8bce6c74dfa6afd7004db0f53ccfb13912b7e9d5
5
5
  SHA512:
6
- metadata.gz: b9483301abb9a88db165a3ad72024449c6731f042fee0b6480b23981999d658af14a87b76aa3170084a9de06e31b6f33ab10e1dc42c788e7a2b07b94b7641d69
7
- data.tar.gz: b14e69f7e18fa55169fedb57d5b5197f3332a4ccb0270dabee0ed5b7b0f9db005d4f81a8ab9e75e8dd4a44c1371e9e7c52be40032c342ae35c91c6e9eb3c4e4e
6
+ metadata.gz: be4ef04a7f885cf2cbd870126a45fc97ec84444426dca4341102256dddbe17eddd3975e63ccd02687527fb43099cda7b1eba70e655cf5ecee4c64b21499237ca
7
+ data.tar.gz: f4c305a56ce2952dc72632d9a3fdc13ee9282eefd40dc8fda65f5233bd8e82409a3ba90a64a4964503cb6ba3825cf89c24143311b6051630148fd21d51ef1a60
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.1
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
- # input for p256-priv requires valid base64 encoded private key
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
- key = OpenSSL::PKey.read Base64.decode64(input)
206
+ candidate = OpenSSL::PKey.read(Base64.decode64(input))
207
+ key = candidate if candidate.is_a?(OpenSSL::PKey::EC)
203
208
  rescue
204
- return [nil, "invalid input"]
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
- privateKey, msg = generate_private_key(pwd, 'ed25519-priv', options)
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
- if did_info["log"][did_info["termination_log_id"]]["doc"] != multi_hash(canonical(revocationLog), LOG_HASH_OPTIONS).first
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.1
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-03-23 00:00:00.000000000 Z
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.6
425
+ rubygems_version: 4.0.15
426
426
  specification_version: 4
427
427
  summary: Own Your Decentralized Identifier for Ruby.
428
428
  test_files: