openbadgeslib 3.13.0__tar.gz → 4.0.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-4.0.0/CONTRIBUTING.md +93 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/Changelog.txt +366 -0
- openbadgeslib-4.0.0/GOVERNANCE.md +72 -0
- {openbadgeslib-3.13.0/openbadgeslib.egg-info → openbadgeslib-4.0.0}/PKG-INFO +19 -5
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/README.md +12 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/__init__.py +51 -19
- openbadgeslib-4.0.0/openbadgeslib/_jsonmodel.py +86 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/__init__.py +39 -2
- openbadgeslib-4.0.0/openbadgeslib/_ob1_api.py +41 -0
- openbadgeslib-4.0.0/openbadgeslib/badge_model.py +199 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/baking.py +17 -9
- openbadgeslib-4.0.0/openbadgeslib/cli_common.py +63 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/config.ini.example +15 -0
- openbadgeslib-4.0.0/openbadgeslib/confparser.py +414 -0
- openbadgeslib-4.0.0/openbadgeslib/errors.py +221 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/issue.py +89 -47
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/keys.py +8 -33
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/mail.py +8 -2
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/__init__.py +3 -7
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/badge.py +11 -155
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/verifier.py +3 -3
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/models.py +7 -41
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/signer.py +4 -4
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/verifier.py +41 -4
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/__init__.py +15 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/__init__.py +24 -2
- openbadgeslib-4.0.0/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +9 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/credential.py +15 -43
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/did.py +40 -11
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/eudi.py +108 -7
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/ldp.py +94 -17
- openbadgeslib-4.0.0/openbadgeslib/ob3/publish.py +459 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/signer.py +41 -16
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status.py +33 -17
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status_list.py +5 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/verifier.py +47 -18
- openbadgeslib-4.0.0/openbadgeslib/openbadges_cli.py +98 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_init.py +26 -8
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_keygenerator.py +20 -28
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_publish.py +151 -327
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_signer.py +31 -25
- openbadgeslib-4.0.0/openbadgeslib/openbadges_status.py +75 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_verifier.py +122 -183
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/util.py +159 -5
- openbadgeslib-4.0.0/openbadgeslib/verify.py +295 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0/openbadgeslib.egg-info}/PKG-INFO +19 -5
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/SOURCES.txt +16 -3
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/entry_points.txt +1 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/requires.txt +8 -4
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/pyproject.toml +35 -5
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conftest.py +9 -9
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_baking.py +56 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_cli_json.py +2 -2
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_cli_smoke.py +169 -15
- openbadgeslib-4.0.0/tests/test_confparser.py +492 -0
- openbadgeslib-4.0.0/tests/test_errors.py +92 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_issue.py +60 -0
- openbadgeslib-4.0.0/tests/test_jsonmodel.py +69 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_jws.py +43 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_keys.py +3 -10
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_mail.py +13 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_badge_io.py +1 -1
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_deprecation.py +11 -11
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_signer.py +11 -12
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_verifier.py +2 -2
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_cli.py +19 -1
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_verifier.py +66 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_conformance_schema.py +4 -2
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_contexts.py +11 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_did_doc.py +50 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_endorsement.py +23 -2
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_eudi_sd_jwt.py +63 -0
- openbadgeslib-4.0.0/tests/test_ob3_eudi_x5c.py +231 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp.py +102 -5
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_ecdsa_sd.py +31 -4
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_publish_cli.py +217 -3
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_signer.py +59 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_status.py +135 -5
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_status_registry.py +30 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_verifier.py +48 -2
- openbadgeslib-4.0.0/tests/test_openbadges_cli.py +103 -0
- openbadgeslib-4.0.0/tests/test_public_api.py +78 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_signer_batch.py +17 -1
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_util.py +141 -4
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_verifier_cli_coverage.py +80 -9
- openbadgeslib-4.0.0/tests/test_verify.py +113 -0
- openbadgeslib-3.13.0/openbadgeslib/badge.py +0 -21
- openbadgeslib-3.13.0/openbadgeslib/confparser.py +0 -240
- openbadgeslib-3.13.0/openbadgeslib/errors.py +0 -137
- openbadgeslib-3.13.0/openbadgeslib/signer.py +0 -13
- openbadgeslib-3.13.0/openbadgeslib/verifier.py +0 -13
- openbadgeslib-3.13.0/tests/test_confparser.py +0 -172
- openbadgeslib-3.13.0/tests/test_ob3_eudi_x5c.py +0 -120
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/LICENSE.txt +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/MANIFEST.in +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/SECURITY.md +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/docs/README.md +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status_registry.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/setup.cfg +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/config1.ini +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/README.md +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_docs.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_examples.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_key_operation.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_error_branches.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_credential.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_did.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_eudi_type_metadata.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_errors.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_sign.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/withxmlheader.svg +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Contributing to openbadgeslib
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in improving **openbadgeslib** — a library and CLI for
|
|
4
|
+
issuing and verifying OpenBadges 2.0 and 3.0 credentials. This page is the quick
|
|
5
|
+
entry point; the full developer guide lives in the wiki
|
|
6
|
+
([Contributing](https://github.com/luisgf/openbadgeslib/wiki/Contributing),
|
|
7
|
+
[Releasing](https://github.com/luisgf/openbadgeslib/wiki/Releasing)).
|
|
8
|
+
|
|
9
|
+
## Reporting bugs and requesting capabilities
|
|
10
|
+
|
|
11
|
+
- **Bugs:** open a [Bug report](https://github.com/luisgf/openbadgeslib/issues/new/choose).
|
|
12
|
+
Include the openbadgeslib version, your Python version, and a minimal
|
|
13
|
+
reproduction.
|
|
14
|
+
- **Capabilities / features:** open a
|
|
15
|
+
[Capability request](https://github.com/luisgf/openbadgeslib/issues/new/choose).
|
|
16
|
+
This is also the **demand signal** the roadmap runs on — several planned items
|
|
17
|
+
are deliberately *built when someone asks for them with a concrete use case*,
|
|
18
|
+
so a well-described request genuinely moves them forward.
|
|
19
|
+
- **Security vulnerabilities:** do **not** open a public issue. Follow
|
|
20
|
+
[SECURITY.md](SECURITY.md) (private GitHub advisory or email).
|
|
21
|
+
|
|
22
|
+
## Development setup
|
|
23
|
+
|
|
24
|
+
Requires a supported Python (see below). Everything runs from an editable
|
|
25
|
+
virtual environment:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
git clone https://github.com/luisgf/openbadgeslib
|
|
29
|
+
cd openbadgeslib
|
|
30
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
31
|
+
pip install --upgrade pip
|
|
32
|
+
pip install -e ".[dev]" # add ,ldp and ,eudi to exercise those tracks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The checks a pull request must pass
|
|
36
|
+
|
|
37
|
+
CI runs these on Python 3.10–3.14; run them locally before opening a PR (via
|
|
38
|
+
your `.venv`):
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
flake8 openbadgeslib tests # style (max-line-length = 120, see setup.cfg)
|
|
42
|
+
mypy # types (config in pyproject.toml)
|
|
43
|
+
pytest # the suite; install [ldp]/[eudi] so their
|
|
44
|
+
# tests actually run instead of skipping
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The Data Integrity (LDP) tests need the `[ldp]` extra (pyld) and the EUDI
|
|
48
|
+
SD-JWT / X.509 tests need `[eudi]` — without them those tests silently
|
|
49
|
+
`importorskip`, so install the extras when touching those areas. Coverage is
|
|
50
|
+
enforced at a floor (currently 93%).
|
|
51
|
+
|
|
52
|
+
## Commit messages
|
|
53
|
+
|
|
54
|
+
Commits follow a [Conventional Commits](https://www.conventionalcommits.org/)
|
|
55
|
+
style so the history is machine-readable and the changelog can be drafted from
|
|
56
|
+
it — CI's `gitlint` check enforces the title format. The shape is
|
|
57
|
+
`type(optional-scope)!: imperative summary` (≤ 80 chars), with the vocabulary:
|
|
58
|
+
|
|
59
|
+
| Type | For | Changelog |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `feat` / `fix` / `security` / `perf` | user-facing change | **yes** |
|
|
62
|
+
| `docs` / `test` / `refactor` / `chore` / `ci` / `build` / `style` | internal, no user-visible effect | no |
|
|
63
|
+
| `release` | the version-bump/changelog commit | no |
|
|
64
|
+
|
|
65
|
+
A `!` (or a `BREAKING CHANGE:` body trailer) marks a breaking change. A
|
|
66
|
+
user-facing `feat`/`fix`/`security`/`perf` should add its `Changelog.txt` entry
|
|
67
|
+
in the same commit (newest-first, under a `* vX.Y.Z - unreleased` header). See
|
|
68
|
+
the wiki [Contributing](https://github.com/luisgf/openbadgeslib/wiki/Contributing)
|
|
69
|
+
page for the full convention.
|
|
70
|
+
|
|
71
|
+
## Supported Python versions
|
|
72
|
+
|
|
73
|
+
openbadgeslib supports **every CPython release that is not end-of-life**. The
|
|
74
|
+
policy is deliberately simple and predictable:
|
|
75
|
+
|
|
76
|
+
- A version is **dropped** — from `requires-python`, the classifiers, and the CI
|
|
77
|
+
matrix — at the next **major** release after it reaches end-of-life. For
|
|
78
|
+
example, Python **3.10 (EOL 2026-10)** is dropped in **4.0.0**; until then it
|
|
79
|
+
stays supported. Grouping drops into majors keeps breaking changes rare.
|
|
80
|
+
- A new CPython minor is **added** to the CI matrix once it has a stable
|
|
81
|
+
(non-pre) release, so downstreams can adopt it early.
|
|
82
|
+
- The current matrix is **3.10–3.14**, and `requires-python = ">=3.10"`.
|
|
83
|
+
|
|
84
|
+
Security fixes always target the latest release only (see
|
|
85
|
+
[SECURITY.md](SECURITY.md)).
|
|
86
|
+
|
|
87
|
+
## Governance and licensing
|
|
88
|
+
|
|
89
|
+
Project governance, the release process, and the maintenance/continuity plan are
|
|
90
|
+
in [GOVERNANCE.md](GOVERNANCE.md). The library is licensed under the LGPLv3 and
|
|
91
|
+
the CLI entry points under BSD-2-Clause (see
|
|
92
|
+
[LICENSE.txt](LICENSE.txt)); by contributing you agree your contribution is
|
|
93
|
+
provided under those same terms.
|
|
@@ -4,6 +4,372 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v4.0.0 - 2026-07-22
|
|
8
|
+
|
|
9
|
+
Grouped breaking release (#170) — freeze the declared public contract and
|
|
10
|
+
unify the CLIs — shipped together with the Round-2 roadmap work: close the
|
|
11
|
+
revocation/trust gaps the deep-logic analysis surfaced, test the fail-closed
|
|
12
|
+
guards, and speed up batch issuance and verification. Breaking changes first,
|
|
13
|
+
then the rest of the cycle.
|
|
14
|
+
|
|
15
|
+
- fix(cli)!: unify every console tool on the 0/1/2 exit-code contract (#233).
|
|
16
|
+
The 19 `sys.exit(-1)` sites (which exited 255) now exit 1, and the verifier
|
|
17
|
+
applies the same status in human mode as under --json: 1 on any failure, 2
|
|
18
|
+
when the signature is valid but the issuer is not anchored (an OB2
|
|
19
|
+
badge-embedded key or a self-asserted did:key), and 0 only when the badge is
|
|
20
|
+
valid AND trusted. A partial batch sign or publish, and a stale/missing
|
|
21
|
+
artifact under `publish --check-live`, now exit 2 in human mode too (they
|
|
22
|
+
exited 1 before). BREAKING: a wrapper that keyed on 255, or that treated a
|
|
23
|
+
valid-but-untrusted badge's exit 0 as success, must be updated.
|
|
24
|
+
- feat(cli): add a unified `openbadges <command>` front-end and centralise the
|
|
25
|
+
shared flags (#234). A new `openbadgeslib/cli_common.py` provides argparse
|
|
26
|
+
parent parsers for -c/--config, -d/--debug, --json and -v/--version, which
|
|
27
|
+
all five build_parser()s now compose — so the flags are defined once with
|
|
28
|
+
consistent help. openbadges-publish gains -d/--debug (it had none) and
|
|
29
|
+
openbadges-init gains argparse (hence -v/--version, and -h now exits 0 via
|
|
30
|
+
argparse). The new `openbadges` entry point dispatches to init/keygen/sign/
|
|
31
|
+
verify/publish, each of which stays installed under its own openbadges-<name>
|
|
32
|
+
script; a command owns everything after its name, so `openbadges sign ...`
|
|
33
|
+
behaves exactly like `openbadges-signer ...`. Minor breaking: openbadges-init
|
|
34
|
+
with a bad/missing argument now exits 2 (argparse), not 1.
|
|
35
|
+
- feat(cli): add `openbadges status [ID]`, the sixth subcommand promised by
|
|
36
|
+
the unified front-end (#170). It is the read-only registry query that until
|
|
37
|
+
now was reachable only as `openbadges-publish -V 3 --list/--status`: with no
|
|
38
|
+
ID it tabulates every issued OB3 credential and its state, with one (a jti
|
|
39
|
+
or a recipient email) it prints that credential's full record including the
|
|
40
|
+
revocation reason. Auditing a credential is a read, so it no longer requires
|
|
41
|
+
typing a publish verb, nor `-V 3` — status lists are OB3-only by
|
|
42
|
+
construction. The query itself is not duplicated: the command and publish's
|
|
43
|
+
flags both call the shared `query_ob3()`, which now takes plain arguments
|
|
44
|
+
instead of a parsed argparse Namespace. `--list`/`--status` keep working
|
|
45
|
+
unchanged. Unlike the other five commands this one has no standalone
|
|
46
|
+
`openbadges-status` script — those aliases exist only because they predate
|
|
47
|
+
the front-end.
|
|
48
|
+
- refactor(ob1)!: consolidate the OB1 compatibility surface (#235). The OB1
|
|
49
|
+
legacy name table (11 names -> leaf module) is single-sourced in
|
|
50
|
+
openbadgeslib/_ob1_api.py instead of being duplicated between the top-level
|
|
51
|
+
package and openbadgeslib.ob1. The three top-level shim modules —
|
|
52
|
+
openbadgeslib.badge, openbadgeslib.signer and openbadgeslib.verifier — are
|
|
53
|
+
removed. OpenBadges 1.0 stays fully supported and reachable via
|
|
54
|
+
openbadgeslib.ob1.* (e.g. `from openbadgeslib.ob1 import Signer`) and the
|
|
55
|
+
unprefixed openbadgeslib.Signer / .Badge / … re-exports, both of which still
|
|
56
|
+
emit the steering DeprecationWarning. BREAKING: `from openbadgeslib.badge
|
|
57
|
+
import ...` (and .signer/.verifier) no longer resolve — repoint them to
|
|
58
|
+
`openbadgeslib.ob1.<module>`.
|
|
59
|
+
- refactor(keys)!: drop the last pycryptodome / python-ecdsa soft-import
|
|
60
|
+
(#170). Both libraries stopped being dependencies in the 3.7 port (#167),
|
|
61
|
+
but `key_to_pem` kept a soft-import shim that still accepted a *live*
|
|
62
|
+
pycryptodome RSA or python-ecdsa key object. That shim (`_legacy_key_to_pem`)
|
|
63
|
+
is removed, along with the `Crypto.*`/`ecdsa.*` mypy overrides: `key_to_pem`
|
|
64
|
+
now takes only `cryptography` key objects and PEM bytes/str, raising
|
|
65
|
+
`UnknownKeyType` otherwise. Key *files* are unaffected — they were already
|
|
66
|
+
plain PEM and round-trip unchanged. BREAKING: pass a `cryptography` key
|
|
67
|
+
object or PEM instead of a live pycryptodome/python-ecdsa object.
|
|
68
|
+
|
|
69
|
+
- fix(ob3): bundle and reference the OB v3p0 extensions context so the
|
|
70
|
+
`credentialSchema` type (`1EdTechJsonSchemaValidator2019`, emitted since
|
|
71
|
+
v3.6.0) resolves to a defined JSON-LD term. Without it the term was
|
|
72
|
+
undefined: the official 1EdTech OB3 validator rejected every issued
|
|
73
|
+
credential ("Undefined JSON-LD term") and canonicalization mapped the type
|
|
74
|
+
to a base-relative IRI, so the Data Integrity proof no longer verified. The
|
|
75
|
+
offline JSON-Schema gate did not catch this (it checks the string, not
|
|
76
|
+
JSON-LD term resolution). Issued credentials now carry a third `@context`
|
|
77
|
+
entry (the official schema pins only the first two and allows extras); the
|
|
78
|
+
context is pinned in the fail-closed allowlist and covered by the drift
|
|
79
|
+
watchdog (#239).
|
|
80
|
+
- security(config): close the bypass in the userinfo guard below: checking
|
|
81
|
+
`urlsplit(...).netloc` only catches a credential whose `@` lands inside the
|
|
82
|
+
authority, and a password holding a `/`, `?` or `#` pushes it out. Both
|
|
83
|
+
layers accepted `https://user:12345/x@host/` — which `did_web_from_url`
|
|
84
|
+
then turned into the issuer id `did:web:user%3A12345:x%40host`, carried by
|
|
85
|
+
every signed credential — and `https://user:secret/x@host/`, which surfaced
|
|
86
|
+
the password through urllib's own `Port could not be cast to integer value
|
|
87
|
+
as 'secret'` (a message the redaction added below could not reach, since
|
|
88
|
+
urllib raises it). Both layers now refuse an `@` or `%40` *anywhere* in a
|
|
89
|
+
URL-shaped value, and `did_web_from_url` wraps the port parse and quotes
|
|
90
|
+
nothing at all. Values with no authority — `[issuer] email`, local paths —
|
|
91
|
+
still accept an `@`. BREAKING (narrow): a `publish_url` whose path contains
|
|
92
|
+
an `@`, e.g. `https://host/@name/`, is now refused; once parsed it is
|
|
93
|
+
indistinguishable from one hiding a credential (#255).
|
|
94
|
+
- security(config): reject a `[issuer]` / `[badge_*]` URL that embeds
|
|
95
|
+
userinfo (`https://user:password@host/`) when the config is loaded. Those
|
|
96
|
+
URLs are public identifiers: `publish_url` is printed by every publish
|
|
97
|
+
command, `urljoin`ed into the ids written to the publicly served
|
|
98
|
+
organization.json / badge.json / key.json (urljoin preserves the
|
|
99
|
+
`user:pass@`), turned into the issuer's did:web, and used to build the
|
|
100
|
+
`credentialStatus` list URLs signed into every OB3 credential — so a
|
|
101
|
+
password there leaked into user-visible output, files served on the open
|
|
102
|
+
web and the badges delivered to recipients alike. Only one narrow path
|
|
103
|
+
guarded against it (did:web derivation, reached solely by
|
|
104
|
+
`[issuer] did = auto`), and that guard echoed the password back in its own
|
|
105
|
+
error for a non-https URL. The check now lives once in
|
|
106
|
+
`ConfParser.read_conf()` — after `${...}` interpolation, so a reference
|
|
107
|
+
cannot smuggle a credential past it — and every consumer (OB1/OB2/OB3
|
|
108
|
+
publishing, DID derivation, status lists) inherits it; the ConfigError
|
|
109
|
+
names the offending section and key but never the value. `did_web_from_url`
|
|
110
|
+
keeps its own guard as defence in depth, with the userinfo check moved
|
|
111
|
+
ahead of the scheme and host checks and their messages redacted. Behaviour
|
|
112
|
+
change: a config that protected a staging host with credentials in a
|
|
113
|
+
published URL no longer loads — drop the userinfo and use an allow-list or
|
|
114
|
+
a token the library never sees. `[smtp] username` / `password` are
|
|
115
|
+
untouched; they never leave the issuer's machine (#252).
|
|
116
|
+
- security(eudi): document and pin the X.509 / eIDAS trust boundary for
|
|
117
|
+
SD-JWT VC verification. All x5c chain validation is delegated to openvc-core
|
|
118
|
+
(path building, temporal validity, SAN<->iss binding); openbadgeslib only
|
|
119
|
+
guarantees the envelope (the x5c header is present, so the anchors are never
|
|
120
|
+
bypassed by a DID fallback — fail-closed). Crucially, certificate revocation
|
|
121
|
+
(CRL/OCSP) is NOT checked by either layer — a revoked-but-unexpired leaf
|
|
122
|
+
still verifies — so a deployment must apply revocation out of band. The
|
|
123
|
+
division of responsibility is spelled out on `verify_badge_sd_jwt` and the
|
|
124
|
+
Security Model wiki page; new boundary tests assert openvc-core rejects an
|
|
125
|
+
expired/not-yet-valid leaf and pin the revocation gap, and the eudi
|
|
126
|
+
floor/latest CI job now runs them so a drift in the delegate's behaviour
|
|
127
|
+
turns the build red (#236).
|
|
128
|
+
- security(ob3): with `--check-status`, verify the status list's OWN JWT-VC
|
|
129
|
+
signature by default and bind its issuer to the badge's, so a compromised
|
|
130
|
+
status host can no longer silently un-revoke a credential. The badge's
|
|
131
|
+
verification key is reused (publish signs the list with it), or the issuer
|
|
132
|
+
DID is resolved. `verify(check_status=True)` gains `verify_status_list=True`
|
|
133
|
+
by default; the verifier CLI gains `--no-verify-status-list` to opt out for
|
|
134
|
+
issuers that serve an unsigned list (#213).
|
|
135
|
+
- security(ob3): `OB3LdpVerifier` fails closed when it has no pinned key and
|
|
136
|
+
the credential's issuer is not a DID — the proof's `verificationMethod`
|
|
137
|
+
cannot be bound to a non-DID issuer, so resolving and trusting it would
|
|
138
|
+
prove internal consistency, not identity (#214).
|
|
139
|
+
- security(ob3): the endorsement validity window fails closed on a malformed
|
|
140
|
+
`validFrom`/`validUntil` (it previously swallowed the parse error and
|
|
141
|
+
accepted the endorsement, unlike every other window) (#218).
|
|
142
|
+
- fix(ob2, ob3): apply a 60-second clock-skew leeway to every validity window
|
|
143
|
+
(vc `validFrom`/`validUntil`, the status list window, OB2 `issuedOn`/
|
|
144
|
+
`expires`), so a slightly-fast issuer clock no longer false-rejects a
|
|
145
|
+
freshly issued credential (#217).
|
|
146
|
+
- perf(ob3, ob1): load the private-key object once per signer instead of
|
|
147
|
+
re-parsing the PEM on every signature (twice per badge on the JWT-VC path).
|
|
148
|
+
An RSA batch of 1000 badges drops from ~86 s to ~1.5 s; the OB1/OB2 JWS
|
|
149
|
+
path reuses the already-loaded key too (#215).
|
|
150
|
+
- perf(baking): build the baked PNG with a single `b''.join` instead of
|
|
151
|
+
quadratic `bytes` concatenation — ~200x faster on images written with many
|
|
152
|
+
small IDAT chunks (Photoshop/libpng), e.g. ~100 ms -> ~0.5 ms for 6.6 MB
|
|
153
|
+
(#216).
|
|
154
|
+
- test: add negative tests for the fail-closed rejection guards that had none
|
|
155
|
+
— OB2 hosted `startsWith`/`allowedOrigins` reject sides, the OB3 status
|
|
156
|
+
anti-gzip-bomb cap and negative index, the LDP verificationMethod binding,
|
|
157
|
+
and a future JWT `nbf` (#219).
|
|
158
|
+
- ci: the nightly conformance job now fails if a 1EdTech validator does not
|
|
159
|
+
come up or if no conformance test actually ran (all-skipped), instead of
|
|
160
|
+
reporting green, and files (or updates) an issue on a scheduled failure
|
|
161
|
+
(#220).
|
|
162
|
+
- ci: install the `[ldp-sd]` extra explicitly and turn the ecdsa-sd-2023
|
|
163
|
+
selective-disclosure test skip into a hard failure under CI, so an
|
|
164
|
+
openvc-core packaging change can no longer silently disable that suite
|
|
165
|
+
(#221).
|
|
166
|
+
- perf(ob3): add an opt-in `CachingDownloader` so verifying a batch of
|
|
167
|
+
credentials from one issuer fetches each did:web document and status list
|
|
168
|
+
once rather than per credential. `OB3LdpVerifier` also memoizes each
|
|
169
|
+
resolved `verificationMethod` per instance, and `OB3Verifier.verify` /
|
|
170
|
+
`check_status` accept a `download=` fetcher to thread it through (#228).
|
|
171
|
+
- perf(ob3): tag the bundled JSON-LD `@context`s `static` in the pyld
|
|
172
|
+
document loader, priming pyld's process-global resolved-context cache so
|
|
173
|
+
term definitions are not rebuilt on every canonicalization (~1.6x faster
|
|
174
|
+
Data Integrity sign/verify); per-call `extra_contexts` stay untagged so a
|
|
175
|
+
changing context is never served stale (#229).
|
|
176
|
+
- feat: extract a programmatic verification facade — `openbadgeslib.verify`
|
|
177
|
+
with `verify_badge(data, ob_version, *, pubkey_pem | resolve_did,
|
|
178
|
+
expected_recipient, check_status, …) -> VerifyResult` (fields `valid`,
|
|
179
|
+
`trusted`, and the decoded `credential`/`assertion`). It runs the whole
|
|
180
|
+
verification — token extraction, JWT-VC/LDP auto-detection, issuer-DID
|
|
181
|
+
resolution, trust classification, signature/status — doing no I/O; an
|
|
182
|
+
invalid badge is a result, not an exception. `openbadges-verifier` is now a
|
|
183
|
+
thin presenter over it (byte-identical output). Part of #222.
|
|
184
|
+
- feat: extract a programmatic publication facade —
|
|
185
|
+
`openbadgeslib.ob3.publish_ob3(conf, output, *, revoke | suspend |
|
|
186
|
+
unsuspend, reason, badge, check_live) -> PublishResult`. It runs the OB3
|
|
187
|
+
issuer-artefact regeneration (the did:web document, the signed Bitstring
|
|
188
|
+
Status List credentials, the opt-in SD-JWT VC Type Metadata) and the
|
|
189
|
+
status-registry revoke/suspend/unsuspend transaction — writing the
|
|
190
|
+
artefacts but doing no printing — and raises `PublishError` /
|
|
191
|
+
`CredentialNotFound` / `AmbiguousCredential` instead of exiting.
|
|
192
|
+
`openbadges-publish -V 3` is now a thin presenter over it (byte-identical
|
|
193
|
+
output). Completes #222.
|
|
194
|
+
- fix(issue): move the version-neutral badge config model (`Badge`,
|
|
195
|
+
`BadgeImgType`) out of the legacy `ob1` package into
|
|
196
|
+
`openbadgeslib.badge_model` (re-exported from `ob1.badge` for
|
|
197
|
+
compatibility). `create_from_conf` now reads the OpenBadges 1.0-only
|
|
198
|
+
`verify_key`/`criteria` keys optionally, so issuing an OB3 badge that omits
|
|
199
|
+
them no longer blows up with a raw `KeyError('verify_key')`; a missing
|
|
200
|
+
required key surfaces as `IssuanceError` through the `issue_*` entry points,
|
|
201
|
+
and a missing image raises a typed error instead of `print` + bare
|
|
202
|
+
`IOError` (#223).
|
|
203
|
+
- refactor(errors): consolidate the exception hierarchy so a single
|
|
204
|
+
`except LibOpenBadgesException` traps every library error. `IssuanceError`
|
|
205
|
+
and `DecompressionLimitExceeded` are re-anchored under the root (still
|
|
206
|
+
importable from `openbadgeslib.issue` / `openbadgeslib.baking`); the config
|
|
207
|
+
helpers' bare `ValueError`s become a new `ConfigError` and the signers'
|
|
208
|
+
unsupported-algorithm error becomes `UnsupportedAlgorithm` — both also
|
|
209
|
+
subclass `ValueError` so `except ValueError` callers keep working.
|
|
210
|
+
`_write_badge_and_log` reads the `[logs]` path inside its `try`, so a missing
|
|
211
|
+
`[logs]` section is reported instead of crashing with a traceback after the
|
|
212
|
+
badge is already on disk. Adds singular aliases (`KeyGenException`,
|
|
213
|
+
`SignerException`, `VerifierException`) for the plural base families (#224).
|
|
214
|
+
- refactor(ob2, ob3): deduplicate the shared JSON helpers (`_iso`,
|
|
215
|
+
`_parse_iso`, `_as_dict`, `_require`) into an internal
|
|
216
|
+
`openbadgeslib._jsonmodel`. The two copies had drifted: OB2's `_iso`
|
|
217
|
+
serialised a naive datetime as *local* time (`astimezone` on a naive value)
|
|
218
|
+
while OB3 anchored it to UTC, so the same timestamp emitted a different
|
|
219
|
+
instant in OB2 than in OB3; the single implementation keeps OB3's
|
|
220
|
+
naive=>UTC rule (#225).
|
|
221
|
+
- feat(confparser): resolve config through typed dataclasses with centralized
|
|
222
|
+
defaults. `load_config(path) -> ConfigParser` is the library entry point
|
|
223
|
+
(raises `ConfigError`); `read_config_or_exit` is now a thin CLI wrapper over
|
|
224
|
+
it (and the flagship example uses `load_config`). New `IssuerConfig` /
|
|
225
|
+
`BadgeSectionConfig` dataclasses (extending the `OB3StatusConfig` pattern)
|
|
226
|
+
resolve the `[issuer]` and badge sections and their defaults once (issuer id,
|
|
227
|
+
`criteria_narrative`->`criteria` fallback, hosted-assertions base), and a
|
|
228
|
+
single `DEFAULT_KEY_TYPE` + `resolve_key_type()` centralises the key_type
|
|
229
|
+
name->KeyType mapping the keygenerator used to inline (#225).
|
|
230
|
+
- security(ob3): warn loudly when `openbadges-publish -V 3` regenerates a
|
|
231
|
+
revocable badge's status lists without a `validUntil` bound (no
|
|
232
|
+
`status_validity_days` set). An unbounded list has no anti-replay freshness —
|
|
233
|
+
an authentic older copy with fewer revocations passes the window unchecked,
|
|
234
|
+
so a revoked credential could be silently resurrected. `PublishResult` gains
|
|
235
|
+
a `no_validity_bound` list (surfaced in the CLI's human output and `--json`),
|
|
236
|
+
and the config example documents the republication cadence (#227).
|
|
237
|
+
- security(ob3): SD-JWT VC (EUDI) issuance no longer silently drops a
|
|
238
|
+
credential's `credentialStatus`. Because SD-JWT VC status carriage/checking
|
|
239
|
+
is not wired yet (both verify paths hard-set `require_status=False`), an
|
|
240
|
+
SD-JWT VC badge is irrevocable; `issue_badge_sd_jwt` now rejects a credential
|
|
241
|
+
that carries a `credentialStatus` with a clear `EudiError` (before requiring
|
|
242
|
+
the [eudi] extra) rather than issue a badge the issuer believes is revocable.
|
|
243
|
+
`badge_to_sd_jwt_claims` also carries `credentialSubject.identifier` (the
|
|
244
|
+
hashed recipient identities), so a badge whose subject travelled via
|
|
245
|
+
`identifier` (no `id`) keeps a checkable recipient. The irrevocability is
|
|
246
|
+
documented prominently on the issue/verify docstrings (#226).
|
|
247
|
+
- security(util): pin the SSRF-validated IP on the HTTPS connection to defeat
|
|
248
|
+
DNS rebinding. `_assert_public_host` validated the resolved IPs, but
|
|
249
|
+
`opener.open` then re-resolved on its own, so a hostile DNS (TTL 0) could
|
|
250
|
+
answer public during the check and internal (169.254.169.254 / loopback /
|
|
251
|
+
RFC1918) at the real connect. A new `_PinnedHTTPSConnection` re-resolves,
|
|
252
|
+
re-validates and dials the validated IP in one step (keeping the hostname
|
|
253
|
+
for TLS SNI / certificate verification and the Host header) — for the initial
|
|
254
|
+
request and every redirect. Also adds a wall-clock download budget
|
|
255
|
+
(`MAX_DOWNLOAD_SECONDS`, 60 s), since the per-socket `timeout=30` bounds each
|
|
256
|
+
read but not a slow-drip response's total time (#231).
|
|
257
|
+
- ci: harden the release-critical workflows. SHA-pin the actions that run with
|
|
258
|
+
`id-token: write` — `pypa/gh-action-pypi-publish` (PyPI Trusted Publishing)
|
|
259
|
+
and the GitHub Pages `upload-pages-artifact` / `deploy-pages` — so a
|
|
260
|
+
compromised upstream tag cannot publish or deploy under our OIDC identity
|
|
261
|
+
(Dependabot already tracks github-actions and bumps the pins). Add a
|
|
262
|
+
least-privilege `permissions: contents: read` to commit-lint.yml. Add a
|
|
263
|
+
`windows-latest` CI leg plus a monkeypatched test of the `fcntl=None`
|
|
264
|
+
StatusRegistry lock fallback, so the "OS Independent" classifier is actually
|
|
265
|
+
exercised on the non-POSIX path. The leg immediately caught a real
|
|
266
|
+
portability bug it fixes: `openbadges-publish` now records `files_written`
|
|
267
|
+
with forward slashes (a Windows `\` would have broken the artefact URLs built
|
|
268
|
+
via `urljoin`); the POSIX file-mode assertions skip on Windows and the
|
|
269
|
+
ldp-sd hard-fail guard keys on an explicit `OPENBADGES_REQUIRE_LDP_SD` flag
|
|
270
|
+
(set by the main test job) so the core-only Windows leg skips it (#230).
|
|
271
|
+
- refactor: declare the stable public surface (freeze prep for v4). The root
|
|
272
|
+
package gains an explicit `__all__` (the "public contract"), re-exports
|
|
273
|
+
`KeyEd25519` (the recommended OB 3.0 / LDP key) alongside `KeyRSA`/`KeyECC`,
|
|
274
|
+
and exposes the `ob2`/`ob3`/`errors` subpackages and the
|
|
275
|
+
`issue_from_conf`/`verify_badge` facades by name. `openbadgeslib.ob3`
|
|
276
|
+
re-exports the `eudi` SD-JWT VC track (its heavy imports stay lazy).
|
|
277
|
+
`SignResult.credential`/`.assertion` and the `_Ob3Context` fields are typed
|
|
278
|
+
with `TYPE_CHECKING` forward refs instead of `Any` (zero runtime cost,
|
|
279
|
+
correct autocompletion / pdoc). Adds a "Stable API" wiki page documenting
|
|
280
|
+
the stable-vs-internal boundary (#232).
|
|
281
|
+
- build(deps): the [dev] extra depends on gitlint-core instead of the gitlint
|
|
282
|
+
meta-package, so it stops installing vulnerable pins. `gitlint` is an empty
|
|
283
|
+
shim for `gitlint-core[trusted-deps]`, and that extra hard-pins click==8.1.3
|
|
284
|
+
and sh==1.14.3 — PYSEC-2026-2132 (command injection in `click.edit()`, fixed
|
|
285
|
+
in 8.3.3) and CVE-2026-54552 (incomplete privilege drop in sh's `_uid`, fixed
|
|
286
|
+
in 2.2.4), which had been failing the weekly Audit job. Neither is reachable
|
|
287
|
+
here — gitlint never calls `click.edit()`, and it does not import `sh` at all
|
|
288
|
+
unless GITLINT_USE_SH_LIB=1, which this repo never sets — but upstream has
|
|
289
|
+
been dormant since 2023, so the pins would keep the audit red indefinitely.
|
|
290
|
+
gitlint-core declares the same dependencies loosely and provides the same
|
|
291
|
+
`gitlint` console script; the [dev] extra now floors `click>=8.3.3` and
|
|
292
|
+
`sh>=2.2.4` (keeping gitlint-core's non-Windows marker for sh) and caps both
|
|
293
|
+
below the next major. No pip-audit ignores were added: the advisories are
|
|
294
|
+
resolved by upgrading, not suppressed. Library users are unaffected — click
|
|
295
|
+
and sh were only ever development dependencies.
|
|
296
|
+
- build(deps)!: raise the PyJWT floor to >=2.13 and the openvc-core floors to
|
|
297
|
+
>=1.21. The OB3 JWT-VC verifier decodes an untrusted badge with a
|
|
298
|
+
signature-verifying `jwt.decode()`, so the PyJWT 2.12/2.13 advisory batch is
|
|
299
|
+
on a reachable path: RFC 7515 §4.1.11 `crit` validation (2.12.0,
|
|
300
|
+
CVE-2026-32597) and the b64=false unencoded-payload DoS (2.13.0,
|
|
301
|
+
CVE-2026-48525), plus JWK/alg-binding hardening; 2.13.0 is the minimal floor
|
|
302
|
+
carrying all of them. Installing [eudi]/[ldp-sd] already pulled a safe PyJWT
|
|
303
|
+
transitively via openvc-core (whose own floor is >=2.13), so this closes the
|
|
304
|
+
gap for a base, extras-free install. The native `_jws` engine is structurally
|
|
305
|
+
immune (it never calls `jwt.decode`). The [eudi]/[ldp-sd] openvc-core floors
|
|
306
|
+
move from >=1.18 / >=1.13 to >=1.21 per the pin's stated policy (floor at the
|
|
307
|
+
version the suite is tested against): 1.21 keeps in-floor the delegated
|
|
308
|
+
SD-JWT fixes openbadgeslib relies on — the key-binding replay fix that now
|
|
309
|
+
requires a nonce AND audience (1.19.3) and unknown-`crit` rejection on the
|
|
310
|
+
SD-JWT lane (1.20.4). No openbadgeslib code changed: openvc's new typed
|
|
311
|
+
ecdsa-sd errors (ProofValueMalformed/ProofMalformed) subclass ProofError, so
|
|
312
|
+
the existing `except ProofError` in ob3/ldp.py still catches them; the full
|
|
313
|
+
suite passes against openvc-core 1.21.0. BREAKING only for a caller that
|
|
314
|
+
pinned PyJWT <2.13 or openvc-core <1.21 alongside openbadgeslib.
|
|
315
|
+
- security(cli): `openbadges-init` creates config.ini owner-only (0600). It
|
|
316
|
+
is the file that goes on to hold the SMTP password and the paths to the
|
|
317
|
+
private keys, but the copy ran outside the umask block that hardens the
|
|
318
|
+
directories, so it inherited the caller's umask — 0644 on a default shell.
|
|
319
|
+
The 0700 parent contained it; this is the second lock, matching the one
|
|
320
|
+
the keygenerator puts on the private key.
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
* v3.14.0 - 2026-07-10
|
|
324
|
+
|
|
325
|
+
A security & correctness hardening pass (audit-bot). Highlights:
|
|
326
|
+
|
|
327
|
+
- security(eudi): reject an X.509-anchored SD-JWT verification whose issuer
|
|
328
|
+
JWT carries no `x5c` chain, so openvc-core cannot fall back to DID /
|
|
329
|
+
issuer-URL key resolution and bypass the EU trust anchors (#186).
|
|
330
|
+
- security(ob3): reject a Data Integrity credential carrying the unsigned VC
|
|
331
|
+
1.1 aliases `expirationDate`/`issuanceDate` or a legacy `StatusList2021Entry`
|
|
332
|
+
status — the RDF-canonicalized proof cannot cover them, so a holder could
|
|
333
|
+
otherwise un-expire / un-revoke a signed credential (#205).
|
|
334
|
+
- security(ob2): reconcile a hosted badge's `expires` against the authoritative
|
|
335
|
+
fetched copy, so a stripped/extended local `expires` no longer defeats the
|
|
336
|
+
expiration check (#187).
|
|
337
|
+
- security(ob3): read `statusSize` from the BitstringStatusListEntry and fail
|
|
338
|
+
closed on a multi-bit entry, so a revoked/suspended multi-bit credential is
|
|
339
|
+
no longer misreported as valid (#188).
|
|
340
|
+
- security(mail): give `SMTP_SSL` a validating TLS context (check_hostname +
|
|
341
|
+
CERT_REQUIRED), so the badge-delivery client no longer exposes its SMTP AUTH
|
|
342
|
+
credentials to an on-path attacker (#203).
|
|
343
|
+
- fix(cli): `openbadges-verifier` exits non-zero in human mode when an OB1/OB2
|
|
344
|
+
badge is invalid (was exit 0), matching OB3 and `--json` (#189).
|
|
345
|
+
- fix(cli): `openbadges-signer` surfaces a partial-batch exit code in human
|
|
346
|
+
mode (#191); `-H/--hosted` is rejected outside `-V 2` (#206); a badge section
|
|
347
|
+
missing `private_key`/`public_key` yields a clean error (#210).
|
|
348
|
+
- fix(cli): `openbadges-init -h/--help` prints usage and exits 0 instead of
|
|
349
|
+
creating a directory named `--help` (#207).
|
|
350
|
+
- fix(issue): a baking failure surfaces as `IssuanceError` on the vc-jwt/OB2
|
|
351
|
+
paths (#190), and a missing `[issuer]`/config key does too (#208).
|
|
352
|
+
- fix(publish): isolate a per-badge key failure in status-list regeneration so
|
|
353
|
+
one bad badge no longer aborts the whole publish (#192).
|
|
354
|
+
- fix(ob3): a non-string Data Integrity `proof.created`/`expires` raises
|
|
355
|
+
`OB3VerificationError` instead of a raw `AttributeError` (#193).
|
|
356
|
+
- fix(ob2): accept an embedded `CryptographicKey` object in a Profile's
|
|
357
|
+
`publicKey` (#194).
|
|
358
|
+
- fix(confparser): reject a `status_size_bits` that is not a positive multiple
|
|
359
|
+
of 8 as a clean config error (#204).
|
|
360
|
+
- fix(keygenerator): switch on debug logging at the start of the run, so `-d`
|
|
361
|
+
also covers config reading and validation — not just the tail of key
|
|
362
|
+
generation, where `enable_debug_logging` used to sit.
|
|
363
|
+
- docs: announce the 4.0.0 deprecation window ahead of the release — a new
|
|
364
|
+
"Upgrading to 4.0" wiki page (dropped Python 3.10, the unified 0/1/2
|
|
365
|
+
exit-code contract, the removed pycryptodome/python-ecdsa compat) linked
|
|
366
|
+
from the sidebar, plus a README heads-up.
|
|
367
|
+
- feat(eudi): `issue_badge_sd_jwt` can embed an `x5c` certificate chain, so a
|
|
368
|
+
badge SD-JWT VC is issued anchored on X.509 and verified in one call with
|
|
369
|
+
`verify_badge_sd_jwt(..., x5c_trust_anchors=[…])` — the issue-side
|
|
370
|
+
counterpart of the verify-side x5c trust (#178), for eIDAS / EU Trusted
|
|
371
|
+
List anchoring. Raises the [eudi] openvc-core floor to >=1.18.
|
|
372
|
+
|
|
7
373
|
* v3.13.0 - 2026-07-09
|
|
8
374
|
|
|
9
375
|
- feat(eudi): **opt-in X.509 / EU Trusted List trust for received SD-JWT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Governance
|
|
2
|
+
|
|
3
|
+
How **openbadgeslib** is maintained, released, and kept available. It is a small
|
|
4
|
+
project with a clear, lightweight process — written down here because third
|
|
5
|
+
parties already depend on it to *verify* credentials, so continuity matters.
|
|
6
|
+
|
|
7
|
+
## Maintainership
|
|
8
|
+
|
|
9
|
+
openbadgeslib is actively maintained by **Luis González Fernández**
|
|
10
|
+
(`luisgf@luisgf.es`), the single active maintainer, who decides on scope,
|
|
11
|
+
roadmap, and releases. The project has a historical co-author, Jesús Cea Avión,
|
|
12
|
+
credited in the copyright headers and
|
|
13
|
+
[Authors, License and FAQ](https://github.com/luisgf/openbadgeslib/wiki/Authors-License-and-FAQ);
|
|
14
|
+
active maintenance today is solo.
|
|
15
|
+
|
|
16
|
+
Decision-making is lightweight: the roadmap is tracked as GitHub issues (label
|
|
17
|
+
`roadmap`, tracking issue
|
|
18
|
+
[#175](https://github.com/luisgf/openbadgeslib/issues/175)), and several items
|
|
19
|
+
are built on demand — a [capability request](CONTRIBUTING.md) with a concrete
|
|
20
|
+
use case is how they get prioritised.
|
|
21
|
+
|
|
22
|
+
## Releasing and publishing
|
|
23
|
+
|
|
24
|
+
Releases are **CI-driven and credential-less to publish**:
|
|
25
|
+
|
|
26
|
+
- The version is single-sourced in `openbadgeslib/util.py`; a pushed `vX.Y.Z`
|
|
27
|
+
tag triggers the CI workflow, which runs the full test matrix (Python
|
|
28
|
+
3.10–3.14) and then publishes to
|
|
29
|
+
[PyPI](https://pypi.org/project/openbadgeslib/).
|
|
30
|
+
- Publishing uses **PyPI Trusted Publishing (OIDC)** — there is **no long-lived
|
|
31
|
+
PyPI API token** stored anywhere. The publish job authenticates to PyPI from
|
|
32
|
+
GitHub Actions via a short-lived OIDC token, so there is no release secret to
|
|
33
|
+
leak, rotate, or lose.
|
|
34
|
+
- Each release is announced as a **GitHub Release** with curated notes drawn
|
|
35
|
+
from `Changelog.txt`; the Release is the canonical announcement channel.
|
|
36
|
+
- The **human fallback** (a maintainer cutting a release by hand) is fully
|
|
37
|
+
documented in the wiki
|
|
38
|
+
[Releasing](https://github.com/luisgf/openbadgeslib/wiki/Releasing) runbook —
|
|
39
|
+
it needs only repository write access plus the CI pipeline, no personal
|
|
40
|
+
credentials.
|
|
41
|
+
|
|
42
|
+
## Bus factor and continuity
|
|
43
|
+
|
|
44
|
+
A single active maintainer is a real risk for a library others verify with. The
|
|
45
|
+
mitigations are deliberate:
|
|
46
|
+
|
|
47
|
+
1. **No secret is a single point of failure.** Trusted Publishing means losing
|
|
48
|
+
access to a token cannot happen — there isn't one. Publishing is bound to the
|
|
49
|
+
GitHub repository's identity, not a person's stored key.
|
|
50
|
+
2. **The whole release process is documented and automated** (the wiki Releasing
|
|
51
|
+
runbook and the `release` skill), so anyone with repository access can ship a
|
|
52
|
+
fix without tribal knowledge.
|
|
53
|
+
3. **Handover requirements are explicit.** Taking over maintenance needs two
|
|
54
|
+
grants: **GitHub** repository admin, and **PyPI** owner/maintainer on the
|
|
55
|
+
[`openbadgeslib` project](https://pypi.org/project/openbadgeslib/) plus the
|
|
56
|
+
configured Trusted Publisher. A new maintainer with both can release
|
|
57
|
+
immediately.
|
|
58
|
+
|
|
59
|
+
**Recommended hardening (maintainer action):** add a second **PyPI project
|
|
60
|
+
owner** and a backup **GitHub admin** so a bus event does not strand the
|
|
61
|
+
project. Until then, note that everything published stays available on PyPI
|
|
62
|
+
indefinitely, and the LGPLv3 permits anyone to fork and continue the work.
|
|
63
|
+
|
|
64
|
+
If the project were to become unmaintained, the last release remains installable
|
|
65
|
+
from PyPI, and this document plus the wiki are enough for a fork to pick it up.
|
|
66
|
+
|
|
67
|
+
## Reporting and contact
|
|
68
|
+
|
|
69
|
+
- Bugs and capability requests: GitHub issues (see [CONTRIBUTING.md](CONTRIBUTING.md)).
|
|
70
|
+
- Security vulnerabilities: **privately**, per [SECURITY.md](SECURITY.md) — never
|
|
71
|
+
a public issue.
|
|
72
|
+
- General contact: `luisgf@luisgf.es`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.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
|
|
@@ -30,22 +30,24 @@ Requires-Python: >=3.10
|
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE.txt
|
|
32
32
|
Requires-Dist: pypng>=0.20220715.0
|
|
33
|
-
Requires-Dist: PyJWT[crypto]>=2.
|
|
33
|
+
Requires-Dist: PyJWT[crypto]>=2.13
|
|
34
34
|
Requires-Dist: cryptography>=42
|
|
35
35
|
Requires-Dist: defusedxml>=0.7
|
|
36
36
|
Provides-Extra: ldp
|
|
37
37
|
Requires-Dist: pyld>=2.0.4; extra == "ldp"
|
|
38
38
|
Provides-Extra: eudi
|
|
39
|
-
Requires-Dist: openvc-core<2,>=1.
|
|
39
|
+
Requires-Dist: openvc-core<2,>=1.21; extra == "eudi"
|
|
40
40
|
Provides-Extra: ldp-sd
|
|
41
|
-
Requires-Dist: openvc-core[data-integrity]<2,>=1.
|
|
41
|
+
Requires-Dist: openvc-core[data-integrity]<2,>=1.21; extra == "ldp-sd"
|
|
42
42
|
Provides-Extra: dev
|
|
43
43
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
44
44
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
45
45
|
Requires-Dist: flake8>=7.0; extra == "dev"
|
|
46
46
|
Requires-Dist: pdoc>=14; extra == "dev"
|
|
47
47
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
48
|
-
Requires-Dist: gitlint>=0.19; extra == "dev"
|
|
48
|
+
Requires-Dist: gitlint-core>=0.19; extra == "dev"
|
|
49
|
+
Requires-Dist: click<9,>=8.3.3; extra == "dev"
|
|
50
|
+
Requires-Dist: sh<3,>=2.2.4; sys_platform != "win32" and extra == "dev"
|
|
49
51
|
Requires-Dist: jsonschema[format-nongpl]>=4.20; extra == "dev"
|
|
50
52
|
Dynamic: license-file
|
|
51
53
|
|
|
@@ -64,6 +66,8 @@ with W3C Bitstring Status Lists and `did:web`. It also ships strict
|
|
|
64
66
|
**OpenBadges 2.0** (JWS / hosted assertions) and a frozen **OpenBadges 1.0**
|
|
65
67
|
legacy format, selected with `-V {1,2,3}` (default `3`).
|
|
66
68
|
|
|
69
|
+
> **Heads-up — 4.0.0 is coming.** Timed to Python 3.10's end of life (2026-10), the next major groups a few breaking changes: Python 3.10 is dropped (`requires-python >= 3.11`), the CLIs unify on one `0/1/2` exit-code contract, and the legacy pycryptodome/python-ecdsa compat shim is removed. OpenBadges 1.0 and the `openbadges-<command>` scripts stay. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
|
|
70
|
+
|
|
67
71
|
## Features
|
|
68
72
|
|
|
69
73
|
- Sign badge images (SVG and PNG) as strict OB 2.0 JWS / hosted assertions (with a frozen OB 1.0 legacy format)
|
|
@@ -348,6 +352,16 @@ flake8 openbadgeslib tests # lint
|
|
|
348
352
|
mypy # type check (config in pyproject.toml)
|
|
349
353
|
```
|
|
350
354
|
|
|
355
|
+
## Contributing
|
|
356
|
+
|
|
357
|
+
Bug reports,
|
|
358
|
+
[capability requests](https://github.com/luisgf/openbadgeslib/issues/new/choose)
|
|
359
|
+
(the roadmap's demand signal), and pull requests are welcome — see
|
|
360
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the workflow and the supported-Python
|
|
361
|
+
policy, and [`GOVERNANCE.md`](GOVERNANCE.md) for maintenance and release
|
|
362
|
+
governance. Security issues go through [`SECURITY.md`](SECURITY.md), never a
|
|
363
|
+
public issue.
|
|
364
|
+
|
|
351
365
|
## Changelog
|
|
352
366
|
|
|
353
367
|
See [`Changelog.txt`](Changelog.txt) for the full history, and the
|
|
@@ -13,6 +13,8 @@ with W3C Bitstring Status Lists and `did:web`. It also ships strict
|
|
|
13
13
|
**OpenBadges 2.0** (JWS / hosted assertions) and a frozen **OpenBadges 1.0**
|
|
14
14
|
legacy format, selected with `-V {1,2,3}` (default `3`).
|
|
15
15
|
|
|
16
|
+
> **Heads-up — 4.0.0 is coming.** Timed to Python 3.10's end of life (2026-10), the next major groups a few breaking changes: Python 3.10 is dropped (`requires-python >= 3.11`), the CLIs unify on one `0/1/2` exit-code contract, and the legacy pycryptodome/python-ecdsa compat shim is removed. OpenBadges 1.0 and the `openbadges-<command>` scripts stay. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
|
|
17
|
+
|
|
16
18
|
## Features
|
|
17
19
|
|
|
18
20
|
- Sign badge images (SVG and PNG) as strict OB 2.0 JWS / hosted assertions (with a frozen OB 1.0 legacy format)
|
|
@@ -297,6 +299,16 @@ flake8 openbadgeslib tests # lint
|
|
|
297
299
|
mypy # type check (config in pyproject.toml)
|
|
298
300
|
```
|
|
299
301
|
|
|
302
|
+
## Contributing
|
|
303
|
+
|
|
304
|
+
Bug reports,
|
|
305
|
+
[capability requests](https://github.com/luisgf/openbadgeslib/issues/new/choose)
|
|
306
|
+
(the roadmap's demand signal), and pull requests are welcome — see
|
|
307
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the workflow and the supported-Python
|
|
308
|
+
policy, and [`GOVERNANCE.md`](GOVERNANCE.md) for maintenance and release
|
|
309
|
+
governance. Security issues go through [`SECURITY.md`](SECURITY.md), never a
|
|
310
|
+
public issue.
|
|
311
|
+
|
|
300
312
|
## Changelog
|
|
301
313
|
|
|
302
314
|
See [`Changelog.txt`](Changelog.txt) for the full history, and the
|