oydid 0.9.6 → 0.9.7

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: e2852f618ab2af5a4076ece7807b931168d061345496397dfc3f409faf25da10
4
- data.tar.gz: 53bc6f370b519b0c72eedbf0bac1fb742fd5288b3696e93c080137531e8eedb7
3
+ metadata.gz: e80efb72fffc8f0a7e8ec1abb8a28376dad2f3463f581bd97719044d32f1f806
4
+ data.tar.gz: ac2fad7ca40541d40ec8b616704bc2225e4ebe144641805347fc7716682cdc8b
5
5
  SHA512:
6
- metadata.gz: 8cc1a99558914d4c968db056cfbd2aa50835ecbc22dc981e54f4eee24c95dbc9fc0e4c7e7c2378c4fe96672255ce54ae9ce91d6d3e3a6e1f2fe96a7d4a26b3fb
7
- data.tar.gz: 420f07f1d3a5145dedfdfb1d65fe8cf83c979bcd99283e48437b2291e04007e3532ea3ea5ff53a9cc8a35fb155989989d0bac094fc8051e9d083641a83a54ff1
6
+ metadata.gz: d0886bb19bc142e297bc454117fec9c904e4ad4ad9b8fec49e359c1824333af1fee5a086dfb05788415eadfc7cb1e16a720ec7bca35bcd58dce2799aded1d4f8
7
+ data.tar.gz: 1e6d067d6f0c7fcf0a824965b253e4f98108efbb7356775e25a46954c3d37e94bf65c34d9295127fd6c61be9a2bf010e09a997f8c25524b6255f67b49621069e
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.6
1
+ 0.9.7
data/lib/oydid/basic.rb CHANGED
@@ -1272,6 +1272,29 @@ class Oydid
1272
1272
  end
1273
1273
  end
1274
1274
 
1275
+ # Where a document would be written: the explicit doc_location, the generic
1276
+ # location, or the default repository - the same fallbacks publish uses.
1277
+ def self.write_location(options)
1278
+ loc = options[:doc_location].to_s
1279
+ loc = options[:location].to_s if loc == ""
1280
+ loc = DEFAULT_LOCATION if loc == ""
1281
+ loc
1282
+ end
1283
+
1284
+ # Ask a repository whether a public key already controls an active DID.
1285
+ # Only a clear "yes" counts: a repository that does not know the endpoint
1286
+ # (404), a non-HTTP location or an unreachable host must not block a create -
1287
+ # the write path enforces the rule in any case, this is the early exit.
1288
+ def self.key_in_active_use?(public_key, location, options = {})
1289
+ return false if public_key.to_s == ""
1290
+ return false unless location.to_s.start_with?("http")
1291
+ retVal = HTTParty.get(location.to_s + "/key/" + public_key.to_s)
1292
+ return false if retVal.code != 200
1293
+ retVal.parsed_response["active"] == true
1294
+ rescue StandardError
1295
+ false
1296
+ end
1297
+
1275
1298
  def self.retrieve_document(doc_identifier, doc_file, doc_location, options)
1276
1299
  # in-process callers can supply the DID document directly (e.g. read from
1277
1300
  # a local database) to avoid any HTTP/file lookup
data/lib/oydid/vc.rb CHANGED
@@ -58,7 +58,7 @@ class Oydid
58
58
  if retVal.code == 401
59
59
  msg = "unauthorized (valid Bearer token required)"
60
60
  else
61
- msg = retVal.parsed_response("error").to_s rescue "invalid response from " + vc_url.to_s
61
+ msg = http_error_message(retVal, vc_url)
62
62
  end
63
63
  return [nil, msg]
64
64
  end
@@ -338,7 +338,7 @@ class Oydid
338
338
  headers: { 'Content-Type' => 'application/json' },
339
339
  body: vc_data.to_json )
340
340
  if retVal.code != 200
341
- err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + vc_url.to_s
341
+ err_msg = http_error_message(retVal, vc_url)
342
342
  return [nil, err_msg]
343
343
  end
344
344
  return [retVal["identifier"], ""]
@@ -355,7 +355,7 @@ class Oydid
355
355
  vp_url = vp_location.sub(/(\/)+$/,'') + "/presentations/" + identifier
356
356
  retVal = HTTParty.get(vp_url)
357
357
  if retVal.code != 200
358
- msg = retVal.parsed_response("error").to_s rescue "invalid response from " + vp_url.to_s
358
+ msg = http_error_message(retVal, vp_url)
359
359
  return [nil, msg]
360
360
  end
361
361
  return [retVal.parsed_response, ""]
@@ -451,7 +451,7 @@ class Oydid
451
451
  headers: { 'Content-Type' => 'application/json' },
452
452
  body: vp_data.to_json )
453
453
  if retVal.code != 200
454
- err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + vp_url.to_s
454
+ err_msg = http_error_message(retVal, vp_url)
455
455
  return [nil, err_msg]
456
456
  end
457
457
  return [vp["identifier"], ""]
