openbadgeslib 4.4.4__tar.gz → 4.6.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.4.4 → openbadgeslib-4.6.0}/CONTRIBUTING.md +15 -8
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/Changelog.txt +89 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/GOVERNANCE.md +1 -1
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/PKG-INFO +10 -6
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/README.md +8 -5
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/__init__.py +13 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_jws/__init__.py +4 -0
- openbadgeslib-4.6.0/openbadgeslib/_jws/utils.py +57 -0
- openbadgeslib-4.6.0/openbadgeslib/_python_support.py +54 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/confparser.py +8 -5
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/mail.py +9 -2
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/badge.py +2 -1
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/verifier.py +82 -13
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/__init__.py +13 -1
- openbadgeslib-4.6.0/openbadgeslib/ob3/clr.py +176 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/__init__.py +22 -4
- openbadgeslib-4.6.0/openbadgeslib/ob3/contexts/clr_context_2.0.1.json +62 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/eudi.py +2 -2
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/status.py +64 -14
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/status_list.py +20 -7
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/nonce.py +3 -1
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/sqlite_store.py +16 -2
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_signer.py +9 -2
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/util.py +10 -2
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/PKG-INFO +10 -6
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/SOURCES.txt +7 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/pyproject.toml +1 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/Dockerfile.ob3 +11 -1
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/README.md +6 -1
- openbadgeslib-4.6.0/tests/conformance/relax-danubetech-checksums.awk +24 -0
- openbadgeslib-4.6.0/tests/test_b64url.py +179 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_cli_smoke.py +29 -2
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_confparser.py +26 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_mail.py +19 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_deprecation.py +12 -5
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_error_branches.py +2 -1
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_verifier.py +56 -1
- openbadgeslib-4.6.0/tests/test_ob3_clr.py +144 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_status.py +37 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_status_list.py +18 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_store.py +40 -2
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_public_api.py +6 -1
- openbadgeslib-4.6.0/tests/test_python_support.py +127 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_signer_batch.py +18 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_util.py +14 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verifier_cli_coverage.py +9 -2
- openbadgeslib-4.4.4/openbadgeslib/_jws/utils.py +0 -32
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/LICENSE.txt +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/MANIFEST.in +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/SECURITY.md +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/docs/README.md +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_jsonmodel.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_ob1_api.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/badge_model.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/baking.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/cli_common.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/config.ini.example +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/issue.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/keys.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/__init__.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/verifier.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/models.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/credential.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/did.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/ldp.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/publish.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/signer.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/status_registry.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/verifier.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/__init__.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/codes.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/errors.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/formats.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/handler.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/memory_store.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/metadata.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/offer.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/reconcile.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/store.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_cli.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_keygenerator.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_publish.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_status.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_verifier.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/verify.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/requires.txt +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/setup.cfg +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/config1.ini +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conftest.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_baking.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_cli_json.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_docs.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_errors.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_examples.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_issue.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_issue_credential.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_jsonmodel.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_jws.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_key_operation.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_keys.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_badge_io.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_verifier.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_credential.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_did.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_endorsement.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_oid4vci.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_type_metadata.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_x5c.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_ecdsa_sd.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_errors.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_sign.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_publish_cli.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_status_registry.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_verifier.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_config.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_protocol.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_reservations.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_openbadges_cli.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verify.py +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/withxmlheader.svg +0 -0
|
@@ -34,7 +34,7 @@ pip install -e ".[dev]" # add ,ldp and ,eudi to exercise those tracks
|
|
|
34
34
|
|
|
35
35
|
## The checks a pull request must pass
|
|
36
36
|
|
|
37
|
-
CI runs these on Python 3.10–3.
|
|
37
|
+
CI runs these on Python 3.10–3.15; run them locally before opening a PR (via
|
|
38
38
|
your `.venv`):
|
|
39
39
|
|
|
40
40
|
```sh
|
|
@@ -73,14 +73,21 @@ page for the full convention.
|
|
|
73
73
|
openbadgeslib supports **every CPython release that is not end-of-life**. The
|
|
74
74
|
policy is deliberately simple and predictable:
|
|
75
75
|
|
|
76
|
+
- A version is **deprecated** once it is in its final months before
|
|
77
|
+
end-of-life (or past it): importing openbadgeslib on it emits a
|
|
78
|
+
`DeprecationWarning`, and `Changelog.txt` announces the major that drops it.
|
|
79
|
+
It stays fully supported — `requires-python`, classifier and CI leg
|
|
80
|
+
unchanged — until that major.
|
|
76
81
|
- 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
|
|
78
|
-
example,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
matrix — at the next **major** release after it reaches end-of-life (for
|
|
83
|
+
example, 3.10 in 5.0; see below). Grouping drops into majors keeps breaking
|
|
84
|
+
changes rare.
|
|
85
|
+
- A new CPython minor is **added** to the CI matrix and the classifiers once it
|
|
86
|
+
has a stable (non-pre) release, so downstreams can adopt it early.
|
|
87
|
+
- The current matrix is **3.10–3.15**, and `requires-python = ">=3.10"`.
|
|
88
|
+
Python **3.10 is deprecated** since 4.6.0 (EOL 2026-10-31) and is dropped in
|
|
89
|
+
5.0, which will require Python 3.11+
|
|
90
|
+
([#242](https://github.com/luisgf/openbadgeslib/issues/242)).
|
|
84
91
|
|
|
85
92
|
Security fixes always target the latest release only (see
|
|
86
93
|
[SECURITY.md](SECURITY.md)).
|
|
@@ -4,6 +4,95 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v4.6.0 - 2026-10-13
|
|
8
|
+
|
|
9
|
+
- feat: Python 3.15 is officially supported (#243). It joins the
|
|
10
|
+
publish-gating CI test matrix (lint + mypy + the full suite with every
|
|
11
|
+
optional extra now run on 3.10–3.15) and gets its trove classifier, so it
|
|
12
|
+
shows as supported on PyPI. Until now 3.15 only ran as an advisory,
|
|
13
|
+
non-gating pre-release job, which is retired.
|
|
14
|
+
|
|
15
|
+
- feat: Python 3.10 is deprecated (CPython end of life: 2026-10-31).
|
|
16
|
+
Importing openbadgeslib on 3.10 now emits a `DeprecationWarning` (shown
|
|
17
|
+
by default only for scripts and the installed `openbadges*` console
|
|
18
|
+
tools, once, on stderr). Nothing else changes in 4.x: `requires-python`,
|
|
19
|
+
the 3.10 classifier and CI leg stay. 5.0 drops 3.10 and requires Python
|
|
20
|
+
3.11+ (#242). To silence it, including in a test suite run with
|
|
21
|
+
`filterwarnings = error`, which otherwise fails at import, add the
|
|
22
|
+
message filter
|
|
23
|
+
`ignore:Python 3.10 is deprecated in openbadgeslib:DeprecationWarning`
|
|
24
|
+
(a module-based filter does not match; details on the Installation
|
|
25
|
+
wiki page).
|
|
26
|
+
|
|
27
|
+
- fix: configuration files and mail templates are read as UTF-8 (a leading
|
|
28
|
+
BOM is skipped) and `signer.log` is written as UTF-8 on every platform,
|
|
29
|
+
instead of in the locale encoding. Python 3.15 made UTF-8 the default
|
|
30
|
+
(PEP 686), so on Windows the same non-ASCII config used to be read
|
|
31
|
+
differently (and baked into badges garbled) on 3.10–3.14; a config that
|
|
32
|
+
is not valid UTF-8 now fails on every version with a `ConfigError` that
|
|
33
|
+
says so, and such a mail template makes `-M` report a mail error naming
|
|
34
|
+
the file. `--recipients-file`, already read as UTF-8, now also skips a
|
|
35
|
+
BOM (it used to stick to the first recipient) and reports a file that is
|
|
36
|
+
not valid UTF-8 instead of failing with a traceback.
|
|
37
|
+
|
|
38
|
+
- fix(oid4vci): `SqliteOID4VCIStore` closes the SQLite connection it opened
|
|
39
|
+
when configuring or migrating it fails (e.g. an unknown schema version),
|
|
40
|
+
instead of leaking the handle until garbage collection.
|
|
41
|
+
|
|
42
|
+
- fix: `download_file` closes the response held by an `HTTPError` before
|
|
43
|
+
re-raising it, so a caller that tolerates a failed fetch (the OB1 key and
|
|
44
|
+
revocation lookups) no longer leaves the socket to garbage collection
|
|
45
|
+
(a `ResourceWarning` on recent Pythons). The error's body is therefore
|
|
46
|
+
no longer readable once it propagates; its `.code`, `.headers` and
|
|
47
|
+
message are unaffected.
|
|
48
|
+
|
|
49
|
+
- security(jws): base64url is decoded strictly and canonically (RFC 4648
|
|
50
|
+
§5): the URL-safe alphabet, then no `=` padding or exactly the canonical
|
|
51
|
+
amount, with zero unused trailing bits (§3.5). `urlsafe_b64decode`
|
|
52
|
+
dropped characters outside the alphabet, took `+`/`/` for `-`/`_` and
|
|
53
|
+
ignored extra `=` and the trailing bits, so the bytes of one signature
|
|
54
|
+
had many accepted encodings. A JWS (OB1, OB2) whose signature segment is
|
|
55
|
+
spelled any other way no longer verifies; whitespace around the whole
|
|
56
|
+
token, such as a trailing newline, is ignored, but not inside a segment.
|
|
57
|
+
The library's other base64url decode sites reject the same input as
|
|
58
|
+
malformed: a status list's JWT payload or multibase (`u`) `encodedList`,
|
|
59
|
+
the SD-JWT VC issuer header read to route `x5c` (`EudiError`), and an
|
|
60
|
+
OID4VCI `c_nonce` (invalid, not spent). A bare StatusList2021
|
|
61
|
+
`encodedList` in the standard base64 alphabet (`+`, `/`, `=`), which
|
|
62
|
+
that spec allows, still decodes. OB3 JWT-VC tokens and status-list
|
|
63
|
+
proofs are verified by PyJWT and are unchanged. Prompted by Python 3.15,
|
|
64
|
+
whose `urlsafe_b64decode` warns (`FutureWarning`) that a future version
|
|
65
|
+
will discard `+` and `/`, so the same input would decode differently
|
|
66
|
+
across Python versions.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
* v4.5.0 - 2026-09-12
|
|
70
|
+
|
|
71
|
+
- feat(ob3): public `decode_encoded_list(encoded) -> bytes` and
|
|
72
|
+
`set_indices(bitstring) -> frozenset[int]` for a published Bitstring
|
|
73
|
+
Status List `encodedList` (#322). Same multibase `u` / base64url /
|
|
74
|
+
bounded-gzip (5 MiB) / MSB-first decode the verifier already uses;
|
|
75
|
+
the private `_decode_encoded_list` name remains as a thin alias.
|
|
76
|
+
|
|
77
|
+
- feat(ob3): `build_status_list_credential(..., ttl=)` writes Bitstring
|
|
78
|
+
Status List §2.2 `ttl` (milliseconds) on `credentialSubject`, never at
|
|
79
|
+
the top level — `ttl` is a JSON-LD term scoped to `BitstringStatusList`,
|
|
80
|
+
so a top-level copy would not be signed (#321). Negative values raise
|
|
81
|
+
`ValueError`.
|
|
82
|
+
|
|
83
|
+
- feat(ob3): native CLR 2.0 `ClrCredential` builder (`build_clr_credential`,
|
|
84
|
+
`sign_clr_credential`) with the versioned `context-2.0.1.json` bundled
|
|
85
|
+
and hash-pinned (`CLR_CONTEXT_SHA256`) (#323). Identifier objects and
|
|
86
|
+
member credentials are copied as given — email hashing and member-count
|
|
87
|
+
caps stay downstream. Signing injects the pinned context via
|
|
88
|
+
`extra_contexts` (requires `[ldp]`).
|
|
89
|
+
|
|
90
|
+
- feat(ob2): `OB2Verifier.verify_hosted(document)` verifies a hosted
|
|
91
|
+
assertion JSON document (dict or JSON string) without synthesizing a
|
|
92
|
+
dummy JWS. The HTTPS fetcher is injectable via
|
|
93
|
+
`OB2Verifier(..., download=)`.
|
|
94
|
+
|
|
95
|
+
|
|
7
96
|
* v4.4.4 - 2026-09-10
|
|
8
97
|
|
|
9
98
|
- security(baking): SVG baking strips ``<script>`` / ``foreignObject`` /
|
|
@@ -25,7 +25,7 @@ Releases are **CI-driven and credential-less to publish**:
|
|
|
25
25
|
|
|
26
26
|
- The version is single-sourced in `openbadgeslib/util.py`; a pushed `vX.Y.Z`
|
|
27
27
|
tag triggers the CI workflow, which runs the full test matrix (Python
|
|
28
|
-
3.10–3.
|
|
28
|
+
3.10–3.15) and then publishes to
|
|
29
29
|
[PyPI](https://pypi.org/project/openbadgeslib/).
|
|
30
30
|
- Publishing uses **PyPI Trusted Publishing (OIDC)** — there is **no long-lived
|
|
31
31
|
PyPI API token** stored anywhere. The publish job authenticates to PyPI from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.6.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
|
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
25
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
27
|
Classifier: Natural Language :: English
|
|
27
28
|
Classifier: Natural Language :: Spanish
|
|
@@ -57,7 +58,7 @@ Dynamic: license-file
|
|
|
57
58
|
|
|
58
59
|
[](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
|
|
59
60
|
[](https://pypi.org/project/openbadgeslib/)
|
|
60
|
-
[](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
|
|
61
62
|
[](#license)
|
|
62
63
|
|
|
63
64
|
**A production-ready Python library & CLI for the full Open Badges 3.0 issuer
|
|
@@ -68,7 +69,7 @@ with W3C Bitstring Status Lists and `did:web`. It also ships strict
|
|
|
68
69
|
**OpenBadges 2.0** (JWS / hosted assertions) and a frozen **OpenBadges 1.0**
|
|
69
70
|
legacy format, selected with `-V {1,2,3}` (default `3`).
|
|
70
71
|
|
|
71
|
-
> **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10
|
|
72
|
+
> **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10 stays supported throughout 4.x** (`requires-python >= 3.10`; CI tests 3.10–3.15) but is deprecated since 4.6.0, ahead of its 2026-10-31 end of life; 5.0 drops it and requires Python 3.11+. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
|
|
72
73
|
|
|
73
74
|
## Features
|
|
74
75
|
|
|
@@ -96,7 +97,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
|
|
|
96
97
|
(EdDSA) — *plus* native **W3C Data Integrity / LDP** signing
|
|
97
98
|
(`eddsa-rdfc-2022`, Ed25519, optional `[ldp]` extra). Both proof formats
|
|
98
99
|
issue *and* verify offline, no external service.
|
|
99
|
-
- **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.
|
|
100
|
+
- **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.15, a small
|
|
100
101
|
dependency set, dataclasses + explicit validation (no Pydantic).
|
|
101
102
|
- **Dual-licensed** LGPLv3 (library) / BSD-2-Clause (CLI tools).
|
|
102
103
|
|
|
@@ -122,7 +123,7 @@ additive EUDI SD-JWT VC wallet track.
|
|
|
122
123
|
| `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
|
|
123
124
|
| Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
|
|
124
125
|
| Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
|
|
125
|
-
| Typing / CI | `mypy --strict`, CI 3.10–3.
|
|
126
|
+
| Typing / CI | `mypy --strict`, CI 3.10–3.15 | — | — | Pydantic | — |
|
|
126
127
|
| License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
|
|
127
128
|
|
|
128
129
|
(a) The actively-maintained community fork (open-educational-badges): it
|
|
@@ -141,7 +142,10 @@ all three OB versions.
|
|
|
141
142
|
|
|
142
143
|
## Requirements
|
|
143
144
|
|
|
144
|
-
- Python >= 3.10 (tested on 3.10–3.
|
|
145
|
+
- Python >= 3.10 (tested on 3.10–3.15). Python 3.10 is deprecated since
|
|
146
|
+
4.6.0 — importing openbadgeslib on it emits a `DeprecationWarning` — and
|
|
147
|
+
openbadgeslib 5.0 will require Python 3.11 or newer
|
|
148
|
+
([#242](https://github.com/luisgf/openbadgeslib/issues/242)).
|
|
145
149
|
- [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
|
|
146
150
|
- [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
|
|
147
151
|
- [cryptography](https://pypi.org/project/cryptography/) >= 45
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
|
|
4
4
|
[](https://pypi.org/project/openbadgeslib/)
|
|
5
|
-
[](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
|
|
6
6
|
[](#license)
|
|
7
7
|
|
|
8
8
|
**A production-ready Python library & CLI for the full Open Badges 3.0 issuer
|
|
@@ -13,7 +13,7 @@ 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
|
-
> **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10
|
|
16
|
+
> **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10 stays supported throughout 4.x** (`requires-python >= 3.10`; CI tests 3.10–3.15) but is deprecated since 4.6.0, ahead of its 2026-10-31 end of life; 5.0 drops it and requires Python 3.11+. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
|
|
17
17
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
@@ -41,7 +41,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
|
|
|
41
41
|
(EdDSA) — *plus* native **W3C Data Integrity / LDP** signing
|
|
42
42
|
(`eddsa-rdfc-2022`, Ed25519, optional `[ldp]` extra). Both proof formats
|
|
43
43
|
issue *and* verify offline, no external service.
|
|
44
|
-
- **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.
|
|
44
|
+
- **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.15, a small
|
|
45
45
|
dependency set, dataclasses + explicit validation (no Pydantic).
|
|
46
46
|
- **Dual-licensed** LGPLv3 (library) / BSD-2-Clause (CLI tools).
|
|
47
47
|
|
|
@@ -67,7 +67,7 @@ additive EUDI SD-JWT VC wallet track.
|
|
|
67
67
|
| `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
|
|
68
68
|
| Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
|
|
69
69
|
| Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
|
|
70
|
-
| Typing / CI | `mypy --strict`, CI 3.10–3.
|
|
70
|
+
| Typing / CI | `mypy --strict`, CI 3.10–3.15 | — | — | Pydantic | — |
|
|
71
71
|
| License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
|
|
72
72
|
|
|
73
73
|
(a) The actively-maintained community fork (open-educational-badges): it
|
|
@@ -86,7 +86,10 @@ all three OB versions.
|
|
|
86
86
|
|
|
87
87
|
## Requirements
|
|
88
88
|
|
|
89
|
-
- Python >= 3.10 (tested on 3.10–3.
|
|
89
|
+
- Python >= 3.10 (tested on 3.10–3.15). Python 3.10 is deprecated since
|
|
90
|
+
4.6.0 — importing openbadgeslib on it emits a `DeprecationWarning` — and
|
|
91
|
+
openbadgeslib 5.0 will require Python 3.11 or newer
|
|
92
|
+
([#242](https://github.com/luisgf/openbadgeslib/issues/242)).
|
|
90
93
|
- [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
|
|
91
94
|
- [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
|
|
92
95
|
- [cryptography](https://pypi.org/project/cryptography/) >= 45
|
|
@@ -72,6 +72,19 @@ from .issue import ( # noqa: F401
|
|
|
72
72
|
from .verify import verify_badge # noqa: F401
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
# ── Supported Python versions ────────────────────────────────────────────────
|
|
76
|
+
# On a deprecated CPython (3.10, dropped in 5.0 — #242) the package's first
|
|
77
|
+
# import emits a DeprecationWarning, whichever entry point triggers it (import
|
|
78
|
+
# openbadgeslib, a submodule, an installed console script). PEP 565 shows it by
|
|
79
|
+
# default only when __main__ is the importer: a script or a console script,
|
|
80
|
+
# not `python -m openbadgeslib.<tool>` or an importlib-based loader (attributed
|
|
81
|
+
# to runpy/importlib, so hidden unless warnings are enabled: -W, -X dev,
|
|
82
|
+
# pytest). It runs after the imports above because a call placed before them
|
|
83
|
+
# would trip flake8's E402.
|
|
84
|
+
from . import _python_support
|
|
85
|
+
_python_support.warn_if_deprecated()
|
|
86
|
+
|
|
87
|
+
|
|
75
88
|
# ── OpenBadges 1.0 (legacy) ──────────────────────────────────────────────────
|
|
76
89
|
# The unprefixed OB1 names (Signer, Verifier, Badge, …) stay importable from
|
|
77
90
|
# the top-level package for backward compatibility. They are the legacy
|
|
@@ -127,6 +127,10 @@ def verify_block(msg: Union[str, bytes], key: Optional[Any] = None) -> bool:
|
|
|
127
127
|
"""Verify a JWS compact serialization (bytes or str). Returns True or raises SignatureError."""
|
|
128
128
|
if isinstance(msg, str):
|
|
129
129
|
msg = msg.encode('utf-8')
|
|
130
|
+
# Transport whitespace around the whole token (a trailing newline from a
|
|
131
|
+
# file or a third-party baker) is not part of any signed segment; inside
|
|
132
|
+
# a segment it still fails the strict base64url decode.
|
|
133
|
+
msg = msg.strip()
|
|
130
134
|
|
|
131
135
|
try:
|
|
132
136
|
head_b64, payload_b64, sig_b64 = msg.split(b'.')
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import binascii
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from typing import Any, Union
|
|
7
|
+
|
|
8
|
+
_B64URL_ALPHABET = re.compile(rb'[A-Za-z0-9_-]*')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def base64url_decode(data: Union[str, bytes]) -> bytes:
|
|
12
|
+
# Strict, canonical RFC 4648 §5: the URL-safe alphabet, then either no
|
|
13
|
+
# padding (JWS/JWT strip it) or exactly the canonical (-len) % 4 '='
|
|
14
|
+
# characters, with zero unused trailing bits (§3.5). urlsafe_b64decode()
|
|
15
|
+
# silently drops other characters, takes '+'/'/' as aliases of '-'/'_'
|
|
16
|
+
# (Python 3.15 warns that this will change), and ignores extra '=' and
|
|
17
|
+
# the trailing bits, so the same bytes had many accepted encodings. Every
|
|
18
|
+
# caller feeds it untrusted input, so anything else raises binascii.Error
|
|
19
|
+
# (a ValueError); a non-str/bytes argument raises TypeError.
|
|
20
|
+
if isinstance(data, str):
|
|
21
|
+
if not data.isascii():
|
|
22
|
+
raise binascii.Error('base64url data must be ASCII')
|
|
23
|
+
data = data.encode('ascii')
|
|
24
|
+
elif isinstance(data, (bytearray, memoryview)):
|
|
25
|
+
data = bytes(data)
|
|
26
|
+
elif not isinstance(data, bytes):
|
|
27
|
+
raise TypeError('base64url data must be str or bytes, not %s'
|
|
28
|
+
% type(data).__name__)
|
|
29
|
+
stripped = data.rstrip(b'=')
|
|
30
|
+
pad = -len(stripped) % 4
|
|
31
|
+
if (not _B64URL_ALPHABET.fullmatch(stripped) or len(stripped) % 4 == 1
|
|
32
|
+
or len(data) - len(stripped) not in (0, pad)):
|
|
33
|
+
raise binascii.Error('invalid base64url data')
|
|
34
|
+
raw = base64.urlsafe_b64decode(stripped + b'=' * pad)
|
|
35
|
+
if base64.urlsafe_b64encode(raw).rstrip(b'=') != stripped:
|
|
36
|
+
raise binascii.Error('non-canonical base64url data (non-zero pad bits)')
|
|
37
|
+
return raw
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def base64url_encode(input: bytes) -> bytes:
|
|
41
|
+
return base64.urlsafe_b64encode(input).replace(b'=', b'')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def to_json(a: Any) -> bytes:
|
|
45
|
+
return json.dumps(a).encode('utf-8')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def from_json(a: Union[str, bytes]) -> Any:
|
|
49
|
+
if isinstance(a, (bytes, bytearray)):
|
|
50
|
+
a = a.decode('utf-8')
|
|
51
|
+
return json.loads(a)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def to_base64(a: bytes) -> bytes: return base64url_encode(a)
|
|
55
|
+
def from_base64(a: bytes) -> bytes: return base64url_decode(a)
|
|
56
|
+
def encode(a: Any) -> bytes: return to_base64(to_json(a))
|
|
57
|
+
def decode(a: bytes) -> Any: return from_json(from_base64(a))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenBadges Library
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
|
|
5
|
+
|
|
6
|
+
All rights reserved.
|
|
7
|
+
|
|
8
|
+
This library is free software; you can redistribute it and/or
|
|
9
|
+
modify it under the terms of the GNU Lesser General Public
|
|
10
|
+
License as published by the Free Software Foundation; either
|
|
11
|
+
version 3.0 of the License, or (at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This library is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
16
|
+
Lesser General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU Lesser General Public
|
|
19
|
+
License along with this library.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# The runtime side of the "Supported Python versions" policy (CONTRIBUTING.md):
|
|
23
|
+
# a CPython that is still supported but nearing (or past) end of life is
|
|
24
|
+
# deprecated — importing openbadgeslib on it emits a DeprecationWarning — and
|
|
25
|
+
# is dropped only at the next major release.
|
|
26
|
+
#
|
|
27
|
+
# Python 3.10 is deprecated since 4.6.0 and dropped in 5.0 (#242). That drop
|
|
28
|
+
# raises requires-python, the mypy python_version and DEPRECATED_BELOW together,
|
|
29
|
+
# rewording DEPRECATION_MESSAGE for the next deprecated version — or deletes
|
|
30
|
+
# this module (and its call in __init__) if none is pending removal.
|
|
31
|
+
|
|
32
|
+
import sys
|
|
33
|
+
import warnings
|
|
34
|
+
from typing import Tuple
|
|
35
|
+
|
|
36
|
+
#: Importing openbadgeslib on a Python older than this emits the warning below.
|
|
37
|
+
DEPRECATED_BELOW = (3, 11)
|
|
38
|
+
|
|
39
|
+
DEPRECATION_MESSAGE = (
|
|
40
|
+
'Python 3.10 is deprecated in openbadgeslib (CPython end of life: '
|
|
41
|
+
'2026-10-31); openbadgeslib 5.0 will require Python 3.11 or newer.')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def warn_if_deprecated(version_info: Tuple[object, ...] = sys.version_info) -> None:
|
|
45
|
+
"""Emit DEPRECATION_MESSAGE if *version_info* is a deprecated Python.
|
|
46
|
+
|
|
47
|
+
Called once from the package ``__init__``; tests pass a fake version.
|
|
48
|
+
"""
|
|
49
|
+
if version_info < DEPRECATED_BELOW:
|
|
50
|
+
# stacklevel=3 skips this helper and the package __init__ (warnings
|
|
51
|
+
# also skips importlib's bootstrap frames, bpo-24305), so the warning
|
|
52
|
+
# is attributed to the code that imported openbadgeslib: PEP 565 then
|
|
53
|
+
# shows it for a script (__main__) and hides it for a library import.
|
|
54
|
+
warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=3)
|
|
@@ -593,11 +593,14 @@ class ConfParser():
|
|
|
593
593
|
self.parser = ConfigParser(interpolation=ExtendedInterpolation())
|
|
594
594
|
|
|
595
595
|
try:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
596
|
+
# Always UTF-8 (a BOM, as some Windows editors write, is skipped):
|
|
597
|
+
# the locale encoding would make the same file read differently
|
|
598
|
+
# across platforms and across 3.10-3.15 (PEP 686).
|
|
599
|
+
self.parser.read(self.config_file, encoding='utf-8-sig')
|
|
600
|
+
except UnicodeDecodeError as exc:
|
|
601
|
+
# The raw UnicodeDecodeError message is too cryptic.
|
|
602
|
+
raise ConfigError("Configuration file %s is not valid UTF-8: %s"
|
|
603
|
+
% (self.config_file, exc)) from None
|
|
601
604
|
except ConfigParserError as exc:
|
|
602
605
|
# Malformed INI syntax (duplicate section/option, a value line
|
|
603
606
|
# with no section header before it, ...) raises directly from
|
|
@@ -126,8 +126,15 @@ class BadgeMail():
|
|
|
126
126
|
""" Return the Subject and Body of the Email. The first line of the file
|
|
127
127
|
is used as Subject """
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
# UTF-8 on every platform and Python (PEP 686); a BOM is skipped so it
|
|
130
|
+
# cannot end up in the Subject.
|
|
131
|
+
try:
|
|
132
|
+
with open(file, 'r', encoding='utf-8-sig') as f:
|
|
133
|
+
data = f.readlines()
|
|
134
|
+
except UnicodeDecodeError as exc:
|
|
135
|
+
# Name the file: the bare codec message does not say what failed.
|
|
136
|
+
raise ValueError('Mail template %s is not valid UTF-8: %s'
|
|
137
|
+
% (file, exc)) from exc
|
|
131
138
|
|
|
132
139
|
if data:
|
|
133
140
|
return data[0].strip('\n'), ''.join(data[1:])
|
|
@@ -65,7 +65,8 @@ class Assertion():
|
|
|
65
65
|
@staticmethod
|
|
66
66
|
def decode(data: bytes) -> 'Assertion':
|
|
67
67
|
try:
|
|
68
|
-
|
|
68
|
+
# Whitespace around the token is transport noise (see verify_block).
|
|
69
|
+
header, body, signature = data.strip().split(b'.')
|
|
69
70
|
return Assertion(header, body, signature)
|
|
70
71
|
except Exception:
|
|
71
72
|
raise AssertionFormatIncorrect()
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import logging
|
|
24
24
|
from datetime import datetime, timezone
|
|
25
|
-
from typing import Any, List, Optional, Union
|
|
25
|
+
from typing import Any, Callable, List, Optional, Union
|
|
26
26
|
from urllib.parse import urlparse
|
|
27
27
|
|
|
28
28
|
from .models import Assertion, CryptographicKey, hash_identity, _parse_iso
|
|
@@ -67,10 +67,16 @@ class OB2Verifier:
|
|
|
67
67
|
pubkey_pem: Optional PEM-encoded trusted public key. When supplied it is
|
|
68
68
|
used to verify SignedBadge assertions and skips resolving the
|
|
69
69
|
badge-declared key.
|
|
70
|
+
download: Optional ``url -> bytes`` fetcher used by hosted verification
|
|
71
|
+
(assertion id, BadgeClass, issuer Profile, revocation list).
|
|
72
|
+
Defaults to :func:`download_file` (HTTPS-only, size-capped).
|
|
70
73
|
"""
|
|
71
74
|
|
|
72
|
-
def __init__(self, pubkey_pem: Optional[Any] = None
|
|
75
|
+
def __init__(self, pubkey_pem: Optional[Any] = None, *,
|
|
76
|
+
download: Optional[Callable[[str], bytes]] = None) -> None:
|
|
73
77
|
self.trusted_pubkey_pem: Optional[Union[str, bytes]] = None
|
|
78
|
+
self._download: Callable[[str], bytes] = (
|
|
79
|
+
download if download is not None else download_file)
|
|
74
80
|
if pubkey_pem is not None:
|
|
75
81
|
pem = key_to_pem(pubkey_pem)
|
|
76
82
|
try:
|
|
@@ -122,6 +128,67 @@ class OB2Verifier:
|
|
|
122
128
|
|
|
123
129
|
return assertion
|
|
124
130
|
|
|
131
|
+
def verify_hosted(self, document: Union[str, dict[str, Any]], *,
|
|
132
|
+
expected_recipient: Optional[str] = None,
|
|
133
|
+
check_revocation: bool = False) -> Assertion:
|
|
134
|
+
"""Verify a hosted OB 2.0 assertion JSON document (not a compact JWS).
|
|
135
|
+
|
|
136
|
+
*document* is the assertion as a dict or a JSON string. The hosted
|
|
137
|
+
model is fetch-from-id: the copy served at ``assertion.id`` is the
|
|
138
|
+
trust anchor (scoped to the issuer Profile), so this does not
|
|
139
|
+
synthesize a dummy JWS. Defence-in-depth JWS verification inside
|
|
140
|
+
:meth:`_verify_hosted` is skipped when there is no token.
|
|
141
|
+
|
|
142
|
+
``expected_recipient`` and ``check_revocation`` behave as in
|
|
143
|
+
:meth:`verify`. Raises :class:`OB2VerificationError` if the document
|
|
144
|
+
is not a HostedBadge assertion or if any check fails.
|
|
145
|
+
"""
|
|
146
|
+
parsed = self._hosted_document(document)
|
|
147
|
+
try:
|
|
148
|
+
assertion = Assertion.from_dict(parsed)
|
|
149
|
+
except ValueError as exc:
|
|
150
|
+
raise OB2VerificationError(
|
|
151
|
+
"Malformed OB 2.0 assertion: %s" % exc) from exc
|
|
152
|
+
if assertion.verification.type != "HostedBadge":
|
|
153
|
+
raise OB2VerificationError(
|
|
154
|
+
"verify_hosted() requires verification.type HostedBadge, got %r"
|
|
155
|
+
% assertion.verification.type)
|
|
156
|
+
|
|
157
|
+
assertion = self._verify_hosted(assertion, "")
|
|
158
|
+
|
|
159
|
+
self._check_expiration(assertion)
|
|
160
|
+
|
|
161
|
+
if expected_recipient is not None:
|
|
162
|
+
self._check_identity(assertion, expected_recipient)
|
|
163
|
+
|
|
164
|
+
if check_revocation:
|
|
165
|
+
self._check_revocation(assertion)
|
|
166
|
+
|
|
167
|
+
return assertion
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _hosted_document(document: Union[str, dict[str, Any]]) -> dict[str, Any]:
|
|
171
|
+
"""Parse a hosted assertion from a dict or JSON string (never a JWS)."""
|
|
172
|
+
if isinstance(document, str):
|
|
173
|
+
text = document.strip()
|
|
174
|
+
if text.count('.') == 2 and not text.startswith('{'):
|
|
175
|
+
raise OB2VerificationError(
|
|
176
|
+
"verify_hosted() expects a hosted assertion JSON document, "
|
|
177
|
+
"not a compact JWS; use verify() for SignedBadge tokens")
|
|
178
|
+
try:
|
|
179
|
+
parsed = jws_utils.from_json(text)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
raise OB2VerificationError(
|
|
182
|
+
"hosted assertion is not valid JSON: %s" % exc) from exc
|
|
183
|
+
elif isinstance(document, dict):
|
|
184
|
+
parsed = document
|
|
185
|
+
else:
|
|
186
|
+
raise OB2VerificationError(
|
|
187
|
+
"hosted assertion must be a dict or JSON string")
|
|
188
|
+
if not isinstance(parsed, dict):
|
|
189
|
+
raise OB2VerificationError("hosted assertion is not a JSON object")
|
|
190
|
+
return parsed
|
|
191
|
+
|
|
125
192
|
# ── signed path ──────────────────────────────────────────────────────────────
|
|
126
193
|
|
|
127
194
|
def _verify_signed(self, assertion: Assertion, token: str) -> None:
|
|
@@ -255,16 +322,18 @@ class OB2Verifier:
|
|
|
255
322
|
# verify it, but never fail the hosted verdict on it (a hosted badge is
|
|
256
323
|
# not required to be signed at all). The creator comes from the
|
|
257
324
|
# authoritative copy, so this never dereferences a holder-chosen URL.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
325
|
+
# verify_hosted() passes an empty token (there is no JWS); skip then.
|
|
326
|
+
if token:
|
|
327
|
+
try:
|
|
328
|
+
if self.trusted_pubkey_pem is not None:
|
|
329
|
+
self._verify_jws(token, self.trusted_pubkey_pem)
|
|
330
|
+
elif authoritative.verification.creator:
|
|
331
|
+
key = self._resolve_creator(authoritative.verification.creator)
|
|
332
|
+
self._verify_jws(token, key.public_key_pem.encode('utf-8'))
|
|
333
|
+
except OB2VerificationError as exc:
|
|
334
|
+
logger.debug("Hosted assertion %s: baked JWS did not verify (%s); "
|
|
335
|
+
"hosted trust comes from the HTTPS fetch, not the signature.",
|
|
336
|
+
assertion.id, exc)
|
|
268
337
|
return authoritative
|
|
269
338
|
|
|
270
339
|
def _check_hosted_scope(self, assertion_id: str, issuer: dict[str, Any]) -> None:
|
|
@@ -373,7 +442,7 @@ class OB2Verifier:
|
|
|
373
442
|
|
|
374
443
|
def _fetch_json(self, url: str, where: str) -> dict[str, Any]:
|
|
375
444
|
try:
|
|
376
|
-
raw =
|
|
445
|
+
raw = self._download(url)
|
|
377
446
|
except Exception as exc:
|
|
378
447
|
raise OB2VerificationError(
|
|
379
448
|
"Could not fetch %s at %s: %s" % (where, url, exc)) from exc
|
|
@@ -27,9 +27,12 @@ from .verifier import (OB3VerificationError, OB3Verifier,
|
|
|
27
27
|
verify_endorsement_jwt)
|
|
28
28
|
from .ldp import (OB3LdpSigner, OB3LdpVerifier, add_data_integrity_proof,
|
|
29
29
|
verify_data_integrity_proof)
|
|
30
|
-
from .status import check_credential_status
|
|
30
|
+
from .status import check_credential_status, decode_encoded_list, set_indices
|
|
31
31
|
from .status_list import (build_status_list_credential, encode_bitstring,
|
|
32
32
|
sign_status_list_credential, status_entry)
|
|
33
|
+
from .clr import (CLR_CONTEXT_SHA256, CLR_CONTEXT_URL, OB_V3_CONTEXT,
|
|
34
|
+
VCDM_V2_CONTEXT, build_clr_credential, bundled_clr_contexts,
|
|
35
|
+
sign_clr_credential)
|
|
33
36
|
from .status_registry import StatusRegistry
|
|
34
37
|
from .did import (build_did_document, did_jwk_from_jwk, did_key_from_pem,
|
|
35
38
|
did_web_from_url, multikey_from_pem, resolve_did,
|
|
@@ -68,16 +71,25 @@ __all__ = [
|
|
|
68
71
|
'verify_data_integrity_proof',
|
|
69
72
|
'StatusRegistry',
|
|
70
73
|
'build_did_document',
|
|
74
|
+
'build_clr_credential',
|
|
71
75
|
'build_status_list_credential',
|
|
76
|
+
'bundled_clr_contexts',
|
|
72
77
|
'check_credential_status',
|
|
78
|
+
'CLR_CONTEXT_SHA256',
|
|
79
|
+
'CLR_CONTEXT_URL',
|
|
80
|
+
'decode_encoded_list',
|
|
73
81
|
'did_jwk_from_jwk',
|
|
74
82
|
'did_key_from_pem',
|
|
75
83
|
'did_web_from_url',
|
|
76
84
|
'encode_bitstring',
|
|
77
85
|
'multikey_from_pem',
|
|
86
|
+
'OB_V3_CONTEXT',
|
|
78
87
|
'resolve_did',
|
|
79
88
|
'resolve_verification_method',
|
|
89
|
+
'set_indices',
|
|
90
|
+
'sign_clr_credential',
|
|
80
91
|
'sign_status_list_credential',
|
|
81
92
|
'status_entry',
|
|
93
|
+
'VCDM_V2_CONTEXT',
|
|
82
94
|
'verify_endorsement_jwt',
|
|
83
95
|
]
|