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 +4 -4
- data/VERSION +1 -1
- data/lib/oydid/basic.rb +23 -0
- data/lib/oydid/vc.rb +4 -4
- data/lib/oydid.rb +29 -4
- data/spec/oydid_spec.rb +106 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e80efb72fffc8f0a7e8ec1abb8a28376dad2f3463f581bd97719044d32f1f806
|
|
4
|
+
data.tar.gz: ac2fad7ca40541d40ec8b616704bc2225e4ebe144641805347fc7716682cdc8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0886bb19bc142e297bc454117fec9c904e4ad4ad9b8fec49e359c1824333af1fee5a086dfb05788415eadfc7cb1e16a720ec7bca35bcd58dce2799aded1d4f8
|
|
7
|
+
data.tar.gz: 1e6d067d6f0c7fcf0a824965b253e4f98108efbb7356775e25a46954c3d37e94bf65c34d9295127fd6c61be9a2bf010e09a997f8c25524b6255f67b49621069e
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
10
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: simple_dag
|