data/lib/oydid.rb CHANGED
@@ -39,6 +39,12 @@ class Oydid
39
39
  JWS_SECURITY_SUITE = "https://w3id.org/security/suites/jws-2020/v1"
40
40
  DEFAULT_PUBLIC_RESOLVER = "https://dev.uniresolver.io/1.0/identifiers/"
41
41
 
42
+ # A public key controls at most one active DID at a time. The repository
43
+ # enforces this when a document is written; the constant lives here so every
44
+ # caller (repository, registrar driver, CLI) can recognise the rejection by
45
+ # comparison instead of searching the message text.
46
+ KEY_IN_USE_ERROR = "public key already controls an active DID"
47
+
42
48
  # Single-byte multicodec codes for the intermediate BLAKE2b digest sizes
43
49
  # (17-23 bytes), needed to keep a did:oyd identifier short enough for the
44
50
  # 50 character URL limit of the EU Digital Product Passport registry.
@@ -278,6 +284,25 @@ class Oydid
278
284
  else
279
285
  return [nil, nil, nil, "CMSM accepts at most two public keys"]
280
286
  end
287
+
288
+ # Reject a reused document key before the client signs anything.
289
+ # The repository enforces the rule when the document is written,
290
+ # but a CMSM flow only writes after three signatures - with a
291
+ # secure element that is three round trips to hardware for a
292
+ # request that cannot succeed. A repository that does not know
293
+ # the endpoint answers 404 and the flow continues as before.
294
+ #
295
+ # Only for create: a non-rotating UPDATE legitimately carries
296
+ # the document key of the version it replaces, and that is the
297
+ # most common form of update.
298
+ #
299
+ # skip_publish means the caller is the repository itself: its own
300
+ # database, not the one at doc_location, decides - it checks in
301
+ # its CMSM controller before the flow starts.
302
+ if mode.to_s == "create" && !options[:skip_publish] &&
303
+ key_in_active_use?(publicKey, write_location(options), options)
304
+ return [nil, nil, nil, KEY_IN_USE_ERROR]
305
+ end
281
306
  else
282
307
  # continue a persisted flow: the request carries the session and
283
308
  # exactly one new signature, which fills the next open slot
@@ -731,7 +756,7 @@ class Oydid
731
756
  headers: { 'Content-Type' => 'application/json' },
732
757
  body: did_data.to_json )
733
758
  if retVal.code != 200
734
- err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc"
759
+ err_msg = http_error_message(retVal, doc_location.to_s + "/doc")
735
760
  return [false, err_msg]
736
761
  end
737
762
  else
@@ -894,7 +919,7 @@ class Oydid
894
919
  headers: { 'Content-Type' => 'application/json' },
895
920
  body: my_body.to_json )
896
921
  if retVal.code != 200
897
- err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/cmsm"
922
+ err_msg = http_error_message(retVal, doc_location.to_s + "/cmsm")
898
923
  return [nil, err_msg]
899
924
  end
900
925
  else
@@ -1084,7 +1109,7 @@ class Oydid
1084
1109
  body: {"log": log}.to_json )
1085
1110
  code = retVal.code rescue 500
1086
1111
  if code != 200
1087
- err_msg = retVal.parsed_response["error"].to_s rescue "invalid response from " + source_location.to_s + "/log"
1112
+ err_msg = http_error_message(retVal, source_location.to_s + "/log")
1088
1113
  return ["", err_msg]
1089
1114
  end
1090
1115
  log_hash = retVal.parsed_response["log"] rescue ""
@@ -1306,7 +1331,7 @@ class Oydid
1306
1331
  headers: { 'Content-Type' => 'application/json' },
1307
1332
  body: {"log": revoc_log}.to_json )
1308
1333
  if retVal.code != 200
1309
- msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/log/" + did_hash.to_s
1334
+ msg = http_error_message(retVal, doc_location.to_s + "/log/" + did_hash.to_s)
1310
1335
  return [nil, msg]
1311
1336
  end
1312
1337
  else
data/spec/oydid_spec.rb CHANGED
@@ -701,6 +701,39 @@ describe "OYDID handling" do
701
701
  expect(msg).to start_with("invalid response from")
702
702
  expect(Oydid.upstream_error?(msg)).to be false
703
703
  end
