openbadgeslib 4.2.0__tar.gz → 4.3.1__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.2.0 → openbadgeslib-4.3.1}/Changelog.txt +125 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/PKG-INFO +8 -6
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/README.md +3 -3
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/__init__.py +7 -3
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jws/__init__.py +36 -11
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/config.ini.example +45 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/confparser.py +187 -2
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/errors.py +5 -21
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/issue.py +187 -2
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/keys.py +0 -2
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/badge.py +24 -8
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/verifier.py +14 -9
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/__init__.py +4 -2
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/credential.py +21 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/did.py +47 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/eudi.py +8 -2
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/status_registry.py +91 -7
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/__init__.py +110 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/codes.py +138 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/errors.py +128 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/formats.py +61 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/handler.py +348 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/memory_store.py +209 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/metadata.py +239 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/nonce.py +138 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/offer.py +260 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/reconcile.py +135 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/sqlite_store.py +630 -0
- openbadgeslib-4.3.1/openbadgeslib/oid4vci/store.py +318 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_keygenerator.py +26 -5
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_publish.py +114 -1
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_signer.py +57 -11
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_verifier.py +62 -14
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/util.py +8 -5
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/PKG-INFO +8 -6
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/SOURCES.txt +17 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/requires.txt +5 -2
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/pyproject.toml +16 -2
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_cli_json.py +30 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_cli_smoke.py +80 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_errors.py +28 -0
- openbadgeslib-4.3.1/tests/test_issue_credential.py +288 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_jws.py +79 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_key_operation.py +8 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_badge_io.py +32 -4
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_verifier.py +33 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_sd_jwt.py +14 -0
- openbadgeslib-4.3.1/tests/test_oid4vci_config.py +265 -0
- openbadgeslib-4.3.1/tests/test_oid4vci_protocol.py +897 -0
- openbadgeslib-4.3.1/tests/test_oid4vci_reservations.py +295 -0
- openbadgeslib-4.3.1/tests/test_oid4vci_store.py +632 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_signer_batch.py +36 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verifier_cli_coverage.py +58 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/CONTRIBUTING.md +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/GOVERNANCE.md +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/LICENSE.txt +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/MANIFEST.in +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/SECURITY.md +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/docs/README.md +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jsonmodel.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_ob1_api.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/badge_model.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/baking.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/cli_common.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/mail.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/__init__.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/models.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/verifier.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/__init__.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/ldp.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/publish.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/signer.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/status.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/status_list.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/verifier.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_cli.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_status.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/verify.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/setup.cfg +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/config1.ini +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/README.md +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conftest.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/images/sample1.png +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/images/sample1.svg +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_baking.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_confparser.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_docs.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_eddsa.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_examples.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_issue.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_jsonmodel.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_keys.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_mail.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_deprecation.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_error_branches.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_verifier.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_credential.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_did.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_endorsement.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_oid4vci.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_type_metadata.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_x5c.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_ecdsa_sd.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_errors.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_sign.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_publish_cli.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_status.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_status_registry.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_verifier.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_openbadges_cli.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_public_api.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_util.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verify.py +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,131 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v4.3.1 - 2026-08-02
|
|
8
|
+
|
|
9
|
+
Audit hardening: security fixes in the JWS engine and keygenerator, CLI
|
|
10
|
+
contract repairs, and documentation that matches the code.
|
|
11
|
+
|
|
12
|
+
- security(jws): bind ES* algorithms to their NIST curves (ES256→P-256,
|
|
13
|
+
ES384→P-384, ES512→P-521) so `verify_block` no longer accepts an ES256
|
|
14
|
+
header with a P-384/P-521 key (#284).
|
|
15
|
+
- security(jws): reject a JWS whose header carries `crit` (RFC 7515 §4.1.11);
|
|
16
|
+
this library understands no critical extensions (#292).
|
|
17
|
+
- security(cli): write key PEMs with `fchmod` on the open descriptor (no
|
|
18
|
+
path-based chmod TOCTOU) and map write OSError to a clean CLI exit (#289).
|
|
19
|
+
- fix(cli): `openbadges-verifier --json` emits a single JSON object when
|
|
20
|
+
trusted-key/config resolution fails, not a bare `[!]` line (#285).
|
|
21
|
+
- fix(cli): reject OB3-only verifier flags (`--check-status`,
|
|
22
|
+
`--no-verify-status-list`, `--resolve-did`) on `-V 1`/`-V 2` instead of
|
|
23
|
+
silently ignoring them (#286).
|
|
24
|
+
- fix(cli): ensure the signer `-o` directory exists and is writable before
|
|
25
|
+
allocating OB3 status indices, so a bad output path does not orphan
|
|
26
|
+
registry slots (#282).
|
|
27
|
+
- fix(ob1): map assertion body decode failures to `AssertionFormatIncorrect`
|
|
28
|
+
(#281); map `OSError`/`TimeoutError` during status checks to a `VerifyInfo`
|
|
29
|
+
verdict (#283); allow offline verification when a trusted key is supplied
|
|
30
|
+
even if the badge-declared key URL is dark (#290).
|
|
31
|
+
- fix(jws): map `TypeError` from `prepare_key` in `sign()` to
|
|
32
|
+
`SignatureError` (live wrong-family key on the OB1 path) (#288).
|
|
33
|
+
- fix(ob3): map an unparseable SD-JWT signing key to `EudiError` (#287).
|
|
34
|
+
- fix(oid4vci): `SqliteOID4VCIStore.close()` closes every per-thread
|
|
35
|
+
connection (was only the calling thread), clearing the bulk of
|
|
36
|
+
`ResourceWarning: unclosed database` under `--cov` (#301).
|
|
37
|
+
- fix(keys): drop the unused module logger and never-raised KeyGen exception
|
|
38
|
+
classes; PEM loads already map to `*ReadError` (#291).
|
|
39
|
+
- docs: correct `--check-status` status-list signature wording, Upgrading-to-4.0
|
|
40
|
+
exit codes, PyJWT floor ≥2.13, openbadges-init argparse docs, six CLI entry
|
|
41
|
+
points, publish `--json` `no_validity_bound`, and CachingDownloader
|
|
42
|
+
`max_entries` memory wording (#293–#300).
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
* v4.3.0 - 2026-08-01
|
|
46
|
+
|
|
47
|
+
Issue a badge straight to a wallet, over OpenID4VCI.
|
|
48
|
+
|
|
49
|
+
- feat(oid4vci): issue badges to a wallet over OpenID for Verifiable
|
|
50
|
+
Credential Issuance 1.0, pre-authorized code flow. New `openbadgeslib.
|
|
51
|
+
oid4vci` package: Credential Offers, the issuer and RFC 8414 discovery
|
|
52
|
+
documents, and handlers for the token, nonce and credential endpoints. It
|
|
53
|
+
ships NO web server — the handlers return response bodies and the
|
|
54
|
+
integrator mounts the routes — which keeps the boundary openbadges-publish
|
|
55
|
+
already has (it writes files to serve, it does not serve them). The wallet
|
|
56
|
+
key-proof cryptography is delegated to openvc-core's `openvc.openid4vci`
|
|
57
|
+
behind a new `[oid4vci]` extra; the state that openvc refuses to hold —
|
|
58
|
+
codes, tokens, nonces, grants and their lifetimes — lives here, and with it
|
|
59
|
+
the "no wrong-issue" property. Formats: `jwt_vc_json` (revocable) and
|
|
60
|
+
`vc+sd-jwt` (the EUDI track, irrevocable per #226).
|
|
61
|
+
NOT HAIP-conformant and nothing built on it may claim to be: no DPoP, no
|
|
62
|
+
verified key attestations, no client authentication, no authorization code
|
|
63
|
+
flow. See wiki/Issuing-to-Wallets-with-OID4VCI.md and the new Security
|
|
64
|
+
Model section for the full list of what is and is not defended.
|
|
65
|
+
The state store is an interface with two backends: in-memory for a single
|
|
66
|
+
process, and SQLite for the threads and processes of one host. Single-use is
|
|
67
|
+
settled in the database rather than in Python — burning a nonce and claiming
|
|
68
|
+
an issuance are INSERTs under BEGIN IMMEDIATE — so exactly one caller wins
|
|
69
|
+
no matter how many workers race. Secrets are stored as digests, the file is
|
|
70
|
+
0600 in a 0700 directory, and an unknown schema version is refused.
|
|
71
|
+
- feat(issue): `issue_credential_from_conf()` issues an OB 3.0 credential as
|
|
72
|
+
a TOKEN rather than a baked badge image, returning a `CredentialResult`.
|
|
73
|
+
The existing `issue_from_conf` always bakes a PNG/SVG, which is wasted work
|
|
74
|
+
(and the wrong deliverable) when the credential goes to a wallet, an LMS
|
|
75
|
+
API or an email — reaching the raw token previously meant baking an SVG and
|
|
76
|
+
extracting it again. Takes an optional `holder_jwk` to bind the credential
|
|
77
|
+
to a wallet key: for `jwt_vc_json` the subject id becomes that key's
|
|
78
|
+
`did:jwk` and the recipient moves to a salted `credentialSubject.
|
|
79
|
+
identifier` hash, so the badge stays attributable to a person without
|
|
80
|
+
publishing their address; for `vc+sd-jwt` the key goes into `cnf`.
|
|
81
|
+
- feat(ob3): `did_jwk_from_jwk()` derives a did:jwk from a public JWK, and
|
|
82
|
+
`IdentityObject.create()` builds a salted-hash identifier (the OB 3.0
|
|
83
|
+
mirror of the OB 2.0 constructor). Both refuse JWKs carrying private key
|
|
84
|
+
material.
|
|
85
|
+
- feat(ob3): the status registry gains reservations (schema version 2).
|
|
86
|
+
An OID4VCI offer reserves its revocation slot when the offer is built, not
|
|
87
|
+
when the wallet claims — deciding it at claim time would put the registry's
|
|
88
|
+
exclusive lock and full-file rewrite inside a concurrent HTTP path, on a
|
|
89
|
+
lock that is a no-op where fcntl is missing. Reserved entries show as
|
|
90
|
+
`reserved` in `--list`; `openbadges-publish -V 3 --reclaim-unclaimed`
|
|
91
|
+
frees the ones whose offer lapsed, refusing to touch any slot the registry
|
|
92
|
+
and the OID4VCI store do not both agree was never issued against. Schema
|
|
93
|
+
version 1 files load unchanged, and a registry with no reservations
|
|
94
|
+
serialises byte-identically to before.
|
|
95
|
+
- fix(oid4vci): the SQLite store's opportunistic garbage collection takes an
|
|
96
|
+
injectable clock. It ran on wall-clock time while every query takes an
|
|
97
|
+
explicit `now=`, so a write collected the very rows a caller had just
|
|
98
|
+
written against a frozen instant — which made the store's own test suite
|
|
99
|
+
pass on the day it was written and fail a week later on the calendar
|
|
100
|
+
rather than on a code change.
|
|
101
|
+
- security(verifier): `openbadges-verifier -V 1` no longer reports
|
|
102
|
+
`trusted: true` for a badge that FAILED verification (#278). The OB 1.0 path
|
|
103
|
+
set the trust flag before the valid/invalid branch, so a tampered badge
|
|
104
|
+
checked against an operator-supplied key (-k/--local) emitted
|
|
105
|
+
`{valid: false, trusted: true, status: SIGNATURE_ERROR}` — contradicting the
|
|
106
|
+
invariant the OB 2.0 and OB 3.0 paths already honoured (#258) and that a
|
|
107
|
+
consumer reading the JSON object, rather than the exit status, relies on.
|
|
108
|
+
Exit codes are unchanged: such a badge already exited 1.
|
|
109
|
+
- fix(cli): `openbadges-signer` maps a misconfigured badge section to a clean
|
|
110
|
+
`[!]` error instead of a raw traceback (#279). It built the badge model
|
|
111
|
+
without the mapper that turns a missing config key or an unreadable
|
|
112
|
+
key/image into an IssuanceError, so the most likely first-run mistake — a
|
|
113
|
+
section missing `private_key`, or keys not generated yet — ended in a
|
|
114
|
+
KeyError traceback in human mode (`--json` was already covered).
|
|
115
|
+
- fix(errors): the documented exception tree no longer misfiles
|
|
116
|
+
`AmbiguousCredential` (#280). `openbadgeslib.errors` carried a dead
|
|
117
|
+
`AmbiguousCredential(StatusError)` that was never raised, while the one
|
|
118
|
+
actually raised and exported is `ob3.publish.AmbiguousCredential`, a
|
|
119
|
+
`PublishError` — so `except StatusError` around a publish/revoke call, as
|
|
120
|
+
the tree in the docstring and the wiki instructed, did not catch it. The
|
|
121
|
+
dead class is gone and the `PublishError` family (with `CredentialNotFound`)
|
|
122
|
+
is documented in both places.
|
|
123
|
+
- build(deps): the openvc-core floors move to >=1.24 across `[eudi]`,
|
|
124
|
+
`[ldp-sd]` and `[oid4vci]`, which is the release the suite is actually
|
|
125
|
+
tested against — the rule the extras already documented, applied uniformly
|
|
126
|
+
(`[ldp-sd]` had been left behind at >=1.21). Substantively 1.23 still
|
|
127
|
+
suffices for the OpenID4VCI key proof and 1.22 for the Token Status List;
|
|
128
|
+
1.24 is additive over both (`ResolveProofKey` is unchanged and not
|
|
129
|
+
deprecated), so an install already on 1.24 sees no behaviour change.
|
|
130
|
+
|
|
131
|
+
|
|
7
132
|
* v4.2.0 - 2026-07-29
|
|
8
133
|
|
|
9
134
|
The OpenID4VCI credential configuration an issuer advertises can now declare
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
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
|
|
@@ -36,9 +36,11 @@ Requires-Dist: defusedxml>=0.7
|
|
|
36
36
|
Provides-Extra: ldp
|
|
37
37
|
Requires-Dist: pyld>=2.0.4; extra == "ldp"
|
|
38
38
|
Provides-Extra: eudi
|
|
39
|
-
Requires-Dist: openvc-core<2,>=1.
|
|
39
|
+
Requires-Dist: openvc-core<2,>=1.24; extra == "eudi"
|
|
40
40
|
Provides-Extra: ldp-sd
|
|
41
|
-
Requires-Dist: openvc-core[data-integrity]<2,>=1.
|
|
41
|
+
Requires-Dist: openvc-core[data-integrity]<2,>=1.24; extra == "ldp-sd"
|
|
42
|
+
Provides-Extra: oid4vci
|
|
43
|
+
Requires-Dist: openvc-core<2,>=1.24; extra == "oid4vci"
|
|
42
44
|
Provides-Extra: dev
|
|
43
45
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
44
46
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -83,7 +85,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
|
|
|
83
85
|
- Issue and verify badges as EUDI **SD-JWT VC** (the EU wallet / ARF format)
|
|
84
86
|
with selective disclosure, via the optional `[eudi]` extra
|
|
85
87
|
- did:web issuer identity: `did.json` generation and DID resolution
|
|
86
|
-
-
|
|
88
|
+
- Six command-line entry points: the unified `openbadges` front-end plus five `openbadges-*` aliases
|
|
87
89
|
|
|
88
90
|
## Why openbadgeslib
|
|
89
91
|
|
|
@@ -119,7 +121,7 @@ additive EUDI SD-JWT VC wallet track.
|
|
|
119
121
|
| Revocation / suspension | ✅ W3C Bitstring Status List | hosted check | `1EdTechRevocationList` | ad-hoc flag | — |
|
|
120
122
|
| `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
|
|
121
123
|
| Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
|
|
122
|
-
| Form factor | library + 5
|
|
124
|
+
| Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
|
|
123
125
|
| Typing / CI | `mypy --strict`, CI 3.10–3.14 | — | — | Pydantic | — |
|
|
124
126
|
| License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
|
|
125
127
|
|
|
@@ -141,7 +143,7 @@ all three OB versions.
|
|
|
141
143
|
|
|
142
144
|
- Python >= 3.10 (tested on 3.10–3.14)
|
|
143
145
|
- [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
|
|
144
|
-
- [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.
|
|
146
|
+
- [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
|
|
145
147
|
- [cryptography](https://pypi.org/project/cryptography/) >= 42
|
|
146
148
|
- [defusedxml](https://pypi.org/project/defusedxml/) >= 0.7
|
|
147
149
|
|
|
@@ -30,7 +30,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
|
|
|
30
30
|
- Issue and verify badges as EUDI **SD-JWT VC** (the EU wallet / ARF format)
|
|
31
31
|
with selective disclosure, via the optional `[eudi]` extra
|
|
32
32
|
- did:web issuer identity: `did.json` generation and DID resolution
|
|
33
|
-
-
|
|
33
|
+
- Six command-line entry points: the unified `openbadges` front-end plus five `openbadges-*` aliases
|
|
34
34
|
|
|
35
35
|
## Why openbadgeslib
|
|
36
36
|
|
|
@@ -66,7 +66,7 @@ additive EUDI SD-JWT VC wallet track.
|
|
|
66
66
|
| Revocation / suspension | ✅ W3C Bitstring Status List | hosted check | `1EdTechRevocationList` | ad-hoc flag | — |
|
|
67
67
|
| `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
|
|
68
68
|
| Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
|
|
69
|
-
| Form factor | library + 5
|
|
69
|
+
| Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
|
|
70
70
|
| Typing / CI | `mypy --strict`, CI 3.10–3.14 | — | — | Pydantic | — |
|
|
71
71
|
| License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
|
|
72
72
|
|
|
@@ -88,7 +88,7 @@ all three OB versions.
|
|
|
88
88
|
|
|
89
89
|
- Python >= 3.10 (tested on 3.10–3.14)
|
|
90
90
|
- [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
|
|
91
|
-
- [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.
|
|
91
|
+
- [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
|
|
92
92
|
- [cryptography](https://pypi.org/project/cryptography/) >= 42
|
|
93
93
|
- [defusedxml](https://pypi.org/project/defusedxml/) >= 0.7
|
|
94
94
|
|
|
@@ -66,7 +66,8 @@ from ._ob1_api import OB1_API as _OB1_API # noqa: F401
|
|
|
66
66
|
# (hence cryptography) — inside the run-to-run noise — so the documentation of
|
|
67
67
|
# the recommended modern API is worth more than it.
|
|
68
68
|
from .issue import ( # noqa: F401
|
|
69
|
-
IssuanceError, SignResult,
|
|
69
|
+
CredentialResult, IssuanceError, SignResult, issue_credential_from_conf,
|
|
70
|
+
issue_from_conf,
|
|
70
71
|
)
|
|
71
72
|
from .verify import verify_badge # noqa: F401
|
|
72
73
|
|
|
@@ -113,6 +114,9 @@ __all__ = [
|
|
|
113
114
|
'OpenBadgeCredential', 'Achievement', 'Issuer',
|
|
114
115
|
# Keys (KeyEd25519 is the recommended OB 3.0 / LDP key type).
|
|
115
116
|
'KeyFactory', 'KeyRSA', 'KeyECC', 'KeyEd25519',
|
|
116
|
-
# Programmatic issue / verify facades.
|
|
117
|
-
|
|
117
|
+
# Programmatic issue / verify facades. issue_credential_from_conf is the
|
|
118
|
+
# token-returning sibling of issue_from_conf, for delivering a credential
|
|
119
|
+
# over a digital channel (a wallet, an API) rather than as a badge image.
|
|
120
|
+
'issue_from_conf', 'issue_credential_from_conf', 'verify_badge',
|
|
121
|
+
'IssuanceError', 'SignResult', 'CredentialResult',
|
|
118
122
|
]
|
|
@@ -48,16 +48,23 @@ def _prepare_key_arg(key: Any) -> Any:
|
|
|
48
48
|
return key_to_pem(key)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
# Each entry is (algorithm class, hash id). EdDSA's
|
|
52
|
-
#
|
|
51
|
+
# Each entry is (algorithm class, hash id, expected_curve). EdDSA's
|
|
52
|
+
# OKPAlgorithm takes no hash or curve — both are None and _algo_for constructs
|
|
53
|
+
# it with no args. RSA has no curve. EC algorithms bind the NIST curve that
|
|
54
|
+
# RFC 7518 §3.4 pairs with each ES* alg (ES256→P-256, ES384→P-384, ES512→P-521)
|
|
55
|
+
# so a P-521 key cannot "verify" an ES256 header and a P-256 key cannot sign
|
|
56
|
+
# ES512 — the same defence jwt.decode() gets from PyJWT's default registry
|
|
57
|
+
# (#284). Without expected_curve, ECAlgorithm accepts any EC key for any ES*
|
|
58
|
+
# hash, which is a lost defence-in-depth layer and an interop asymmetry with
|
|
59
|
+
# conformant verifiers.
|
|
53
60
|
_ALGORITHMS = {
|
|
54
|
-
'RS256': (RSAAlgorithm, RSAAlgorithm.SHA256),
|
|
55
|
-
'RS384': (RSAAlgorithm, RSAAlgorithm.SHA384),
|
|
56
|
-
'RS512': (RSAAlgorithm, RSAAlgorithm.SHA512),
|
|
57
|
-
'ES256': (ECAlgorithm, ECAlgorithm.SHA256),
|
|
58
|
-
'ES384': (ECAlgorithm, ECAlgorithm.SHA384),
|
|
59
|
-
'ES512': (ECAlgorithm, ECAlgorithm.SHA512),
|
|
60
|
-
'EdDSA': (OKPAlgorithm, None),
|
|
61
|
+
'RS256': (RSAAlgorithm, RSAAlgorithm.SHA256, None),
|
|
62
|
+
'RS384': (RSAAlgorithm, RSAAlgorithm.SHA384, None),
|
|
63
|
+
'RS512': (RSAAlgorithm, RSAAlgorithm.SHA512, None),
|
|
64
|
+
'ES256': (ECAlgorithm, ECAlgorithm.SHA256, ec.SECP256R1),
|
|
65
|
+
'ES384': (ECAlgorithm, ECAlgorithm.SHA384, ec.SECP384R1),
|
|
66
|
+
'ES512': (ECAlgorithm, ECAlgorithm.SHA512, ec.SECP521R1),
|
|
67
|
+
'EdDSA': (OKPAlgorithm, None, None),
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
|
|
@@ -65,9 +72,11 @@ def _algo_for(alg_name: str) -> Any:
|
|
|
65
72
|
entry = _ALGORITHMS.get(alg_name)
|
|
66
73
|
if entry is None:
|
|
67
74
|
raise RouteMissingError(f"Algorithm {alg_name!r} is not supported")
|
|
68
|
-
cls, hash_id = entry
|
|
75
|
+
cls, hash_id, expected_curve = entry
|
|
69
76
|
if hash_id is None:
|
|
70
77
|
return cls()
|
|
78
|
+
if expected_curve is not None:
|
|
79
|
+
return cls(hash_id, expected_curve)
|
|
71
80
|
return cls(hash_id)
|
|
72
81
|
|
|
73
82
|
|
|
@@ -104,7 +113,13 @@ def sign(header_dict: Dict[str, Any], payload_dict: Dict[str, Any], key: Any) ->
|
|
|
104
113
|
try:
|
|
105
114
|
prepared = algo.prepare_key(_prepare_key_arg(key))
|
|
106
115
|
return cast(bytes, algo.sign(signing_input, prepared))
|
|
107
|
-
except (InvalidKeyError, ValueError) as exc:
|
|
116
|
+
except (InvalidKeyError, ValueError, TypeError, AttributeError) as exc:
|
|
117
|
+
# prepare_key raises a bare TypeError ("Expecting a PEM-formatted key.")
|
|
118
|
+
# when handed a live cryptography object of the wrong family — the OB1
|
|
119
|
+
# path passes Badge.priv_key as a live object via _prepare_key_arg
|
|
120
|
+
# (#215). Mirror verify_block's except tuple so a mismatched key family
|
|
121
|
+
# surfaces as SignatureError, not a raw traceback out of the signer
|
|
122
|
+
# (#288).
|
|
108
123
|
raise SignatureError(str(exc)) from exc
|
|
109
124
|
|
|
110
125
|
|
|
@@ -150,6 +165,16 @@ def verify_block(msg: Union[str, bytes], key: Optional[Any] = None) -> bool:
|
|
|
150
165
|
"Algorithm %r in JWS header is not allowed for this key type"
|
|
151
166
|
% alg_name)
|
|
152
167
|
|
|
168
|
+
# RFC 7515 §4.1.11: a verifier MUST reject a JWS whose 'crit' header names
|
|
169
|
+
# extensions it does not understand. This library understands none, so any
|
|
170
|
+
# present 'crit' (even an empty list — non-conformant but still a signal the
|
|
171
|
+
# issuer intended critical processing) fails closed rather than silently
|
|
172
|
+
# dropping the constraint while a conformant verifier would reject (#292).
|
|
173
|
+
if 'crit' in header:
|
|
174
|
+
raise SignatureError(
|
|
175
|
+
"JWS header requires unsupported 'crit' extensions: %r"
|
|
176
|
+
% (header.get('crit'),))
|
|
177
|
+
|
|
153
178
|
signing_input = head_b64 + b'.' + payload_b64
|
|
154
179
|
try:
|
|
155
180
|
raw_sig = utils.from_base64(sig_b64)
|
|
@@ -52,6 +52,45 @@ revocationList = revoked.json
|
|
|
52
52
|
; imsglobal vct.
|
|
53
53
|
;sd_jwt_vct = ${publish_url}vct/openbadge
|
|
54
54
|
|
|
55
|
+
; OpenID for Verifiable Credential Issuance (OID4VCI), issuer side. Optional and
|
|
56
|
+
; entirely opt-in: without this section nothing changes, and a badge is only
|
|
57
|
+
; offered over OID4VCI if its own 'oid4vci_formats' key says so.
|
|
58
|
+
;
|
|
59
|
+
; This library does NOT run the endpoints. It builds the offers, verifies the
|
|
60
|
+
; wallet's key proof (via openvc-core, the [oid4vci] extra) and issues the
|
|
61
|
+
; credential; you mount the routes in your own web framework at the URLs below.
|
|
62
|
+
;
|
|
63
|
+
; NOT a HAIP-conformant issuer: no DPoP, no key attestation checking, no client
|
|
64
|
+
; authentication and no authorization code flow. Pre-authorized code only.
|
|
65
|
+
;[oid4vci]
|
|
66
|
+
; The Credential Issuer Identifier: the https base URL where you serve
|
|
67
|
+
; /.well-known/openid-credential-issuer. Every wallet key proof binds its 'aud'
|
|
68
|
+
; to this exact string, so it must match what you actually serve. Default:
|
|
69
|
+
; [issuer] publish_url.
|
|
70
|
+
;credential_issuer = https://openbadges.issuer.badge/issuer/
|
|
71
|
+
; The three endpoints you mount. Default: derived from credential_issuer.
|
|
72
|
+
;credential_endpoint = https://openbadges.issuer.badge/issuer/credential
|
|
73
|
+
;nonce_endpoint = https://openbadges.issuer.badge/issuer/nonce
|
|
74
|
+
;token_endpoint = https://openbadges.issuer.badge/issuer/token
|
|
75
|
+
; Where the flow's short-lived state lives (pre-authorized codes, access tokens,
|
|
76
|
+
; spent nonces). SQLite, single host: NOT on NFS/SMB, where its locking breaks.
|
|
77
|
+
; The directory is created 0700 and holds recipient PII — do not back it up.
|
|
78
|
+
; Default: ${paths:base}/oid4vci.sqlite3
|
|
79
|
+
;store_path = ${paths:base}/oid4vci.sqlite3
|
|
80
|
+
; Lifetimes, in seconds. The offer TTL bounds how long a QR code stays
|
|
81
|
+
; redeemable; keep it short, it is a bearer secret.
|
|
82
|
+
;offer_ttl_s = 600
|
|
83
|
+
;nonce_ttl_s = 120
|
|
84
|
+
;token_ttl_s = 300
|
|
85
|
+
; How stale a wallet's key proof may be before it is refused.
|
|
86
|
+
;proof_max_age_s = 300
|
|
87
|
+
; Credentials one Credential Request may claim. Leave at 1 unless you have a
|
|
88
|
+
; reason: a batch mints one credential per key proof off a single grant.
|
|
89
|
+
;batch_size = 1
|
|
90
|
+
; The out-of-band PIN, when an offer asks for one. 'numeric' or 'text'.
|
|
91
|
+
;tx_code_length = 6
|
|
92
|
+
;tx_code_input_mode = numeric
|
|
93
|
+
|
|
55
94
|
;Badge configuration sections.
|
|
56
95
|
[badge_1]
|
|
57
96
|
name = Badge 1
|
|
@@ -98,6 +137,12 @@ key_type = RSA
|
|
|
98
137
|
; lapses; pick a cadence you can sustain (e.g. 7 => weekly). Unset = no bound
|
|
99
138
|
; (offline-friendly, but publishes a loud warning).
|
|
100
139
|
;status_validity_days = 7
|
|
140
|
+
; OID4VCI opt-in for this badge (comma-separated subset of: jwt_vc_json,
|
|
141
|
+
; vc+sd-jwt). Unset means the badge is not published in the issuer metadata and
|
|
142
|
+
; cannot be offered to a wallet. jwt_vc_json is revocable via the status lists
|
|
143
|
+
; above; vc+sd-jwt is the EUDI track and is IRREVOCABLE, so it cannot be
|
|
144
|
+
; combined with status_lists. vc+sd-jwt also needs key_type = ED25519 or ECC.
|
|
145
|
+
;oid4vci_formats = jwt_vc_json
|
|
101
146
|
;mail = ${paths:base}/badge_1_mail.txt
|
|
102
147
|
|
|
103
148
|
[badge_2]
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
from configparser import ConfigParser, ExtendedInterpolation, Error as ConfigParserError
|
|
25
25
|
from dataclasses import dataclass
|
|
26
|
-
from typing import Dict, List, Optional, TYPE_CHECKING
|
|
26
|
+
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
|
|
27
27
|
import os
|
|
28
28
|
import sys
|
|
29
29
|
import logging
|
|
@@ -197,6 +197,189 @@ def ob3_status_config(conf: ConfigParser,
|
|
|
197
197
|
validity_days=validity_days)
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
# ── OID4VCI (OpenID for Verifiable Credential Issuance) ──────────────────────
|
|
201
|
+
# Issuing to a wallet inverts the library's usual direction: instead of the
|
|
202
|
+
# issuer pushing a badge to a known recipient, the wallet claims it against an
|
|
203
|
+
# offer and proves possession of its own key. That needs a handful of
|
|
204
|
+
# deployment values the rest of the config has no reason to carry — the public
|
|
205
|
+
# identifier the wallet's key proofs bind to, and the lifetimes of the
|
|
206
|
+
# short-lived secrets the flow mints.
|
|
207
|
+
#
|
|
208
|
+
# The whole section is optional: without it, nothing else in the library
|
|
209
|
+
# changes behaviour.
|
|
210
|
+
|
|
211
|
+
#: How long a Credential Offer's pre-authorized code stays redeemable. Short by
|
|
212
|
+
#: default: it is a bearer secret that travels in a QR code or a link.
|
|
213
|
+
DEFAULT_OFFER_TTL_S = 600
|
|
214
|
+
|
|
215
|
+
#: Lifetime of a c_nonce handed out by the nonce endpoint.
|
|
216
|
+
DEFAULT_NONCE_TTL_S = 120
|
|
217
|
+
|
|
218
|
+
#: Lifetime of an access token minted at the token endpoint. It only has to
|
|
219
|
+
#: survive the wallet's immediate follow-up call to the credential endpoint.
|
|
220
|
+
DEFAULT_TOKEN_TTL_S = 300
|
|
221
|
+
|
|
222
|
+
#: How stale a wallet key proof's `iat` may be (openvc's max_age_s).
|
|
223
|
+
DEFAULT_PROOF_MAX_AGE_S = 300
|
|
224
|
+
|
|
225
|
+
#: Digits in a numeric tx_code, and the input modes OID4VCI 1.0 defines.
|
|
226
|
+
DEFAULT_TX_CODE_LENGTH = 6
|
|
227
|
+
TX_CODE_INPUT_MODES = ('numeric', 'text')
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class OID4VCIConfig:
|
|
232
|
+
"""Resolved [oid4vci] section: the issuer identifier, the endpoint URLs and
|
|
233
|
+
the lifetimes of the flow's short-lived secrets.
|
|
234
|
+
|
|
235
|
+
``credential_issuer`` is load-bearing beyond being an identifier: it is the
|
|
236
|
+
value every wallet key proof must name in its ``aud``, so it has to match
|
|
237
|
+
byte for byte what is actually served at
|
|
238
|
+
``<credential_issuer>/.well-known/openid-credential-issuer``."""
|
|
239
|
+
|
|
240
|
+
credential_issuer: str
|
|
241
|
+
credential_endpoint: str
|
|
242
|
+
nonce_endpoint: str
|
|
243
|
+
token_endpoint: str
|
|
244
|
+
store_path: str
|
|
245
|
+
offer_ttl_s: int = DEFAULT_OFFER_TTL_S
|
|
246
|
+
nonce_ttl_s: int = DEFAULT_NONCE_TTL_S
|
|
247
|
+
token_ttl_s: int = DEFAULT_TOKEN_TTL_S
|
|
248
|
+
proof_max_age_s: int = DEFAULT_PROOF_MAX_AGE_S
|
|
249
|
+
batch_size: int = 1
|
|
250
|
+
tx_code_length: int = DEFAULT_TX_CODE_LENGTH
|
|
251
|
+
tx_code_input_mode: str = 'numeric'
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _positive_int(conf: ConfigParser, section: str, key: str,
|
|
255
|
+
default: int) -> int:
|
|
256
|
+
"""Read a positive integer config key, or its default when unset."""
|
|
257
|
+
raw = (conf[section].get(key) or '').strip() if section in conf else ''
|
|
258
|
+
if not raw:
|
|
259
|
+
return default
|
|
260
|
+
try:
|
|
261
|
+
value = int(raw)
|
|
262
|
+
except ValueError:
|
|
263
|
+
raise ConfigError("[%s] %s must be an integer, got %r"
|
|
264
|
+
% (section, key, raw)) from None
|
|
265
|
+
if value <= 0:
|
|
266
|
+
raise ConfigError("[%s] %s must be positive" % (section, key))
|
|
267
|
+
return value
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def oid4vci_config(conf: ConfigParser) -> OID4VCIConfig:
|
|
271
|
+
"""Resolve the optional [oid4vci] section.
|
|
272
|
+
|
|
273
|
+
Every key has a default, so a typical deployment writes none of them: the
|
|
274
|
+
issuer identifier falls back to ``[issuer] publish_url`` and the three
|
|
275
|
+
endpoints are derived from it. Raises :class:`ConfigError` when there is no
|
|
276
|
+
base to derive an issuer identifier from, when it is not https, or when a
|
|
277
|
+
lifetime is not a positive integer.
|
|
278
|
+
|
|
279
|
+
``https`` is required rather than encouraged. A key proof's ``aud`` binds
|
|
280
|
+
the wallet's signature to this exact string; over plaintext http an attacker
|
|
281
|
+
who can rewrite the metadata response also chooses what the wallet signs
|
|
282
|
+
for, which makes the binding decorative.
|
|
283
|
+
"""
|
|
284
|
+
from urllib.parse import urljoin
|
|
285
|
+
|
|
286
|
+
section = conf['oid4vci'] if 'oid4vci' in conf else None
|
|
287
|
+
base = (section.get('credential_issuer') if section else None) or \
|
|
288
|
+
conf['issuer'].get('publish_url') or ''
|
|
289
|
+
base = base.strip()
|
|
290
|
+
if not base:
|
|
291
|
+
raise ConfigError(
|
|
292
|
+
"[oid4vci] needs a credential_issuer (or an [issuer] publish_url "
|
|
293
|
+
"to derive it from): it is the identifier wallet key proofs bind "
|
|
294
|
+
"their 'aud' to")
|
|
295
|
+
if not base.startswith('https://'):
|
|
296
|
+
raise ConfigError(
|
|
297
|
+
"[oid4vci] credential_issuer must be an https URL, got %r — a "
|
|
298
|
+
"wallet key proof's audience binding is meaningless over plaintext"
|
|
299
|
+
% base)
|
|
300
|
+
# A trailing slash makes urljoin append rather than replace the last path
|
|
301
|
+
# segment; without it, an issuer at https://host/issuer would derive
|
|
302
|
+
# https://host/credential.
|
|
303
|
+
endpoint_base = base if base.endswith('/') else base + '/'
|
|
304
|
+
|
|
305
|
+
def endpoint(key: str, default_path: str) -> str:
|
|
306
|
+
explicit = (section.get(key) or '').strip() if section else ''
|
|
307
|
+
return explicit or urljoin(endpoint_base, default_path)
|
|
308
|
+
|
|
309
|
+
store_path = (section.get('store_path') if section else None) or \
|
|
310
|
+
os.path.join(conf['paths']['base'], 'oid4vci.sqlite3')
|
|
311
|
+
|
|
312
|
+
input_mode = ((section.get('tx_code_input_mode') if section else None)
|
|
313
|
+
or 'numeric').strip()
|
|
314
|
+
if input_mode not in TX_CODE_INPUT_MODES:
|
|
315
|
+
raise ConfigError("[oid4vci] tx_code_input_mode must be one of %s, "
|
|
316
|
+
"got %r" % (', '.join(TX_CODE_INPUT_MODES),
|
|
317
|
+
input_mode))
|
|
318
|
+
|
|
319
|
+
return OID4VCIConfig(
|
|
320
|
+
credential_issuer=base,
|
|
321
|
+
credential_endpoint=endpoint('credential_endpoint', 'credential'),
|
|
322
|
+
nonce_endpoint=endpoint('nonce_endpoint', 'nonce'),
|
|
323
|
+
token_endpoint=endpoint('token_endpoint', 'token'),
|
|
324
|
+
store_path=store_path,
|
|
325
|
+
offer_ttl_s=_positive_int(conf, 'oid4vci', 'offer_ttl_s',
|
|
326
|
+
DEFAULT_OFFER_TTL_S),
|
|
327
|
+
nonce_ttl_s=_positive_int(conf, 'oid4vci', 'nonce_ttl_s',
|
|
328
|
+
DEFAULT_NONCE_TTL_S),
|
|
329
|
+
token_ttl_s=_positive_int(conf, 'oid4vci', 'token_ttl_s',
|
|
330
|
+
DEFAULT_TOKEN_TTL_S),
|
|
331
|
+
proof_max_age_s=_positive_int(conf, 'oid4vci', 'proof_max_age_s',
|
|
332
|
+
DEFAULT_PROOF_MAX_AGE_S),
|
|
333
|
+
batch_size=_positive_int(conf, 'oid4vci', 'batch_size', 1),
|
|
334
|
+
tx_code_length=_positive_int(conf, 'oid4vci', 'tx_code_length',
|
|
335
|
+
DEFAULT_TX_CODE_LENGTH),
|
|
336
|
+
tx_code_input_mode=input_mode,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def oid4vci_formats(conf: ConfigParser, badge_section: str) -> Tuple[str, ...]:
|
|
341
|
+
"""The OID4VCI credential formats a badge section opts into, in order.
|
|
342
|
+
|
|
343
|
+
Returns an empty tuple when the badge does not opt in (no
|
|
344
|
+
``oid4vci_formats`` key), which callers treat as "this badge is not offered
|
|
345
|
+
over OID4VCI" — the same contract as :func:`ob3_status_config` returning
|
|
346
|
+
None. Opt-in rather than opt-out because listing a badge in the issuer
|
|
347
|
+
metadata publishes its existence and makes it claimable.
|
|
348
|
+
|
|
349
|
+
Raises :class:`ConfigError` for an unknown format, or for ``vc+sd-jwt`` on a
|
|
350
|
+
section with ``key_type = RSA``: SD-JWT VC has no RSA algorithm profile, so
|
|
351
|
+
that pairing could never issue and the operator should learn about it when
|
|
352
|
+
the config loads, not when a wallet is waiting.
|
|
353
|
+
"""
|
|
354
|
+
from .keys import KeyType
|
|
355
|
+
from .oid4vci.formats import EC_ONLY_FORMATS, OID4VCI_FORMATS
|
|
356
|
+
|
|
357
|
+
raw = conf[badge_section].get('oid4vci_formats', '')
|
|
358
|
+
formats: List[str] = []
|
|
359
|
+
for piece in raw.split(','):
|
|
360
|
+
fmt = piece.strip()
|
|
361
|
+
if not fmt:
|
|
362
|
+
continue
|
|
363
|
+
if fmt not in OID4VCI_FORMATS:
|
|
364
|
+
raise ConfigError(
|
|
365
|
+
"[%s] oid4vci_formats: unknown format %r (choose from %s)"
|
|
366
|
+
% (badge_section, fmt, ', '.join(OID4VCI_FORMATS)))
|
|
367
|
+
if fmt not in formats:
|
|
368
|
+
formats.append(fmt)
|
|
369
|
+
if not formats:
|
|
370
|
+
return ()
|
|
371
|
+
|
|
372
|
+
key_type = resolve_key_type(conf[badge_section].get('key_type'))
|
|
373
|
+
if key_type is KeyType.RSA:
|
|
374
|
+
rsa_incapable = [f for f in formats if f in EC_ONLY_FORMATS]
|
|
375
|
+
if rsa_incapable:
|
|
376
|
+
raise ConfigError(
|
|
377
|
+
"[%s] oid4vci_formats includes %s, which cannot be signed with "
|
|
378
|
+
"an RSA key — use key_type = ED25519 or ECC, or drop that "
|
|
379
|
+
"format" % (badge_section, ', '.join(rsa_incapable)))
|
|
380
|
+
return tuple(formats)
|
|
381
|
+
|
|
382
|
+
|
|
200
383
|
# ── centralized config defaults ──────────────────────────────────────────────
|
|
201
384
|
# One inventory for the accepted-key defaults, so each lives in a single place
|
|
202
385
|
# rather than being re-spelled at every read site. (The status-list defaults —
|
|
@@ -285,7 +468,9 @@ def badge_section_config(conf: ConfigParser,
|
|
|
285
468
|
#: Sections whose values are the *public* identifiers of a deployment, checked
|
|
286
469
|
#: by :func:`reject_url_userinfo`. [smtp] is deliberately excluded: its
|
|
287
470
|
#: username/password are credentials that stay on the issuer's machine.
|
|
288
|
-
|
|
471
|
+
#: [oid4vci] belongs here because its credential_issuer and endpoint URLs are
|
|
472
|
+
#: published in the issuer metadata and handed to every wallet that reads it.
|
|
473
|
+
_PUBLIC_URL_SECTIONS = ('issuer', 'oid4vci')
|
|
289
474
|
_PUBLIC_URL_SECTION_PREFIX = 'badge_'
|
|
290
475
|
|
|
291
476
|
|
|
@@ -31,8 +31,7 @@ class LibOpenBadgesException(Exception):
|
|
|
31
31
|
|
|
32
32
|
LibOpenBadgesException
|
|
33
33
|
├── KeyGenExceptions (alias: KeyGenException)
|
|
34
|
-
│ ├── GenPrivateKeyError
|
|
35
|
-
│ ├── PrivateKeySaveError / PublicKeySaveError
|
|
34
|
+
│ ├── GenPrivateKeyError
|
|
36
35
|
│ ├── PrivateKeyReadError / PublicKeyReadError
|
|
37
36
|
│ └── UnknownKeyType
|
|
38
37
|
├── SignerExceptions (alias: SignerException)
|
|
@@ -47,9 +46,10 @@ class LibOpenBadgesException(Exception):
|
|
|
47
46
|
├── IssuanceError
|
|
48
47
|
├── DecompressionLimitExceeded
|
|
49
48
|
├── StatusError (issuer-side revocation)
|
|
50
|
-
│ └── StatusListFull / UnknownCredential /
|
|
51
|
-
│ /
|
|
52
|
-
|
|
49
|
+
│ └── StatusListFull / UnknownCredential / AlreadyRevoked
|
|
50
|
+
│ / AlreadySuspended / NotSuspended / RegistryCorrupt
|
|
51
|
+
├── PublishError (openbadgeslib.ob3.publish)
|
|
52
|
+
│ └── CredentialNotFound / AmbiguousCredential
|
|
53
53
|
├── OB2VerificationError (openbadgeslib.ob2.verifier)
|
|
54
54
|
└── OB3VerificationError (openbadgeslib.ob3.verifier)
|
|
55
55
|
|
|
@@ -89,18 +89,6 @@ class GenPrivateKeyError(KeyGenExceptions):
|
|
|
89
89
|
pass
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
class GenPublicKeyError(KeyGenExceptions):
|
|
93
|
-
pass
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class PrivateKeySaveError(KeyGenExceptions):
|
|
97
|
-
pass
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class PublicKeySaveError(KeyGenExceptions):
|
|
101
|
-
pass
|
|
102
|
-
|
|
103
|
-
|
|
104
92
|
class PrivateKeyReadError(KeyGenExceptions):
|
|
105
93
|
pass
|
|
106
94
|
|
|
@@ -201,10 +189,6 @@ class UnknownCredential(StatusError):
|
|
|
201
189
|
pass
|
|
202
190
|
|
|
203
191
|
|
|
204
|
-
class AmbiguousCredential(StatusError):
|
|
205
|
-
pass
|
|
206
|
-
|
|
207
|
-
|
|
208
192
|
class AlreadyRevoked(StatusError):
|
|
209
193
|
pass
|
|
210
194
|
|