openbadgeslib 4.6.1__tar.gz → 4.6.2__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.6.1 → openbadgeslib-4.6.2}/Changelog.txt +65 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/PKG-INFO +6 -6
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/did.py +33 -1
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/eudi.py +186 -10
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/ldp.py +5 -1
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/util.py +1 -1
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/PKG-INFO +6 -6
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/requires.txt +5 -5
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/pyproject.toml +22 -11
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_did.py +32 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_sd_jwt.py +316 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_type_metadata.py +13 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_x5c.py +82 -2
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/CONTRIBUTING.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/GOVERNANCE.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/LICENSE.txt +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/MANIFEST.in +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/README.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/SECURITY.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/docs/README.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/__init__.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jsonmodel.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jws/__init__.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_ob1_api.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_python_support.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/badge_model.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/baking.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/cli_common.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/config.ini.example +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/confparser.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/issue.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/keys.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/mail.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/__init__.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/badge.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/verifier.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/models.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/verifier.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/__init__.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/_context_validation.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/clr.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/__init__.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/clr_context_2.0.1.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/credential.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/publish.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/signer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/status.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/status_list.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/status_registry.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/verifier.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/__init__.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/codes.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/errors.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/formats.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/handler.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/memory_store.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/metadata.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/nonce.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/offer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/reconcile.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/sqlite_store.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/store.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_cli.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_keygenerator.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_publish.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_signer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_status.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_verifier.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/verify.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/SOURCES.txt +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/setup.cfg +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/config1.ini +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/README.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/relax-danubetech-checksums.awk +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conftest.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/images/sample1.png +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/images/sample1.svg +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_b64url.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_baking.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_cli_json.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_cli_smoke.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_confparser.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_docs.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_eddsa.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_errors.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_examples.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_issue.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_issue_credential.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_jsonmodel.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_jws.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_key_operation.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_keys.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_mail.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_badge_io.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_deprecation.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_verifier.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_error_branches.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_verifier.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_clr.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_credential.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_endorsement.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_oid4vci.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_context_validation.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_ecdsa_sd.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_errors.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_sign.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_publish_cli.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_status.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_status_registry.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_verifier.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_config.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_protocol.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_reservations.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_store.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_openbadges_cli.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_public_api.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_python_support.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_signer_batch.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_util.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verifier_cli_coverage.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verify.py +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,71 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v4.6.2 - 2026-10-16
|
|
8
|
+
|
|
9
|
+
- security(deps): the openvc-core floor of `[eudi]`, `[ldp-sd]` and
|
|
10
|
+
`[oid4vci]` rises to 1.35.0. Two behaviours below it reach this
|
|
11
|
+
library's public API. An SD-JWT VC whose issuer JWT carries `aud`
|
|
12
|
+
verified for any relying party, so a badge its issuer restricted to one
|
|
13
|
+
verifier was honoured by all (RFC 7519 §4.1.3); 1.35.0 enforces it, and
|
|
14
|
+
`verify_badge_sd_jwt(audience=…)` now also gates it. And
|
|
15
|
+
`status_list_token_resolver(leeway_s=…)` took any number, so an infinite
|
|
16
|
+
leeway made an expired status-list token verify as current; 1.35.0
|
|
17
|
+
accepts whole seconds from 0 to 86400 only. Neither is an advisory in
|
|
18
|
+
openvc-core. The run-time guard of the ecdsa-sd-2023 verify path stays at
|
|
19
|
+
1.33.2, the security minimum of that path. `[ldp]` and `[ldp-sd]` also cap
|
|
20
|
+
pyld below 4: pyld 4.0 (unreleased, breaking) drops statements from the
|
|
21
|
+
signed dataset that 3.x keeps, an IRI with two `#` or a literal with an
|
|
22
|
+
invalid `@language`, which context validation does not refuse, so the
|
|
23
|
+
credential would still show what the proof no longer covers.
|
|
24
|
+
|
|
25
|
+
- security(eudi): `verify_badge_sd_jwt` enforces the badge's own
|
|
26
|
+
`validFrom` and `validUntil` on both trust paths (pinned key and X.509
|
|
27
|
+
trust anchors), and `issue_badge_sd_jwt` writes the credential's
|
|
28
|
+
`validUntil` as the issuer JWT's `exp`, and a `validFrom` still ahead as
|
|
29
|
+
`nbf`. A badge is a flat IETF SD-JWT VC, and openvc-core reads
|
|
30
|
+
`validFrom` / `validUntil` as a validity window only for a W3C VCDM
|
|
31
|
+
payload, so a badge past its `validUntil` verified as unexpired unless
|
|
32
|
+
`expires_in_s` happened to be passed; badges issued by other
|
|
33
|
+
implementations with only a `validUntil` were affected the same way. The
|
|
34
|
+
earlier of `validUntil` and `expires_in_s` wins, a window value that is
|
|
35
|
+
not an ISO 8601 timestamp with an offset (as `datetime.fromisoformat`
|
|
36
|
+
reads it, which is stricter before Python 3.11) fails closed, and the
|
|
37
|
+
leeway is the 60 seconds the other OB 3.0 verifiers use.
|
|
38
|
+
`issue_badge_sd_jwt` now refuses, with `EudiError`, to make `name`,
|
|
39
|
+
`achievement`, `validFrom` or `validUntil` selectively disclosable: a
|
|
40
|
+
holder could withhold the disclosure and present an expired badge as one
|
|
41
|
+
that never expires, and the stripped presentation still verified. Only
|
|
42
|
+
the recipient (`credentialSubject`) is meant to be withheld. A bare string
|
|
43
|
+
for `disclosable=` is refused too (it was read one character at a time,
|
|
44
|
+
which left the recipient in the clear), and `expires_in_s` must be a
|
|
45
|
+
number within about 317 years of now. Nothing can detect a withheld
|
|
46
|
+
disclosure on a badge that already exists, and an existing badge has no
|
|
47
|
+
`exp`/`nbf` for other verifiers: re-issue badges issued with a
|
|
48
|
+
`disclosable=` that named one of those claims, and any badge that must
|
|
49
|
+
stop verifying at its `validUntil` everywhere. The Type Metadata document
|
|
50
|
+
is unchanged, byte for byte (a change would break the `vct#integrity` pin
|
|
51
|
+
of every badge already issued), and a test now pins its hash.
|
|
52
|
+
With `x5c_trust_anchors`, `verify_badge_sd_jwt` no longer accepts a plain
|
|
53
|
+
JWT (a token with no `~`): openvc-core's `verify_credential` takes one
|
|
54
|
+
and then ignored `expected_vct` and `require_key_binding` and returned
|
|
55
|
+
another type, so a JWT signed by an anchored issuer passed as a badge;
|
|
56
|
+
the pinned-key path always refused it. `status_list_token_resolver`
|
|
57
|
+
validates `leeway_s` when it is built, so a bad value fails there rather
|
|
58
|
+
than on the first status check of a later badge. A hostile issuer JWT
|
|
59
|
+
header nested deeper than the JSON parser recurses now fails as
|
|
60
|
+
`EudiError` instead of escaping `verify_badge_sd_jwt(x5c_trust_anchors=…)`
|
|
61
|
+
as a bare `RecursionError`, and a window value too long to be a timestamp
|
|
62
|
+
is not echoed back in the error.
|
|
63
|
+
|
|
64
|
+
- fix(did): a `did:web` document nested deeper than the JSON parser
|
|
65
|
+
recurses, or deeper than 64 levels (a DID document is a handful), is
|
|
66
|
+
reported as the usual `OB3VerificationError` instead of escaping
|
|
67
|
+
`resolve_did`, `fetch_did_web_document` and the verifiers that resolve an
|
|
68
|
+
issuer's DID as a bare `RecursionError`. CPython 3.14 and later parse
|
|
69
|
+
nesting far past the recursion limit, so the parser alone no longer bounds
|
|
70
|
+
what the later `repr()` and `json.dumps` of the document survive.
|
|
71
|
+
|
|
7
72
|
* v4.6.1 - 2026-10-14
|
|
8
73
|
|
|
9
74
|
- security(ldp): `OB3LdpVerifier` reads only JSON its Data Integrity proof
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 4.6.
|
|
3
|
+
Version: 4.6.2
|
|
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
|
|
@@ -35,14 +35,14 @@ Requires-Dist: PyJWT[crypto]>=2.15.1
|
|
|
35
35
|
Requires-Dist: cryptography>=45
|
|
36
36
|
Requires-Dist: defusedxml>=0.7
|
|
37
37
|
Provides-Extra: ldp
|
|
38
|
-
Requires-Dist: pyld
|
|
38
|
+
Requires-Dist: pyld<4,>=3.3.0; extra == "ldp"
|
|
39
39
|
Provides-Extra: eudi
|
|
40
|
-
Requires-Dist: openvc-core<2,>=1.
|
|
40
|
+
Requires-Dist: openvc-core<2,>=1.35.0; extra == "eudi"
|
|
41
41
|
Provides-Extra: ldp-sd
|
|
42
|
-
Requires-Dist: openvc-core[data-integrity]<2,>=1.
|
|
43
|
-
Requires-Dist: pyld
|
|
42
|
+
Requires-Dist: openvc-core[data-integrity]<2,>=1.35.0; extra == "ldp-sd"
|
|
43
|
+
Requires-Dist: pyld<4,>=3.3.0; extra == "ldp-sd"
|
|
44
44
|
Provides-Extra: oid4vci
|
|
45
|
-
Requires-Dist: openvc-core<2,>=1.
|
|
45
|
+
Requires-Dist: openvc-core<2,>=1.35.0; extra == "oid4vci"
|
|
46
46
|
Provides-Extra: dev
|
|
47
47
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
48
48
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -258,14 +258,46 @@ def _fetch_did_web_document(did: str, fetch: Any) -> dict[str, Any]:
|
|
|
258
258
|
"could not fetch DID document %s: %s" % (url, exc)) from exc
|
|
259
259
|
try:
|
|
260
260
|
doc = json.loads(raw.decode('utf-8'))
|
|
261
|
-
except (ValueError, UnicodeDecodeError) as exc:
|
|
261
|
+
except (ValueError, UnicodeDecodeError, RecursionError) as exc:
|
|
262
|
+
# RecursionError: the host serves a document nested deeper than the
|
|
263
|
+
# parser recurses, which is untrusted input like any other malformed one.
|
|
262
264
|
raise OB3VerificationError(
|
|
263
265
|
"malformed DID document at %s: %s" % (url, exc)) from exc
|
|
264
266
|
if not isinstance(doc, dict):
|
|
265
267
|
raise OB3VerificationError("DID document is not a JSON object")
|
|
268
|
+
if _nested_deeper_than(doc, MAX_DID_DOCUMENT_DEPTH):
|
|
269
|
+
raise OB3VerificationError(
|
|
270
|
+
"DID document at %s is nested more than %d levels deep"
|
|
271
|
+
% (url, MAX_DID_DOCUMENT_DEPTH))
|
|
266
272
|
return doc
|
|
267
273
|
|
|
268
274
|
|
|
275
|
+
#: A DID document is shallow (a handful of levels). json.loads accepts nesting
|
|
276
|
+
#: far past what anything that later walks, repr()s or re-serialises the document
|
|
277
|
+
#: survives (on CPython 3.14 and later its limit is a C stack budget, well beyond
|
|
278
|
+
#: the recursion limit), and the host serving it is the one party the verifier
|
|
279
|
+
#: does not control.
|
|
280
|
+
MAX_DID_DOCUMENT_DEPTH = 64
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _nested_deeper_than(value: Any, limit: int) -> bool:
|
|
284
|
+
"""True if *value* (parsed JSON) has containers nested more than *limit*
|
|
285
|
+
deep. Iterative: the point is to survive input a recursive walk would not."""
|
|
286
|
+
stack: list[Tuple[Any, int]] = [(value, 1)]
|
|
287
|
+
while stack:
|
|
288
|
+
node, depth = stack.pop()
|
|
289
|
+
if isinstance(node, dict):
|
|
290
|
+
children: Any = node.values()
|
|
291
|
+
elif isinstance(node, list):
|
|
292
|
+
children = node
|
|
293
|
+
else:
|
|
294
|
+
continue
|
|
295
|
+
if depth > limit:
|
|
296
|
+
return True
|
|
297
|
+
stack.extend((child, depth + 1) for child in children)
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
|
|
269
301
|
def fetch_did_web_document(did: str, download: Any = None) -> dict[str, Any]:
|
|
270
302
|
"""Fetch and parse the DID document a did:web identifier resolves to.
|
|
271
303
|
|
|
@@ -41,15 +41,19 @@
|
|
|
41
41
|
import base64
|
|
42
42
|
import hashlib
|
|
43
43
|
import json
|
|
44
|
+
import math
|
|
45
|
+
import time
|
|
44
46
|
|
|
47
|
+
from datetime import datetime, timezone
|
|
45
48
|
from typing import Any, Callable, Iterable, Mapping, Optional, Union, cast
|
|
46
49
|
|
|
47
50
|
from .credential import OpenBadgeCredential
|
|
51
|
+
from .._jsonmodel import _parse_iso
|
|
48
52
|
from .._jws.utils import base64url_decode
|
|
49
53
|
from ..errors import LibOpenBadgesException, UnknownKeyType
|
|
50
54
|
from ..keys import (
|
|
51
55
|
KeyType, detect_key_type, ec_curve_from_pem, public_jwk_from_pem)
|
|
52
|
-
from ..util import download_file
|
|
56
|
+
from ..util import CLOCK_SKEW_LEEWAY, download_file
|
|
53
57
|
|
|
54
58
|
# A vct (Verifiable Credential Type) for Open Badges expressed as SD-JWT VC.
|
|
55
59
|
OB3_SD_JWT_VCT = "https://purl.imsglobal.org/spec/ob/v3p0#OpenBadgeCredential"
|
|
@@ -61,6 +65,25 @@ OB3_SD_JWT_FORMAT = "dc+sd-jwt"
|
|
|
61
65
|
# holder can present the achievement while withholding who they are.
|
|
62
66
|
DEFAULT_DISCLOSABLE = ("credentialSubject",)
|
|
63
67
|
|
|
68
|
+
# Claims issue_badge_sd_jwt refuses to make selectively disclosable. A holder
|
|
69
|
+
# may withhold any disclosure and a withheld one is indistinguishable from a
|
|
70
|
+
# decoy (RFC 9901 §9.7), so a disclosable validFrom/validUntil lets the holder
|
|
71
|
+
# present an expired or not-yet-valid badge as one with no validity window; the
|
|
72
|
+
# achievement and its name are what the badge IS, and the Type Metadata marks
|
|
73
|
+
# them mandatory. Only the recipient identity is meant to be withheld.
|
|
74
|
+
_NEVER_DISCLOSABLE = ("name", "achievement", "validFrom", "validUntil")
|
|
75
|
+
|
|
76
|
+
# The largest leeway_s openvc-core accepts for a status-list token (one day).
|
|
77
|
+
_MAX_LEEWAY_S = 86400
|
|
78
|
+
|
|
79
|
+
# The longest validity timestamp worth parsing ("2026-10-16T12:34:56.123456+00:00"
|
|
80
|
+
# is 32); anything longer is not one and is not echoed back in an error message.
|
|
81
|
+
_MAX_TIMESTAMP_CHARS = 64
|
|
82
|
+
|
|
83
|
+
# A bound on expires_in_s (about 317 years): enough for any real credential, and
|
|
84
|
+
# it keeps the exp a number every verifier can read.
|
|
85
|
+
_MAX_EXPIRES_IN_S = 10 ** 10
|
|
86
|
+
|
|
64
87
|
_INSTALL_HINT = (
|
|
65
88
|
"SD-JWT VC support needs the [eudi] extra: pip install openbadgeslib[eudi]")
|
|
66
89
|
|
|
@@ -183,8 +206,12 @@ def badge_type_metadata(vct: str = OB3_SD_JWT_VCT, *, name: str = "Open Badge 3.
|
|
|
183
206
|
|
|
184
207
|
Its ``claims`` describe the payload :func:`badge_to_sd_jwt_claims` emits:
|
|
185
208
|
``name`` / ``achievement`` / ``validFrom`` are always disclosed (``mandatory``),
|
|
186
|
-
the recipient ``credentialSubject`` and ``validUntil`` are optional (
|
|
187
|
-
|
|
209
|
+
the recipient ``credentialSubject`` and ``validUntil`` are optional (``sd:
|
|
210
|
+
allowed``). The document is deliberately left as published: :func:`issue_badge_sd_jwt`
|
|
211
|
+
never makes ``validUntil`` disclosable even though the document permits it
|
|
212
|
+
(nothing that reads Type Metadata enforces ``sd``), because a byte change here
|
|
213
|
+
would break the ``vct#integrity`` pin of every badge already issued against
|
|
214
|
+
the old bytes. ``vct`` is the type identifier a wallet resolves this from — it
|
|
188
215
|
must equal the issued badge's ``vct`` (openvc-core enforces that identity
|
|
189
216
|
check). Serve :func:`type_metadata_document_bytes` of this document at *vct*
|
|
190
217
|
and pin it with :func:`type_metadata_integrity`.
|
|
@@ -428,6 +455,60 @@ def badge_credential_configuration(
|
|
|
428
455
|
return configuration
|
|
429
456
|
|
|
430
457
|
|
|
458
|
+
def _timestamp(value: Any, name: str) -> datetime:
|
|
459
|
+
"""Parse a badge's validity timestamp (ISO 8601 with a UTC offset, as
|
|
460
|
+
``datetime.fromisoformat`` reads it: stricter before Python 3.11), raising
|
|
461
|
+
ValueError with a short message whatever the value: it came from the
|
|
462
|
+
token and is never echoed back whole."""
|
|
463
|
+
if not isinstance(value, str):
|
|
464
|
+
raise ValueError("%s must be an ISO 8601 string, got a %s"
|
|
465
|
+
% (name, type(value).__name__))
|
|
466
|
+
if len(value) > _MAX_TIMESTAMP_CHARS:
|
|
467
|
+
raise ValueError("%s is not an ISO 8601 timestamp (%d characters)"
|
|
468
|
+
% (name, len(value)))
|
|
469
|
+
return _parse_iso(value, name)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _expiry(claims: Mapping[str, Any], expires_in_s: Optional[float]) -> Optional[int]:
|
|
473
|
+
"""The issuer JWT ``exp`` for a badge: its ``validUntil`` and/or now plus
|
|
474
|
+
*expires_in_s*, whichever is earlier; None when it has neither."""
|
|
475
|
+
candidates: list[int] = []
|
|
476
|
+
if "validUntil" in claims:
|
|
477
|
+
try:
|
|
478
|
+
candidates.append(int(_timestamp(claims["validUntil"], "validUntil").timestamp()))
|
|
479
|
+
except (ValueError, OverflowError) as exc:
|
|
480
|
+
raise EudiError("cannot derive the expiry of this badge: %s" % exc) from exc
|
|
481
|
+
if expires_in_s is not None:
|
|
482
|
+
if (isinstance(expires_in_s, bool) or not isinstance(expires_in_s, (int, float))
|
|
483
|
+
or (isinstance(expires_in_s, float) and not math.isfinite(expires_in_s))
|
|
484
|
+
or abs(expires_in_s) > _MAX_EXPIRES_IN_S):
|
|
485
|
+
raise EudiError(
|
|
486
|
+
"expires_in_s must be a number of seconds within %d of now, got %r"
|
|
487
|
+
% (_MAX_EXPIRES_IN_S, expires_in_s))
|
|
488
|
+
candidates.append(int(time.time() + expires_in_s))
|
|
489
|
+
return min(candidates) if candidates else None
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _not_before(claims: Mapping[str, Any]) -> Optional[int]:
|
|
493
|
+
"""The issuer JWT ``nbf`` for a badge whose ``validFrom`` is still ahead:
|
|
494
|
+
the claim every SD-JWT VC verifier enforces, so a verifier that does not
|
|
495
|
+
know ``validFrom`` refuses it too. None for a badge valid from now or
|
|
496
|
+
earlier — setting it there would only turn a verifier's clock skew into a
|
|
497
|
+
refusal of a freshly issued badge."""
|
|
498
|
+
if "validFrom" not in claims:
|
|
499
|
+
return None
|
|
500
|
+
try:
|
|
501
|
+
starts = _timestamp(claims["validFrom"], "validFrom")
|
|
502
|
+
except ValueError as exc:
|
|
503
|
+
raise EudiError("cannot derive the start of this badge: %s" % exc) from exc
|
|
504
|
+
if starts <= datetime.now(timezone.utc):
|
|
505
|
+
return None
|
|
506
|
+
try:
|
|
507
|
+
return int(starts.timestamp())
|
|
508
|
+
except OverflowError as exc:
|
|
509
|
+
raise EudiError("cannot derive the start of this badge: %s" % exc) from exc
|
|
510
|
+
|
|
511
|
+
|
|
431
512
|
def issue_badge_sd_jwt(
|
|
432
513
|
credential: OpenBadgeCredential,
|
|
433
514
|
*,
|
|
@@ -444,10 +525,22 @@ def issue_badge_sd_jwt(
|
|
|
444
525
|
"""Issue *credential* as an SD-JWT VC.
|
|
445
526
|
|
|
446
527
|
Returns the compact SD-JWT (``<issuer-jwt>~<disclosure>~…``). Only claims in
|
|
447
|
-
*disclosable* that are actually present are made selectively disclosable
|
|
448
|
-
|
|
528
|
+
*disclosable* that are actually present are made selectively disclosable, and
|
|
529
|
+
``name``, ``achievement``, ``validFrom`` and ``validUntil`` are not eligible:
|
|
530
|
+
naming one raises :class:`EudiError`, because a holder could withhold it
|
|
531
|
+
undetected (an expired badge would present as one with no expiry). Pass
|
|
532
|
+
*holder_jwk* to bind the credential to a holder key (``cnf``) for a later
|
|
449
533
|
Key-Binding presentation. Ed25519, P-256 or P-384 keys only.
|
|
450
534
|
|
|
535
|
+
EXPIRY: the credential's ``validUntil`` is also written as the issuer JWT's
|
|
536
|
+
``exp``, the claim every SD-JWT VC verifier enforces (a flat IETF payload's
|
|
537
|
+
``validUntil`` is not read as a bound by the delegate). *expires_in_s* sets an
|
|
538
|
+
earlier expiry still; when both are present the earlier one wins, so neither
|
|
539
|
+
can lengthen the other. A credential whose ``validUntil`` is already past is
|
|
540
|
+
issued as it stands and fails verification as expired. A ``validFrom`` still
|
|
541
|
+
ahead is written as ``nbf`` for the same reason (not when it is now or past:
|
|
542
|
+
that would only turn a verifier's clock skew into a refusal).
|
|
543
|
+
|
|
451
544
|
Pass *vct_integrity* (from :func:`type_metadata_integrity`) to embed a
|
|
452
545
|
``vct#integrity`` claim that pins the Type Metadata served at *vct*, so a
|
|
453
546
|
wallet resolving it fails closed on any tampering — always disclosed,
|
|
@@ -492,6 +585,20 @@ def issue_badge_sd_jwt(
|
|
|
492
585
|
"issue this copy from a credential without credentialStatus and pass "
|
|
493
586
|
"status=build_token_status_reference(uri=…, index=…) with its own "
|
|
494
587
|
"index in the status-list token.")
|
|
588
|
+
if isinstance(disclosable, (str, bytes)):
|
|
589
|
+
raise EudiError(
|
|
590
|
+
"disclosable must be an iterable of claim names, not a single "
|
|
591
|
+
"string (it would be read one character at a time)")
|
|
592
|
+
requested = list(disclosable)
|
|
593
|
+
refused = [name for name in requested if name in _NEVER_DISCLOSABLE]
|
|
594
|
+
if refused:
|
|
595
|
+
raise EudiError(
|
|
596
|
+
"%s cannot be selectively disclosable: a holder could withhold %s "
|
|
597
|
+
"undetected, so a verifier could not tell a badge without %s from "
|
|
598
|
+
"one that hides it. Only the recipient (credentialSubject) is "
|
|
599
|
+
"meant to be withheld."
|
|
600
|
+
% (", ".join(refused), "it" if len(refused) == 1 else "them",
|
|
601
|
+
"it" if len(refused) == 1 else "them"))
|
|
495
602
|
status_claim = _status_claim(status) if status is not None else None
|
|
496
603
|
_, _, _, SdJwtVcProofSuite = _require_openvc()
|
|
497
604
|
signing_key = _signing_key(privkey_pem, kid or ("%s#key-1" % credential.issuer.id))
|
|
@@ -500,13 +607,19 @@ def issue_badge_sd_jwt(
|
|
|
500
607
|
claims["vct#integrity"] = vct_integrity
|
|
501
608
|
if status_claim is not None:
|
|
502
609
|
claims["status"] = status_claim
|
|
610
|
+
expiry = _expiry(claims, expires_in_s)
|
|
611
|
+
if expiry is not None:
|
|
612
|
+
claims["exp"] = expiry
|
|
613
|
+
not_before = _not_before(claims)
|
|
614
|
+
if not_before is not None:
|
|
615
|
+
claims["nbf"] = not_before
|
|
503
616
|
# "status" is never made selectively disclosable, whatever the caller passed
|
|
504
617
|
# in *disclosable* — see the docstring.
|
|
505
|
-
present = [name for name in
|
|
618
|
+
present = [name for name in requested if name in claims and name != "status"]
|
|
506
619
|
try:
|
|
507
620
|
return cast(str, SdJwtVcProofSuite().issue(
|
|
508
621
|
claims, signing_key=signing_key, disclosable=present, vct=vct,
|
|
509
|
-
holder_jwk=holder_jwk,
|
|
622
|
+
holder_jwk=holder_jwk, x5c=x5c))
|
|
510
623
|
except EudiError:
|
|
511
624
|
raise
|
|
512
625
|
except Exception as exc:
|
|
@@ -536,6 +649,14 @@ def status_list_token_resolver(
|
|
|
536
649
|
key, as ``openbadges-publish`` does for the Bitstring track). Writing your
|
|
537
650
|
own is fine — it must return the verified token's claims dict.
|
|
538
651
|
"""
|
|
652
|
+
# The delegate validates this too (a whole number of seconds, 0 to 86400,
|
|
653
|
+
# from openvc-core 1.35.0; older releases clamped or took any number, so an
|
|
654
|
+
# infinite leeway made an expired list verify). Fail when the resolver is
|
|
655
|
+
# built, not on the first status check of some later badge.
|
|
656
|
+
if (isinstance(leeway_s, bool) or not isinstance(leeway_s, int)
|
|
657
|
+
or not 0 <= leeway_s <= _MAX_LEEWAY_S):
|
|
658
|
+
raise EudiError("leeway_s must be a whole number of seconds from 0 to %d, "
|
|
659
|
+
"got %r" % (_MAX_LEEWAY_S, leeway_s))
|
|
539
660
|
fetch = download if download is not None else download_file
|
|
540
661
|
|
|
541
662
|
def resolve(uri: str) -> dict[str, Any]:
|
|
@@ -610,6 +731,32 @@ def _check_sd_jwt_status(claims: Mapping[str, Any], *, require_status: bool,
|
|
|
610
731
|
return result
|
|
611
732
|
|
|
612
733
|
|
|
734
|
+
def _check_validity_window(claims: Mapping[str, Any]) -> None:
|
|
735
|
+
"""Enforce the badge's own ``validFrom`` / ``validUntil`` on verified claims.
|
|
736
|
+
|
|
737
|
+
The delegate reads them as a validity window only for a W3C VCDM payload
|
|
738
|
+
(one with a VCDM ``@context``); this track's payload is a flat IETF SD-JWT
|
|
739
|
+
VC, so on its own a badge past its ``validUntil`` verifies as long as its
|
|
740
|
+
``exp`` (which :func:`issue_badge_sd_jwt` derives from it) is absent or
|
|
741
|
+
unexpired. Third-party badges, and badges issued before ``exp`` was
|
|
742
|
+
derived, rely on this. Same window and the same leeway as the other OB 3.0
|
|
743
|
+
verifiers (``CLOCK_SKEW_LEEWAY``); a value that is present but not a
|
|
744
|
+
timestamp with an offset fails closed.
|
|
745
|
+
"""
|
|
746
|
+
now = datetime.now(timezone.utc)
|
|
747
|
+
for name in ("validFrom", "validUntil"):
|
|
748
|
+
if name not in claims:
|
|
749
|
+
continue
|
|
750
|
+
try:
|
|
751
|
+
when = _timestamp(claims[name], name)
|
|
752
|
+
except ValueError as exc:
|
|
753
|
+
raise EudiError("badge has a malformed %s: %s" % (name, exc)) from exc
|
|
754
|
+
if name == "validUntil" and when < now - CLOCK_SKEW_LEEWAY:
|
|
755
|
+
raise EudiError("badge has expired (validUntil %s)" % claims[name])
|
|
756
|
+
if name == "validFrom" and when > now + CLOCK_SKEW_LEEWAY:
|
|
757
|
+
raise EudiError("badge is not yet valid (validFrom %s)" % claims[name])
|
|
758
|
+
|
|
759
|
+
|
|
613
760
|
def verify_badge_sd_jwt(
|
|
614
761
|
token: str,
|
|
615
762
|
*,
|
|
@@ -627,7 +774,20 @@ def verify_badge_sd_jwt(
|
|
|
627
774
|
Returns openvc-core's ``VerifiedSdJwt`` (``.claims``, ``.issuer``, ``.vct``,
|
|
628
775
|
``.key_bound``, ``.confirmation``). Raises :class:`EudiError` on any failure.
|
|
629
776
|
Pass *audience*/*nonce* (and ``require_key_binding=True``) to check a Key
|
|
630
|
-
Binding JWT from a holder presentation.
|
|
777
|
+
Binding JWT from a holder presentation. *audience* also gates the issuer
|
|
778
|
+
JWT's own ``aud`` when it has one (openvc-core >=1.35.0, RFC 7519 §4.1.3): a
|
|
779
|
+
badge its issuer restricted to a relying party is refused by every other
|
|
780
|
+
verifier, and by this one when *audience* is unset or different.
|
|
781
|
+
|
|
782
|
+
The badge's validity window is enforced on both trust paths: a ``validUntil``
|
|
783
|
+
in the past (beyond the usual clock-skew leeway) or a ``validFrom`` in the
|
|
784
|
+
future raises :class:`EudiError`, as does a value that is not an ISO 8601
|
|
785
|
+
timestamp with an offset (as ``datetime.fromisoformat`` reads it, which is
|
|
786
|
+
stricter before Python 3.11), alongside the issuer JWT's ``exp``/``nbf`` that
|
|
787
|
+
the delegate checks. A holder can still hide a ``validUntil`` that a *third-party*
|
|
788
|
+
issuer made selectively disclosable and the holder withheld; that cannot be
|
|
789
|
+
detected, so badges from issuers that do so must be treated accordingly
|
|
790
|
+
(this library never issues one: see :func:`issue_badge_sd_jwt`).
|
|
631
791
|
|
|
632
792
|
Revocation (IETF Token Status List) is checked when you supply
|
|
633
793
|
*resolve_status_list_token* — the callable
|
|
@@ -704,6 +864,7 @@ def verify_badge_sd_jwt(
|
|
|
704
864
|
raise
|
|
705
865
|
except Exception as exc:
|
|
706
866
|
raise EudiError("SD-JWT VC badge verification failed: %s" % exc) from exc
|
|
867
|
+
_check_validity_window(verified.claims)
|
|
707
868
|
# The suite verifies the signature and the disclosures but has no status
|
|
708
869
|
# stage (the delegate does status one layer up, in verify_credential — which
|
|
709
870
|
# is the x5c path). Check it here so both paths honour the same seam.
|
|
@@ -722,7 +883,9 @@ def _issuer_jwt_has_x5c(token: str) -> bool:
|
|
|
722
883
|
header_b64 = token.split("~", 1)[0].split(".", 1)[0]
|
|
723
884
|
try:
|
|
724
885
|
header = json.loads(base64url_decode(header_b64))
|
|
725
|
-
except (ValueError, TypeError) as exc:
|
|
886
|
+
except (ValueError, TypeError, RecursionError) as exc:
|
|
887
|
+
# RecursionError: a hostile header nested deep enough to blow the JSON
|
|
888
|
+
# decoder's stack; it is not a ValueError and must not escape raw.
|
|
726
889
|
raise EudiError(
|
|
727
890
|
"could not parse the SD-JWT issuer JWT header: %s" % exc) from exc
|
|
728
891
|
return bool(isinstance(header, dict) and header.get("x5c"))
|
|
@@ -751,6 +914,15 @@ def _verify_sd_jwt_x5c(token: str, x5c_trust_anchors: Any, *,
|
|
|
751
914
|
from openvc import VerificationPolicy, verify_credential
|
|
752
915
|
except ImportError as exc:
|
|
753
916
|
raise EudiError(_INSTALL_HINT) from exc
|
|
917
|
+
# This is the SD-JWT VC verifier. openvc-core's verify_credential also takes
|
|
918
|
+
# a plain VC-JWT (no "~") and then ignores expected_vct and
|
|
919
|
+
# require_key_binding and hands back a different result type, so a JWT an
|
|
920
|
+
# anchored issuer signed would pass here as a badge; the pinned-key path
|
|
921
|
+
# refuses it for lacking the separator, and so must this one.
|
|
922
|
+
if not isinstance(token, str) or "~" not in token:
|
|
923
|
+
raise EudiError(
|
|
924
|
+
"not an SD-JWT VC (no ~ separator): verify_badge_sd_jwt does not "
|
|
925
|
+
"accept a plain JWT on the X.509 trust path either")
|
|
754
926
|
# X.509 trust MUST anchor to the chain in the issuer JWT's x5c header.
|
|
755
927
|
# openvc-core silently falls back to DID / issuer-URL key resolution when the
|
|
756
928
|
# token carries no x5c, which would bypass x5c_trust_anchors entirely (a
|
|
@@ -774,4 +946,8 @@ def _verify_sd_jwt_x5c(token: str, x5c_trust_anchors: Any, *,
|
|
|
774
946
|
raise EudiError(
|
|
775
947
|
"SD-JWT VC badge verification against the X.509 trust anchors "
|
|
776
948
|
"failed: %s" % exc) from exc
|
|
777
|
-
|
|
949
|
+
verified = result.raw
|
|
950
|
+
if not isinstance(getattr(verified, "claims", None), Mapping):
|
|
951
|
+
raise EudiError("the delegate returned something other than an SD-JWT VC")
|
|
952
|
+
_check_validity_window(verified.claims)
|
|
953
|
+
return verified
|
|
@@ -88,7 +88,11 @@ MAX_LDP_DOCUMENT_BYTES = 256 * 1024
|
|
|
88
88
|
#: let nested arrays, re-spelled policy terms or a derived proof that merges
|
|
89
89
|
#: blank nodes through (GHSA-vrxg-57v4-85mc, GHSA-h9rq-wxcw-4rjc; see
|
|
90
90
|
#: ob3/_context_validation.py), so the JSON they report could differ from
|
|
91
|
-
#: what the issuer signed.
|
|
91
|
+
#: what the issuer signed. This is the security minimum of the delegated
|
|
92
|
+
#: verify path, deliberately below the ``>=1.35.0`` the extras declare (that
|
|
93
|
+
#: floor adds the SD-JWT VC issuer-JWT ``aud`` check, which this path does not
|
|
94
|
+
#: use): it refuses a hand-installed release that is unsafe here, not one that
|
|
95
|
+
#: is merely older than the tested pin.
|
|
92
96
|
_OPENVC_MIN_VERSION = (1, 33, 2)
|
|
93
97
|
|
|
94
98
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 4.6.
|
|
3
|
+
Version: 4.6.2
|
|
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
|
|
@@ -35,14 +35,14 @@ Requires-Dist: PyJWT[crypto]>=2.15.1
|
|
|
35
35
|
Requires-Dist: cryptography>=45
|
|
36
36
|
Requires-Dist: defusedxml>=0.7
|
|
37
37
|
Provides-Extra: ldp
|
|
38
|
-
Requires-Dist: pyld
|
|
38
|
+
Requires-Dist: pyld<4,>=3.3.0; extra == "ldp"
|
|
39
39
|
Provides-Extra: eudi
|
|
40
|
-
Requires-Dist: openvc-core<2,>=1.
|
|
40
|
+
Requires-Dist: openvc-core<2,>=1.35.0; extra == "eudi"
|
|
41
41
|
Provides-Extra: ldp-sd
|
|
42
|
-
Requires-Dist: openvc-core[data-integrity]<2,>=1.
|
|
43
|
-
Requires-Dist: pyld
|
|
42
|
+
Requires-Dist: openvc-core[data-integrity]<2,>=1.35.0; extra == "ldp-sd"
|
|
43
|
+
Requires-Dist: pyld<4,>=3.3.0; extra == "ldp-sd"
|
|
44
44
|
Provides-Extra: oid4vci
|
|
45
|
-
Requires-Dist: openvc-core<2,>=1.
|
|
45
|
+
Requires-Dist: openvc-core<2,>=1.35.0; extra == "oid4vci"
|
|
46
46
|
Provides-Extra: dev
|
|
47
47
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
48
48
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -17,14 +17,14 @@ jsonschema[format-nongpl]>=4.20
|
|
|
17
17
|
sh<3,>=2.2.4
|
|
18
18
|
|
|
19
19
|
[eudi]
|
|
20
|
-
openvc-core<2,>=1.
|
|
20
|
+
openvc-core<2,>=1.35.0
|
|
21
21
|
|
|
22
22
|
[ldp]
|
|
23
|
-
pyld
|
|
23
|
+
pyld<4,>=3.3.0
|
|
24
24
|
|
|
25
25
|
[ldp-sd]
|
|
26
|
-
openvc-core[data-integrity]<2,>=1.
|
|
27
|
-
pyld
|
|
26
|
+
openvc-core[data-integrity]<2,>=1.35.0
|
|
27
|
+
pyld<4,>=3.3.0
|
|
28
28
|
|
|
29
29
|
[oid4vci]
|
|
30
|
-
openvc-core<2,>=1.
|
|
30
|
+
openvc-core<2,>=1.35.0
|
|
@@ -94,8 +94,14 @@ dependencies = [
|
|
|
94
94
|
# refused. pyld before 3.3 also canonicalizes an integral float (3.0) as
|
|
95
95
|
# "3.0E0" where 3.3 and jsonld.js give "3", so a proof made on one did not
|
|
96
96
|
# verify on the other.
|
|
97
|
+
# Ceiling below 4: pyld 4.0 is unreleased and breaking, and its toRDF drops
|
|
98
|
+
# statements 3.x keeps (an IRI with two `#`, a literal with an invalid
|
|
99
|
+
# @language). Context validation is written against 3.x's absolute-IRI test and
|
|
100
|
+
# reads the JSON, not the dataset, so a statement pyld 4 drops would still read
|
|
101
|
+
# as present in the credential while the proof never signed it
|
|
102
|
+
# (GHSA-rp5q-966c-5592). Lift the ceiling only with that reworked and tested.
|
|
97
103
|
ldp = [
|
|
98
|
-
"pyld>=3.3.0",
|
|
104
|
+
"pyld>=3.3.0,<4",
|
|
99
105
|
]
|
|
100
106
|
# EUDI track: issue/verify badges as SD-JWT VC (the EU wallet / ARF format) via
|
|
101
107
|
# the generic openvc-core library. openvc-core is stable (1.x) with a published
|
|
@@ -110,13 +116,17 @@ ldp = [
|
|
|
110
116
|
# only one of them came back key_bound=True although it was never bound to
|
|
111
117
|
# this verifier. 1.30.0 refuses that half-configured check, and it also
|
|
112
118
|
# bounds untrusted JSON nesting so a deep document fails with a typed error,
|
|
113
|
-
# not a bare RecursionError.
|
|
114
|
-
# 1.
|
|
115
|
-
#
|
|
119
|
+
# not a bare RecursionError. 1.35.0 starts enforcing the issuer JWT's own
|
|
120
|
+
# `aud` (RFC 7519 §4.1.3): before it, a third-party SD-JWT VC whose issuer
|
|
121
|
+
# restricted it to another relying party verified for everyone, and a status
|
|
122
|
+
# list token verified as unexpired under a hostile leeway_s. Per the rule
|
|
123
|
+
# above, the pin itself sits at 1.35.0, the release the suite is tested
|
|
124
|
+
# against, which also carries every openvc fix in between
|
|
125
|
+
# (GHSA-h9rq-wxcw-4rjc among them). Every extra that pulls openvc-core
|
|
116
126
|
# shares this one floor; the CI `eudi` job checks that they agree and runs
|
|
117
127
|
# the [eudi] and [ldp-sd] suites against it.
|
|
118
128
|
eudi = [
|
|
119
|
-
"openvc-core>=1.
|
|
129
|
+
"openvc-core>=1.35.0,<2",
|
|
120
130
|
]
|
|
121
131
|
# ecdsa-sd-2023 (selective disclosure) VERIFY for the OB 3.0 Data Integrity
|
|
122
132
|
# track: credentials secured with the ecdsa-sd-2023 cryptosuite are verified by
|
|
@@ -131,13 +141,14 @@ eudi = [
|
|
|
131
141
|
# of what was signed, so a member re-keyed to its full IRI or moved under
|
|
132
142
|
# @included escaped them; 1.33.2 closes GHSA-h9rq-wxcw-4rjc (nested arrays,
|
|
133
143
|
# re-spelled policy terms, a derived proof merging blank nodes). The pin sits
|
|
134
|
-
# at 1.
|
|
144
|
+
# at 1.35.0, the shared tested floor
|
|
135
145
|
# described under [eudi]. OB3LdpVerifier also runs its own context validation
|
|
136
146
|
# (with pyld) before it delegates, and openvc-core[data-integrity] admits
|
|
137
|
-
# pyld 2.x, so this extra carries the same pyld
|
|
147
|
+
# pyld 2.x (and, unbounded above, pyld 4), so this extra carries the same pyld
|
|
148
|
+
# range as [ldp].
|
|
138
149
|
ldp-sd = [
|
|
139
|
-
"openvc-core[data-integrity]>=1.
|
|
140
|
-
"pyld>=3.3.0",
|
|
150
|
+
"openvc-core[data-integrity]>=1.35.0,<2",
|
|
151
|
+
"pyld>=3.3.0,<4",
|
|
141
152
|
]
|
|
142
153
|
# OID4VCI issuance: hand a badge to a wallet over the pre-authorized code flow.
|
|
143
154
|
# openbadgeslib owns the offers, the state (codes, tokens, nonces, grants) and
|
|
@@ -150,7 +161,7 @@ ldp-sd = [
|
|
|
150
161
|
# require_key_attestation= on verify_credential_request_proofs, so an
|
|
151
162
|
# issuer that advertises key_attestations_required can refuse a proof
|
|
152
163
|
# without one before the signature and before the nonce is spent. Those are
|
|
153
|
-
# the feature minimums; the pin is the shared 1.
|
|
164
|
+
# the feature minimums; the pin is the shared 1.35.0 floor described under
|
|
154
165
|
# [eudi], which also applies 1.30.0's bound on untrusted JSON nesting to the
|
|
155
166
|
# Credential Requests and key proofs this extra hands to openvc.
|
|
156
167
|
#
|
|
@@ -159,7 +170,7 @@ ldp-sd = [
|
|
|
159
170
|
# so an install that does not ask for [oid4vci] is never broken by its absence.
|
|
160
171
|
# See openbadgeslib/oid4vci/ and wiki/Issuing-to-Wallets-with-OID4VCI.md.
|
|
161
172
|
oid4vci = [
|
|
162
|
-
"openvc-core>=1.
|
|
173
|
+
"openvc-core>=1.35.0,<2",
|
|
163
174
|
]
|
|
164
175
|
dev = [
|
|
165
176
|
"pytest>=8.0",
|
|
@@ -495,3 +495,35 @@ class TestForIssuerDidKid:
|
|
|
495
495
|
with pytest.raises(OB3VerificationError, match='different DID'):
|
|
496
496
|
OB3Verifier.for_issuer_did(
|
|
497
497
|
did, download=_doc_download(doc)).verify(token)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def test_a_did_document_nested_past_the_parser_is_a_typed_failure():
|
|
501
|
+
# 200 000 `[`: json.loads raises RecursionError, which is neither a
|
|
502
|
+
# ValueError nor caught as one, and used to escape the fetch raw.
|
|
503
|
+
from openbadgeslib.ob3.did import fetch_did_web_document
|
|
504
|
+
with pytest.raises(OB3VerificationError, match='malformed DID document'):
|
|
505
|
+
fetch_did_web_document('did:web:example.com',
|
|
506
|
+
download=lambda url: b'[' * 200000)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@pytest.mark.parametrize('depth', [65, 5000, 90000])
|
|
510
|
+
def test_a_did_document_nested_past_the_bound_is_refused(depth):
|
|
511
|
+
# Balanced and valid JSON. Depends on the interpreter whether json.loads
|
|
512
|
+
# takes it (3.14 parses ~100 000 levels, older ones stop near the recursion
|
|
513
|
+
# limit); either way it must come out as a typed failure, never as the
|
|
514
|
+
# RecursionError that repr() or json.dumps raise later on what it parsed.
|
|
515
|
+
from openbadgeslib.ob3.did import fetch_did_web_document
|
|
516
|
+
body = b'{"a":' * depth + b'1' + b'}' * depth
|
|
517
|
+
with pytest.raises(OB3VerificationError, match='nested|malformed DID document'):
|
|
518
|
+
fetch_did_web_document('did:web:example.com', download=lambda url: body)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_an_ordinary_did_document_is_far_below_the_depth_bound():
|
|
522
|
+
from openbadgeslib.ob3.did import (MAX_DID_DOCUMENT_DEPTH,
|
|
523
|
+
_nested_deeper_than)
|
|
524
|
+
doc = {'id': 'did:web:example.com',
|
|
525
|
+
'verificationMethod': [{'id': '#k', 'publicKeyJwk': {'kty': 'OKP'}}],
|
|
526
|
+
'service': [{'serviceEndpoint': {'uris': ['https://a', ['b']]}}]}
|
|
527
|
+
assert not _nested_deeper_than(doc, MAX_DID_DOCUMENT_DEPTH)
|
|
528
|
+
assert _nested_deeper_than(doc, 3)
|
|
529
|
+
assert not _nested_deeper_than([], 1) and not _nested_deeper_than('x', 0)
|