704
+
705
+ # Only the reading paths used the helper. The writing ones built their
706
+ # message with parsed_response("error") - round brackets, so an
707
+ # ArgumentError, so the surrounding rescue swallowed whatever the
708
+ # repository had said. A rejected create then read as "invalid response
709
+ # from .../doc" and the actual reason never reached the caller.
710
+ it "passes the repository's error through when writing a DID" do
711
+ stub_request(:post, "https://oydid.ownyourdata.eu/doc").to_return(
712
+ status: 400,
713
+ headers: { "Content-Type" => "application/json" },
714
+ body: { "error" => "public key already controls an active DID" }.to_json)
715
+
716
+ retVal, msg = Oydid.create({ "id" => "spec" },
717
+ { doc_pwd: "spec-doc-pwd", rev_pwd: "spec-rev-pwd",
718
+ location: "https://oydid.ownyourdata.eu",
719
+ digest: "sha2-256", encode: "base58btc" })
720
+
721
+ expect(retVal).to be_nil
722
+ expect(msg).to eq("public key already controls an active DID")
723
+ end
724
+
725
+ it "marks a 5xx while writing as an upstream error" do
726
+ stub_request(:post, "https://oydid.ownyourdata.eu/doc").to_return(status: 502, body: "")
727
+
728
+ retVal, msg = Oydid.create({ "id" => "spec" },
729
+ { doc_pwd: "spec-doc-pwd", rev_pwd: "spec-rev-pwd",
730
+ location: "https://oydid.ownyourdata.eu",
731
+ digest: "sha2-256", encode: "base58btc" })
732
+
733
+ expect(retVal).to be_nil
734
+ expect(Oydid.upstream_error?(msg)).to be true
735
+ expect(msg).to include("502")
736
+ end
704
737
  end
705
738
 
706
739
  # main functionds
@@ -1114,6 +1147,79 @@ describe "OYDID handling" do
1114
1147
  status.transform_keys(&:to_s)
1115
1148
  end
1116
1149
 
1150
+ # The write path rejects a reused document key, but a CMSM flow only writes
1151
+ # after three signatures. With a secure element those are three round trips
1152
+ # to hardware for a request that cannot succeed, so phase 1 asks first.
1153
+ describe "document key already in use" do
1154
+ let(:key_url) { "https://oydid.ownyourdata.eu/key/" + pub }
1155
+
1156
+ def remote_options(extra = {})
1157
+ cmsm_options({ skip_publish: false,
1158
+ location: "https://oydid.ownyourdata.eu" }.merge(extra))
1159
+ end
1160
+
1161
+ it "stops phase 1 when the repository reports the key as active" do
1162
+ stub_request(:get, key_url).to_return(
1163
+ status: 200,
1164
+ headers: { "Content-Type" => "application/json" },
1165
+ body: { "active" => true, "did" => "zQmSpec" }.to_json)
1166
+
1167
+ status, msg = Oydid.create({ "key" => pub }, remote_options)
1168
+
1169
+ expect(status).to be_nil
1170
+ expect(msg).to eq(Oydid::KEY_IN_USE_ERROR)
1171
+ end
1172
+
1173
+ it "continues when the key controls only revoked DIDs" do
1174
+ stub_request(:get, key_url).to_return(
1175
+ status: 200,
1176
+ headers: { "Content-Type" => "application/json" },
1177
+ body: { "active" => false }.to_json)
1178
+
1179
+ status, msg = Oydid.create({ "key" => pub }, remote_options)
1180
+
1181
+ expect(msg).to eq("cmsm")
1182
+ expect(status.transform_keys(&:to_s)["with"]).to eq("key-doc")
1183
+ end
1184
+
1185
+ # A repository that predates the endpoint answers 404. That must not stop
1186
+ # a create - the write path still enforces the rule.
1187
+ it "continues when the repository does not know the endpoint" do
1188
+ stub_request(:get, key_url).to_return(status: 404, body: "")
1189
+
1190
+ status, msg = Oydid.create({ "key" => pub }, remote_options)
1191
+
1192
+ expect(msg).to eq("cmsm")
1193
+ end
1194
+
1195
+ # A non-rotating update legitimately carries the document key of the
1196
+ # version it replaces - the most common form of update. The guardrail is
1197
+ # about create only, so the update path must not even ask.
1198
+ it "asks nobody on update" do
1199
+ stub_request(:get, key_url).to_return(
1200
+ status: 200,
1201
+ headers: { "Content-Type" => "application/json" },
1202
+ body: { "active" => true }.to_json)
1203
+ stub_request(:get, %r{\Ahttps://oydid\.ownyourdata\.eu/doc/}).to_return(status: 404, body: "")
1204
+
1205
+ status, msg = Oydid.update({ "key" => pub }, "did:oyd:zQmSpecUnknownDid", remote_options)
1206
+
1207
+ expect(status).to be_nil
1208
+ expect(msg).not_to eq(Oydid::KEY_IN_USE_ERROR)
1209
+ expect(a_request(:get, key_url)).not_to have_been_made
1210
+ end
1211
+
1212
+ # skip_publish means the repository itself is calling: its own database
1213
+ # decides, and asking the public repository about a local key would be
1214
+ # both wrong and a needless request.
1215
+ it "asks nobody when the caller stores the DID itself" do
1216
+ status, msg = Oydid.create({ "key" => pub }, cmsm_options)
1217
+
1218
+ expect(msg).to eq("cmsm")
1219
+ expect(a_request(:get, %r{/key/})).not_to have_been_made
1220
+ end
1221
+ end
1222
+
1117
1223
  describe "cmsm_verify_signature" do
1118
1224
  it "accepts a signature made with the named key" do
1119
1225
  signature = Oydid.sign("hello", priv, {}).first
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.9.6
4
+ version: 0.9.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christoph Fabianek
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-04 00:00:00.000000000 Z
10
+ date: 2026-09-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: simple_dag