openbadgeslib 3.4.2__tar.gz → 3.5.0__tar.gz
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.
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/Changelog.txt +26 -0
- {openbadgeslib-3.4.2/openbadgeslib.egg-info → openbadgeslib-3.5.0}/PKG-INFO +5 -3
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/README.md +4 -2
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/keys.py +26 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/eudi.py +28 -14
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/status_registry.py +56 -1
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/openbadges_publish.py +15 -9
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/openbadges_signer.py +10 -8
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/util.py +1 -1
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0/openbadgeslib.egg-info}/PKG-INFO +5 -3
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/conftest.py +27 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_eudi_sd_jwt.py +26 -1
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_status_registry.py +49 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/LICENSE.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/MANIFEST.in +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/docs/README.md +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/__init__.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/_jws/__init__.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/badge.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/baking.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/config.ini.example +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/confparser.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/mail.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob1/__init__.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob1/badge.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob1/verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob2/models.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob2/verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/__init__.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/__init__.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/credential.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/did.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/ldp.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/signer.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/status.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/status_list.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/openbadges_keygenerator.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/openbadges_verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/signer.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib.egg-info/SOURCES.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib.egg-info/requires.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/pyproject.toml +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/setup.cfg +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/config1.ini +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/conformance/README.md +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/runtests.sh +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_cli_json.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_cli_smoke.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_confparser.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_docs.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_jws.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_key_operation.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob1_badge_io.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob1_verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob2_verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_credential.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_did.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_ldp_sign.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_publish_cli.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_status.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_ob3_verifier.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_util.py +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,32 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v3.5.0 - 2026-07-07
|
|
8
|
+
|
|
9
|
+
- fix(status): serialise status-registry writes with an exclusive
|
|
10
|
+
inter-process lock. Concurrent `openbadges-signer` runs (or a signer
|
|
11
|
+
overlapping `openbadges-publish --revoke`) each did load→mutate→save with
|
|
12
|
+
no lock, so the second `save()` clobbered the first's new entry — and a
|
|
13
|
+
delivered credential missing from the registry can never be revoked. Both
|
|
14
|
+
mutation paths now run inside the new `StatusRegistry.locked(path)` context
|
|
15
|
+
manager, which holds a POSIX `fcntl` lock on a sibling `<registry>.lock`
|
|
16
|
+
file across the whole load→mutate→save (a dedicated lock file, because
|
|
17
|
+
`save()` renames the JSON and a lock on its inode would be orphaned). The
|
|
18
|
+
revoke path searches unlocked and reloads the winning registry under the
|
|
19
|
+
lock before mutating. Where `fcntl` is unavailable (non-POSIX) it degrades
|
|
20
|
+
to the previous unlocked behaviour, so single-operator use is unchanged.
|
|
21
|
+
|
|
22
|
+
- feat(eudi): the SD-JWT VC track now also signs with NIST **P-384**
|
|
23
|
+
(ES384) keys, not only Ed25519 and P-256. `issue_badge_sd_jwt` reads the
|
|
24
|
+
ECDSA curve from the key and picks openvc-core's matching backend
|
|
25
|
+
(`P256SigningKey`/`P384SigningKey`), stamping an `alg: ES384` issuer JWT
|
|
26
|
+
for a P-384 PEM; verification was already curve-agnostic. Other curves
|
|
27
|
+
(e.g. P-521) and RSA are still rejected with an actionable message. New
|
|
28
|
+
`openbadgeslib.keys.ec_curve_from_pem` helper. Note: HAIP profiles the
|
|
29
|
+
P-256 family, so P-384 badges may be refused by HAIP-strict EUDI wallets
|
|
30
|
+
— P-256 remains the recommended default.
|
|
31
|
+
|
|
32
|
+
|
|
7
33
|
* v3.4.2 - 2026-07-07
|
|
8
34
|
|
|
9
35
|
- build(eudi): the `[eudi]` extra now tracks `openvc-core>=1.8,<2` (was
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Production-ready Python library & CLI for Open Badges 3.0 (W3C Verifiable Credentials): issue, verify, and revoke JWT-VC credentials with Bitstring Status Lists and did:web — plus strict OB 2.0 and legacy OB 1.0.
|
|
5
5
|
Author-email: Luis González Fernández <luisgf@luisgf.es>, Jesús Cea Avión <jcea@jcea.es>
|
|
6
6
|
License: LGPLv3
|
|
@@ -111,7 +111,7 @@ additive EUDI SD-JWT VC wallet track.
|
|
|
111
111
|
| Issue / verify | ✅ both | verify only | issue; partial verify | both | both (VC + VP) |
|
|
112
112
|
| OB 3.0 proof: VC-JWT (JOSE) | ✅ RS/ES/EdDSA | — | — | roadmap | ✅ not OB-aware |
|
|
113
113
|
| OB 3.0 proof: Data Integrity / LDP | ✅ issue + verify (`eddsa-rdfc-2022`) | — | issue only (Ed25519) | home-grown, non-conformant | older suites; no `rdfc-2022` in wheel |
|
|
114
|
-
| OB 3.0 as EUDI SD-JWT VC (selective disclosure) | ✅ issue + verify (Ed25519/P-256, `[eudi]` extra) | — | — | — | — |
|
|
114
|
+
| OB 3.0 as EUDI SD-JWT VC (selective disclosure) | ✅ issue + verify (Ed25519/P-256/P-384, `[eudi]` extra) | — | — | — | — |
|
|
115
115
|
| Revocation / suspension | ✅ W3C Bitstring Status List | hosted check | `1EdTechRevocationList` | ad-hoc flag | — |
|
|
116
116
|
| `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
|
|
117
117
|
| Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
|
|
@@ -306,7 +306,9 @@ crypto to the generic [`openvc-core`](https://pypi.org/project/openvc-core/)
|
|
|
306
306
|
library. It does not touch the native VC-JWT / Data Integrity issuance above:
|
|
307
307
|
it is a separate credential format for wallet flows, not a third image proof.
|
|
308
308
|
Needs the `[eudi]` extra (`pip install "openbadgeslib[eudi]"`) and an Ed25519
|
|
309
|
-
(EdDSA)
|
|
309
|
+
(EdDSA), NIST P-256 (ES256) or P-384 (ES384) key — SD-JWT's algorithm set (RSA
|
|
310
|
+
is rejected). HAIP profiles the P-256 family, so P-256 stays the safe default
|
|
311
|
+
for EUDI wallets; P-384 may be refused by HAIP-strict verifiers.
|
|
310
312
|
The achievement is always disclosed; the recipient identity is *selectively
|
|
311
313
|
disclosable*, so a holder can prove the badge while withholding who they are.
|
|
312
314
|
|
|
@@ -60,7 +60,7 @@ additive EUDI SD-JWT VC wallet track.
|
|
|
60
60
|
| Issue / verify | ✅ both | verify only | issue; partial verify | both | both (VC + VP) |
|
|
61
61
|
| OB 3.0 proof: VC-JWT (JOSE) | ✅ RS/ES/EdDSA | — | — | roadmap | ✅ not OB-aware |
|
|
62
62
|
| OB 3.0 proof: Data Integrity / LDP | ✅ issue + verify (`eddsa-rdfc-2022`) | — | issue only (Ed25519) | home-grown, non-conformant | older suites; no `rdfc-2022` in wheel |
|
|
63
|
-
| OB 3.0 as EUDI SD-JWT VC (selective disclosure) | ✅ issue + verify (Ed25519/P-256, `[eudi]` extra) | — | — | — | — |
|
|
63
|
+
| OB 3.0 as EUDI SD-JWT VC (selective disclosure) | ✅ issue + verify (Ed25519/P-256/P-384, `[eudi]` extra) | — | — | — | — |
|
|
64
64
|
| Revocation / suspension | ✅ W3C Bitstring Status List | hosted check | `1EdTechRevocationList` | ad-hoc flag | — |
|
|
65
65
|
| `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
|
|
66
66
|
| Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
|
|
@@ -255,7 +255,9 @@ crypto to the generic [`openvc-core`](https://pypi.org/project/openvc-core/)
|
|
|
255
255
|
library. It does not touch the native VC-JWT / Data Integrity issuance above:
|
|
256
256
|
it is a separate credential format for wallet flows, not a third image proof.
|
|
257
257
|
Needs the `[eudi]` extra (`pip install "openbadgeslib[eudi]"`) and an Ed25519
|
|
258
|
-
(EdDSA)
|
|
258
|
+
(EdDSA), NIST P-256 (ES256) or P-384 (ES384) key — SD-JWT's algorithm set (RSA
|
|
259
|
+
is rejected). HAIP profiles the P-256 family, so P-256 stays the safe default
|
|
260
|
+
for EUDI wallets; P-384 may be refused by HAIP-strict verifiers.
|
|
259
261
|
The achievement is always disclosed; the recipient identity is *selectively
|
|
260
262
|
disclosable*, so a holder can prove the badge while withholding who they are.
|
|
261
263
|
|
|
@@ -253,6 +253,32 @@ def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict:
|
|
|
253
253
|
return json.loads(jwk_json)
|
|
254
254
|
|
|
255
255
|
|
|
256
|
+
def ec_curve_from_pem(pem_data: Union[str, bytes]) -> Optional[str]:
|
|
257
|
+
"""Return the NIST curve name of an elliptic-curve key PEM, else ``None``.
|
|
258
|
+
|
|
259
|
+
The name is ``cryptography``'s (``'secp256r1'`` for P-256, ``'secp384r1'``
|
|
260
|
+
for P-384, …); ``None`` if the PEM is not an EC key (RSA, Ed25519 or
|
|
261
|
+
unreadable). Accepts a public or private PEM. The SD-JWT VC (EUDI) track
|
|
262
|
+
uses this to pick the ES256 vs ES384 signing backend — the ECDSA curve
|
|
263
|
+
fixes the JOSE algorithm.
|
|
264
|
+
"""
|
|
265
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
266
|
+
data = _pem_bytes(pem_data)
|
|
267
|
+
for load in (
|
|
268
|
+
lambda: _crypto_serialization.load_pem_public_key(data),
|
|
269
|
+
lambda: _crypto_serialization.load_pem_private_key(data, password=None),
|
|
270
|
+
):
|
|
271
|
+
try:
|
|
272
|
+
key = load()
|
|
273
|
+
except Exception:
|
|
274
|
+
continue
|
|
275
|
+
if isinstance(key, (ec.EllipticCurvePublicKey,
|
|
276
|
+
ec.EllipticCurvePrivateKey)):
|
|
277
|
+
return key.curve.name
|
|
278
|
+
return None # readable, but not EC
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
256
282
|
def _is_ed25519_pem(pem_data: Union[str, bytes]) -> bool:
|
|
257
283
|
"""True if pem_data is an Ed25519 public or private key.
|
|
258
284
|
|
|
@@ -31,14 +31,15 @@
|
|
|
31
31
|
# withholding their identity.
|
|
32
32
|
#
|
|
33
33
|
# Requires the optional ``[eudi]`` extra (``pip install openbadgeslib[eudi]``),
|
|
34
|
-
# which pulls ``openvc-core``. SD-JWT allows only Ed25519 (EdDSA) and NIST
|
|
35
|
-
# (ES256)
|
|
34
|
+
# which pulls ``openvc-core``. SD-JWT allows only Ed25519 (EdDSA) and the NIST
|
|
35
|
+
# curves P-256 (ES256) / P-384 (ES384) — RSA is not in its algorithm set.
|
|
36
36
|
|
|
37
37
|
from typing import Any, Iterable, Optional
|
|
38
38
|
|
|
39
39
|
from .credential import OpenBadgeCredential
|
|
40
40
|
from ..errors import LibOpenBadgesException
|
|
41
|
-
from ..keys import
|
|
41
|
+
from ..keys import (
|
|
42
|
+
KeyType, detect_key_type, ec_curve_from_pem, public_jwk_from_pem)
|
|
42
43
|
|
|
43
44
|
# A vct (Verifiable Credential Type) for Open Badges expressed as SD-JWT VC.
|
|
44
45
|
OB3_SD_JWT_VCT = "https://purl.imsglobal.org/spec/ob/v3p0#OpenBadgeCredential"
|
|
@@ -58,24 +59,37 @@ class EudiError(LibOpenBadgesException):
|
|
|
58
59
|
def _require_openvc() -> Any:
|
|
59
60
|
"""Import the openvc-core pieces, or raise with an actionable hint."""
|
|
60
61
|
try:
|
|
61
|
-
from openvc.keys import
|
|
62
|
+
from openvc.keys import (
|
|
63
|
+
Ed25519SigningKey, P256SigningKey, P384SigningKey)
|
|
62
64
|
from openvc.proof.sd_jwt import SdJwtVcProofSuite
|
|
63
65
|
except ImportError as exc:
|
|
64
66
|
raise EudiError(_INSTALL_HINT) from exc
|
|
65
|
-
return Ed25519SigningKey, P256SigningKey, SdJwtVcProofSuite
|
|
67
|
+
return Ed25519SigningKey, P256SigningKey, P384SigningKey, SdJwtVcProofSuite
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
def _signing_key(privkey_pem: Any, kid: str) -> Any:
|
|
69
|
-
"""Build the openvc SigningKey matching the PEM's key type
|
|
70
|
-
|
|
71
|
+
"""Build the openvc SigningKey matching the PEM's key type / curve.
|
|
72
|
+
|
|
73
|
+
Ed25519 -> EdDSA; NIST P-256 -> ES256; NIST P-384 -> ES384. The ECDSA
|
|
74
|
+
curve is read from the key itself, since it fixes the JOSE algorithm.
|
|
75
|
+
"""
|
|
76
|
+
Ed25519SigningKey, P256SigningKey, P384SigningKey, _ = _require_openvc()
|
|
71
77
|
key_type = detect_key_type(privkey_pem)
|
|
72
78
|
if key_type is KeyType.ED25519:
|
|
73
79
|
return Ed25519SigningKey.from_pem(privkey_pem, kid=kid)
|
|
74
|
-
if key_type is KeyType.ECC: # NIST
|
|
75
|
-
|
|
80
|
+
if key_type is KeyType.ECC: # NIST curve -> ES256/ES384
|
|
81
|
+
curve = ec_curve_from_pem(privkey_pem)
|
|
82
|
+
if curve == "secp256r1": # P-256 -> ES256
|
|
83
|
+
return P256SigningKey.from_pem(privkey_pem, kid=kid)
|
|
84
|
+
if curve == "secp384r1": # P-384 -> ES384
|
|
85
|
+
return P384SigningKey.from_pem(privkey_pem, kid=kid)
|
|
86
|
+
raise EudiError(
|
|
87
|
+
"SD-JWT VC over ECDSA needs a NIST P-256 (ES256) or P-384 (ES384) "
|
|
88
|
+
"key; got curve %r." % curve)
|
|
76
89
|
raise EudiError(
|
|
77
|
-
"SD-JWT VC allows only Ed25519 (EdDSA) or NIST P-256 (ES256
|
|
78
|
-
"%s (RSA is not in the SD-JWT algorithm set)."
|
|
90
|
+
"SD-JWT VC allows only Ed25519 (EdDSA) or NIST P-256/P-384 (ES256/"
|
|
91
|
+
"ES384) keys; got %s (RSA is not in the SD-JWT algorithm set)."
|
|
92
|
+
% key_type.value)
|
|
79
93
|
|
|
80
94
|
|
|
81
95
|
def badge_to_sd_jwt_claims(credential: OpenBadgeCredential) -> dict:
|
|
@@ -117,9 +131,9 @@ def issue_badge_sd_jwt(
|
|
|
117
131
|
Returns the compact SD-JWT (``<issuer-jwt>~<disclosure>~…``). Only claims in
|
|
118
132
|
*disclosable* that are actually present are made selectively disclosable.
|
|
119
133
|
Pass *holder_jwk* to bind the credential to a holder key (``cnf``) for a later
|
|
120
|
-
Key-Binding presentation. Ed25519
|
|
134
|
+
Key-Binding presentation. Ed25519, P-256 or P-384 keys only.
|
|
121
135
|
"""
|
|
122
|
-
_, _, SdJwtVcProofSuite = _require_openvc()
|
|
136
|
+
_, _, _, SdJwtVcProofSuite = _require_openvc()
|
|
123
137
|
signing_key = _signing_key(privkey_pem, kid or ("%s#key-1" % credential.issuer.id))
|
|
124
138
|
claims = badge_to_sd_jwt_claims(credential)
|
|
125
139
|
present = [name for name in disclosable if name in claims]
|
|
@@ -149,7 +163,7 @@ def verify_badge_sd_jwt(
|
|
|
149
163
|
Pass *audience*/*nonce* (and ``require_key_binding=True``) to check a Key
|
|
150
164
|
Binding JWT from a holder presentation.
|
|
151
165
|
"""
|
|
152
|
-
_, _, SdJwtVcProofSuite = _require_openvc()
|
|
166
|
+
_, _, _, SdJwtVcProofSuite = _require_openvc()
|
|
153
167
|
try:
|
|
154
168
|
public_key_jwk = public_jwk_from_pem(pubkey_pem)
|
|
155
169
|
return SdJwtVcProofSuite().verify(
|
|
@@ -32,14 +32,20 @@
|
|
|
32
32
|
# leave a truncated registry behind, because a lost registry makes every
|
|
33
33
|
# outstanding credential unrevocable.
|
|
34
34
|
|
|
35
|
+
import contextlib
|
|
35
36
|
import json
|
|
36
37
|
import os
|
|
37
38
|
import secrets
|
|
38
39
|
import tempfile
|
|
39
40
|
|
|
41
|
+
try:
|
|
42
|
+
import fcntl # POSIX advisory file locking
|
|
43
|
+
except ImportError: # pragma: no cover - non-POSIX (e.g. Windows)
|
|
44
|
+
fcntl = None # type: ignore[assignment]
|
|
45
|
+
|
|
40
46
|
from dataclasses import dataclass
|
|
41
47
|
from datetime import datetime, timezone
|
|
42
|
-
from typing import Dict, List, Optional, Set
|
|
48
|
+
from typing import Dict, Iterator, List, Optional, Set
|
|
43
49
|
|
|
44
50
|
from ..errors import (
|
|
45
51
|
AlreadyRevoked,
|
|
@@ -59,6 +65,35 @@ _SCHEMA_VERSION = 1
|
|
|
59
65
|
_MAX_RANDOM_TRIES = 1000
|
|
60
66
|
|
|
61
67
|
|
|
68
|
+
@contextlib.contextmanager
|
|
69
|
+
def _exclusive_file_lock(lock_path: str) -> Iterator[None]:
|
|
70
|
+
"""Hold an exclusive inter-process lock for the duration of the block.
|
|
71
|
+
|
|
72
|
+
Backed by POSIX ``fcntl.flock`` on a dedicated lock file. Where ``fcntl``
|
|
73
|
+
is unavailable (non-POSIX platforms) it degrades to no locking — the same
|
|
74
|
+
behaviour as before this guard existed — so single-process use is
|
|
75
|
+
unaffected and nothing regresses; concurrent writers are simply not
|
|
76
|
+
serialised there.
|
|
77
|
+
"""
|
|
78
|
+
directory = os.path.dirname(lock_path) or '.'
|
|
79
|
+
umask = os.umask(0o077)
|
|
80
|
+
try:
|
|
81
|
+
os.makedirs(directory, exist_ok=True)
|
|
82
|
+
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
|
|
83
|
+
finally:
|
|
84
|
+
os.umask(umask)
|
|
85
|
+
try:
|
|
86
|
+
if fcntl is not None:
|
|
87
|
+
fcntl.flock(fd, fcntl.LOCK_EX) # blocks until the lock frees
|
|
88
|
+
yield
|
|
89
|
+
finally:
|
|
90
|
+
try:
|
|
91
|
+
if fcntl is not None:
|
|
92
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
93
|
+
finally:
|
|
94
|
+
os.close(fd)
|
|
95
|
+
|
|
96
|
+
|
|
62
97
|
@dataclass
|
|
63
98
|
class StatusEvent:
|
|
64
99
|
"""A revocation or suspension, with when and (optionally) why."""
|
|
@@ -148,6 +183,26 @@ class StatusRegistry:
|
|
|
148
183
|
raise RegistryCorrupt('%s: %s' % (path, exc)) from exc
|
|
149
184
|
return registry
|
|
150
185
|
|
|
186
|
+
@classmethod
|
|
187
|
+
@contextlib.contextmanager
|
|
188
|
+
def locked(cls, path: str,
|
|
189
|
+
size_bits: int = DEFAULT_SIZE_BITS) -> Iterator['StatusRegistry']:
|
|
190
|
+
"""Load the registry under an exclusive inter-process lock, yield it.
|
|
191
|
+
|
|
192
|
+
The lock (a sibling ``<path>.lock`` file) is held for the whole block,
|
|
193
|
+
so a ``load → allocate/revoke/… → save()`` sequence inside it is
|
|
194
|
+
atomic against other processes. This closes the race where two
|
|
195
|
+
concurrent writers both load, and the second :meth:`save` clobbers the
|
|
196
|
+
first's new entry — leaving a delivered credential unrevocable. Call
|
|
197
|
+
:meth:`save` inside the block; the lock releases on exit.
|
|
198
|
+
|
|
199
|
+
A dedicated lock file (not the registry JSON) is used on purpose:
|
|
200
|
+
:meth:`save` replaces the JSON via ``os.rename``, so a lock held on the
|
|
201
|
+
JSON inode would be orphaned by the first write.
|
|
202
|
+
"""
|
|
203
|
+
with _exclusive_file_lock(path + '.lock'):
|
|
204
|
+
yield cls.load(path, size_bits)
|
|
205
|
+
|
|
151
206
|
def save(self) -> None:
|
|
152
207
|
"""Write the registry atomically (temp file + rename) under a
|
|
153
208
|
restrictive umask; the file names recipients and is issuer-private."""
|
|
@@ -200,19 +200,25 @@ def _publish_ob3(args: argparse.Namespace, parser: argparse.ArgumentParser) -> N
|
|
|
200
200
|
sys.exit(-1)
|
|
201
201
|
|
|
202
202
|
name, registry, entry = matches[0]
|
|
203
|
+
jti = entry.jti
|
|
203
204
|
now = datetime.now(tz=timezone.utc)
|
|
205
|
+
# The search above is an unlocked best-effort locator; the mutation
|
|
206
|
+
# reloads the winning registry under an exclusive lock so it is atomic
|
|
207
|
+
# against a concurrent signer or revoke (see StatusRegistry.locked).
|
|
204
208
|
try:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
with StatusRegistry.locked(registry.path,
|
|
210
|
+
registry.size_bits) as registry:
|
|
211
|
+
if op == 'revoke':
|
|
212
|
+
registry.revoke(jti, now, args.reason)
|
|
213
|
+
elif op == 'suspend':
|
|
214
|
+
registry.suspend(jti, now, args.reason)
|
|
215
|
+
else:
|
|
216
|
+
registry.unsuspend(jti)
|
|
217
|
+
registry.save()
|
|
218
|
+
except (StatusError, OSError) as exc:
|
|
213
219
|
print('[!] %s' % exc)
|
|
214
220
|
sys.exit(-1)
|
|
215
|
-
print('[+] %s %s %s (index %d)' % (verb, name,
|
|
221
|
+
print('[+] %s %s %s (index %d)' % (verb, name, jti, entry.index))
|
|
216
222
|
|
|
217
223
|
# ── regenerate every managed artefact from the registries ───────────────
|
|
218
224
|
publish_url = conf['issuer']['publish_url']
|
|
@@ -348,15 +348,17 @@ def _sign_ob3(args: argparse.Namespace, conf: configparser.ConfigParser, badge:
|
|
|
348
348
|
|
|
349
349
|
# The registry is persisted BEFORE the badge is signed and written:
|
|
350
350
|
# a signing failure leaves a harmless orphan index, while a delivered
|
|
351
|
-
# badge missing from the registry could never be revoked.
|
|
351
|
+
# badge missing from the registry could never be revoked. The whole
|
|
352
|
+
# load→allocate→save runs under an exclusive lock so a concurrent
|
|
353
|
+
# signer cannot clobber this allocation (see StatusRegistry.locked).
|
|
352
354
|
try:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
355
|
+
with StatusRegistry.locked(status_conf.registry_path,
|
|
356
|
+
status_conf.size_bits) as registry:
|
|
357
|
+
assert credential.id is not None \
|
|
358
|
+
and credential.issuance_date is not None
|
|
359
|
+
status_index = registry.allocate(credential.id, recipient_id,
|
|
360
|
+
credential.issuance_date)
|
|
361
|
+
registry.save()
|
|
360
362
|
except (StatusError, OSError) as exc:
|
|
361
363
|
print('[!] Could not allocate a status list index: %s' % exc)
|
|
362
364
|
sys.exit(-1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Production-ready Python library & CLI for Open Badges 3.0 (W3C Verifiable Credentials): issue, verify, and revoke JWT-VC credentials with Bitstring Status Lists and did:web — plus strict OB 2.0 and legacy OB 1.0.
|
|
5
5
|
Author-email: Luis González Fernández <luisgf@luisgf.es>, Jesús Cea Avión <jcea@jcea.es>
|
|
6
6
|
License: LGPLv3
|
|
@@ -111,7 +111,7 @@ additive EUDI SD-JWT VC wallet track.
|
|
|
111
111
|
| Issue / verify | ✅ both | verify only | issue; partial verify | both | both (VC + VP) |
|
|
112
112
|
| OB 3.0 proof: VC-JWT (JOSE) | ✅ RS/ES/EdDSA | — | — | roadmap | ✅ not OB-aware |
|
|
113
113
|
| OB 3.0 proof: Data Integrity / LDP | ✅ issue + verify (`eddsa-rdfc-2022`) | — | issue only (Ed25519) | home-grown, non-conformant | older suites; no `rdfc-2022` in wheel |
|
|
114
|
-
| OB 3.0 as EUDI SD-JWT VC (selective disclosure) | ✅ issue + verify (Ed25519/P-256, `[eudi]` extra) | — | — | — | — |
|
|
114
|
+
| OB 3.0 as EUDI SD-JWT VC (selective disclosure) | ✅ issue + verify (Ed25519/P-256/P-384, `[eudi]` extra) | — | — | — | — |
|
|
115
115
|
| Revocation / suspension | ✅ W3C Bitstring Status List | hosted check | `1EdTechRevocationList` | ad-hoc flag | — |
|
|
116
116
|
| `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
|
|
117
117
|
| Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
|
|
@@ -306,7 +306,9 @@ crypto to the generic [`openvc-core`](https://pypi.org/project/openvc-core/)
|
|
|
306
306
|
library. It does not touch the native VC-JWT / Data Integrity issuance above:
|
|
307
307
|
it is a separate credential format for wallet flows, not a third image proof.
|
|
308
308
|
Needs the `[eudi]` extra (`pip install "openbadgeslib[eudi]"`) and an Ed25519
|
|
309
|
-
(EdDSA)
|
|
309
|
+
(EdDSA), NIST P-256 (ES256) or P-384 (ES384) key — SD-JWT's algorithm set (RSA
|
|
310
|
+
is rejected). HAIP profiles the P-256 family, so P-256 stays the safe default
|
|
311
|
+
for EUDI wallets; P-384 may be refused by HAIP-strict verifiers.
|
|
310
312
|
The achievement is always disclosed; the recipient identity is *selectively
|
|
311
313
|
disclosable*, so a holder can prove the badge while withholding who they are.
|
|
312
314
|
|
|
@@ -39,6 +39,33 @@ def ecc_pub_pem():
|
|
|
39
39
|
return (TESTS_DIR / 'test_verify_ecc.pem').read_bytes()
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
@pytest.fixture(scope='session')
|
|
43
|
+
def p384_keypair():
|
|
44
|
+
# Generated in-session (no committed P-384 key material on disk), like the
|
|
45
|
+
# Ed25519 keypair. NIST P-384 -> ES384 on the EUDI SD-JWT track.
|
|
46
|
+
from cryptography.hazmat.primitives import serialization
|
|
47
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
48
|
+
priv = ec.generate_private_key(ec.SECP384R1())
|
|
49
|
+
priv_pem = priv.private_bytes(
|
|
50
|
+
serialization.Encoding.PEM,
|
|
51
|
+
serialization.PrivateFormat.PKCS8,
|
|
52
|
+
serialization.NoEncryption())
|
|
53
|
+
pub_pem = priv.public_key().public_bytes(
|
|
54
|
+
serialization.Encoding.PEM,
|
|
55
|
+
serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
56
|
+
return priv_pem, pub_pem
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture(scope='session')
|
|
60
|
+
def p384_priv_pem(p384_keypair):
|
|
61
|
+
return p384_keypair[0]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.fixture(scope='session')
|
|
65
|
+
def p384_pub_pem(p384_keypair):
|
|
66
|
+
return p384_keypair[1]
|
|
67
|
+
|
|
68
|
+
|
|
42
69
|
@pytest.fixture(scope='session')
|
|
43
70
|
def ed25519_keypair():
|
|
44
71
|
# Generated in-session (not a committed key file) so the suite ships no
|
|
@@ -39,6 +39,19 @@ class TestSdJwtBadge:
|
|
|
39
39
|
assert result.issuer == ob3_credential.issuer.id
|
|
40
40
|
assert result.claims["achievement"]["name"] == ob3_credential.achievement.name
|
|
41
41
|
|
|
42
|
+
def test_roundtrip_es384(self, ob3_credential, p384_priv_pem, p384_pub_pem):
|
|
43
|
+
import base64
|
|
44
|
+
import json
|
|
45
|
+
token = issue_badge_sd_jwt(ob3_credential, privkey_pem=p384_priv_pem)
|
|
46
|
+
# A P-384 key must pin the ES384 algorithm in the issuer JWT header.
|
|
47
|
+
header_b64 = token.split(".", 1)[0]
|
|
48
|
+
header = json.loads(base64.urlsafe_b64decode(header_b64 + "=="))
|
|
49
|
+
assert header["alg"] == "ES384"
|
|
50
|
+
result = verify_badge_sd_jwt(token, pubkey_pem=p384_pub_pem)
|
|
51
|
+
assert result.issuer == ob3_credential.issuer.id
|
|
52
|
+
assert result.claims["achievement"]["name"] == ob3_credential.achievement.name
|
|
53
|
+
assert result.claims["credentialSubject"]["id"] == ob3_credential.recipient_id
|
|
54
|
+
|
|
42
55
|
def test_claims_shape(self, ob3_credential):
|
|
43
56
|
claims = badge_to_sd_jwt_claims(ob3_credential)
|
|
44
57
|
assert claims["iss"] == ob3_credential.issuer.id
|
|
@@ -100,10 +113,22 @@ class TestSdJwtBadge:
|
|
|
100
113
|
expected_vct="https://example.com/other-type")
|
|
101
114
|
|
|
102
115
|
def test_rsa_key_rejected(self, ob3_credential, rsa_priv_pem):
|
|
103
|
-
# SD-JWT's algorithm set is {ES256, EdDSA}; RSA is not allowed.
|
|
116
|
+
# SD-JWT's algorithm set is {ES256, ES384, EdDSA}; RSA is not allowed.
|
|
104
117
|
with pytest.raises(EudiError, match="Ed25519|P-256|RSA"):
|
|
105
118
|
issue_badge_sd_jwt(ob3_credential, privkey_pem=rsa_priv_pem)
|
|
106
119
|
|
|
120
|
+
def test_unsupported_curve_rejected(self, ob3_credential):
|
|
121
|
+
# SD-JWT's ECDSA set is P-256/P-384; a P-521 key must be refused with a
|
|
122
|
+
# clear message rather than mis-signed under the wrong algorithm.
|
|
123
|
+
from cryptography.hazmat.primitives import serialization
|
|
124
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
125
|
+
p521 = ec.generate_private_key(ec.SECP521R1()).private_bytes(
|
|
126
|
+
serialization.Encoding.PEM,
|
|
127
|
+
serialization.PrivateFormat.PKCS8,
|
|
128
|
+
serialization.NoEncryption())
|
|
129
|
+
with pytest.raises(EudiError, match="P-256|P-384|curve"):
|
|
130
|
+
issue_badge_sd_jwt(ob3_credential, privkey_pem=p521)
|
|
131
|
+
|
|
107
132
|
|
|
108
133
|
# ── behaviour without the [eudi] extra (runs with or without openvc) ─────────
|
|
109
134
|
|
|
@@ -38,6 +38,55 @@ def _registry(tmp_path, size_bits=131072):
|
|
|
38
38
|
return StatusRegistry.load(str(tmp_path / 'badge_1.json'), size_bits)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
# ── inter-process locking ────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
def _alloc_worker(path, size_bits, jti, recipient, when):
|
|
44
|
+
# Module-level so it is importable under the 'spawn' start method. Each
|
|
45
|
+
# call is a separate process doing the full locked load→allocate→save.
|
|
46
|
+
from openbadgeslib.ob3.status_registry import StatusRegistry
|
|
47
|
+
with StatusRegistry.locked(path, size_bits) as registry:
|
|
48
|
+
registry.allocate(jti, recipient, when)
|
|
49
|
+
registry.save()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestLocked:
|
|
53
|
+
def test_yields_loaded_registry_and_creates_lock_file(self, tmp_path):
|
|
54
|
+
path = str(tmp_path / 'badge_1.json')
|
|
55
|
+
with StatusRegistry.locked(path, 131072) as registry:
|
|
56
|
+
assert registry.path == path
|
|
57
|
+
registry.allocate(JTI, 'mailto:a@example.org', NOW)
|
|
58
|
+
registry.save()
|
|
59
|
+
assert os.path.exists(path + '.lock') # dedicated lock file
|
|
60
|
+
assert JTI in StatusRegistry.load(path, 131072).entries
|
|
61
|
+
|
|
62
|
+
def test_concurrent_allocations_are_never_lost(self, tmp_path):
|
|
63
|
+
# Without the lock, N processes each load→allocate→save would clobber
|
|
64
|
+
# one another and only the last save() would survive. With it, every
|
|
65
|
+
# allocation must be present.
|
|
66
|
+
pytest.importorskip('fcntl') # POSIX-only guarantee
|
|
67
|
+
import multiprocessing as mp
|
|
68
|
+
path = str(tmp_path / 'badge_1.json')
|
|
69
|
+
bits = 131072
|
|
70
|
+
n = 6
|
|
71
|
+
ctx = mp.get_context('spawn') # deterministic on macOS
|
|
72
|
+
procs = [
|
|
73
|
+
ctx.Process(target=_alloc_worker,
|
|
74
|
+
args=(path, bits, 'urn:jti:%d' % i,
|
|
75
|
+
'mailto:u%d@example.org' % i, NOW))
|
|
76
|
+
for i in range(n)
|
|
77
|
+
]
|
|
78
|
+
for p in procs:
|
|
79
|
+
p.start()
|
|
80
|
+
for p in procs:
|
|
81
|
+
p.join(timeout=60)
|
|
82
|
+
assert p.exitcode == 0
|
|
83
|
+
|
|
84
|
+
registry = StatusRegistry.load(path, bits)
|
|
85
|
+
assert {e.jti for e in registry.entries.values()} == \
|
|
86
|
+
{'urn:jti:%d' % i for i in range(n)}
|
|
87
|
+
assert len({e.index for e in registry.entries.values()}) == n
|
|
88
|
+
|
|
89
|
+
|
|
41
90
|
# ── allocation ───────────────────────────────────────────────────────────────
|
|
42
91
|
|
|
43
92
|
class TestAllocate:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json
RENAMED
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json
RENAMED
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json
RENAMED
|
File without changes
|
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt
RENAMED
|
File without changes
|
{openbadgeslib-3.4.2 → openbadgeslib-3.5.0}/tests/fixtures/vc_di_eddsa/signed-credential.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|