openbadgeslib 3.14.0__tar.gz → 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/Changelog.txt +316 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/PKG-INFO +7 -5
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/__init__.py +51 -19
- openbadgeslib-4.0.0/openbadgeslib/_jsonmodel.py +86 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/__init__.py +39 -2
- openbadgeslib-4.0.0/openbadgeslib/_ob1_api.py +41 -0
- openbadgeslib-4.0.0/openbadgeslib/badge_model.py +199 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/baking.py +17 -9
- openbadgeslib-4.0.0/openbadgeslib/cli_common.py +63 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/config.ini.example +15 -0
- openbadgeslib-4.0.0/openbadgeslib/confparser.py +414 -0
- openbadgeslib-4.0.0/openbadgeslib/errors.py +221 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/issue.py +55 -30
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/keys.py +8 -33
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/mail.py +1 -1
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/__init__.py +3 -7
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/badge.py +11 -155
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/verifier.py +3 -3
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/models.py +7 -41
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/signer.py +4 -4
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/verifier.py +5 -3
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/__init__.py +15 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/__init__.py +24 -2
- openbadgeslib-4.0.0/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +9 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/credential.py +15 -43
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/did.py +40 -11
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/eudi.py +69 -5
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/ldp.py +51 -13
- openbadgeslib-4.0.0/openbadgeslib/ob3/publish.py +459 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/signer.py +41 -16
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status.py +3 -3
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status_list.py +5 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/verifier.py +47 -18
- openbadgeslib-4.0.0/openbadgeslib/openbadges_cli.py +98 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_init.py +26 -13
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_keygenerator.py +15 -26
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_publish.py +143 -329
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_signer.py +23 -27
- openbadgeslib-4.0.0/openbadgeslib/openbadges_status.py +75 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_verifier.py +119 -191
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/util.py +159 -5
- openbadgeslib-4.0.0/openbadgeslib/verify.py +295 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/PKG-INFO +7 -5
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/SOURCES.txt +14 -3
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/entry_points.txt +1 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/requires.txt +8 -4
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/pyproject.toml +35 -5
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/conftest.py +9 -9
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_baking.py +56 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_cli_json.py +2 -2
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_cli_smoke.py +93 -16
- openbadgeslib-4.0.0/tests/test_confparser.py +492 -0
- openbadgeslib-4.0.0/tests/test_errors.py +92 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_issue.py +19 -0
- openbadgeslib-4.0.0/tests/test_jsonmodel.py +69 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_jws.py +43 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_keys.py +3 -10
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob1_badge_io.py +1 -1
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob1_deprecation.py +11 -11
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob1_signer.py +11 -12
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob1_verifier.py +2 -2
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob2_cli.py +5 -1
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob2_verifier.py +24 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_conformance_schema.py +4 -2
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_contexts.py +11 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_did_doc.py +50 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_endorsement.py +23 -2
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_eudi_sd_jwt.py +63 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_eudi_x5c.py +61 -5
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp.py +61 -5
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_ecdsa_sd.py +31 -4
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_publish_cli.py +193 -3
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_signer.py +59 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_status.py +123 -5
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_status_registry.py +30 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_verifier.py +48 -2
- openbadgeslib-4.0.0/tests/test_openbadges_cli.py +103 -0
- openbadgeslib-4.0.0/tests/test_public_api.py +78 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_util.py +141 -4
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_verifier_cli_coverage.py +30 -15
- openbadgeslib-4.0.0/tests/test_verify.py +113 -0
- openbadgeslib-3.14.0/openbadgeslib/badge.py +0 -21
- openbadgeslib-3.14.0/openbadgeslib/confparser.py +0 -246
- openbadgeslib-3.14.0/openbadgeslib/errors.py +0 -137
- openbadgeslib-3.14.0/openbadgeslib/signer.py +0 -13
- openbadgeslib-3.14.0/openbadgeslib/verifier.py +0 -13
- openbadgeslib-3.14.0/tests/test_confparser.py +0 -199
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/CONTRIBUTING.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/GOVERNANCE.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/LICENSE.txt +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/MANIFEST.in +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/README.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/SECURITY.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/docs/README.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status_registry.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/setup.cfg +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/config1.ini +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/conformance/README.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_docs.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_examples.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_key_operation.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_mail.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob2_error_branches.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_credential.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_did.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_eudi_type_metadata.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_errors.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_sign.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_signer_batch.py +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-3.14.0 → openbadgeslib-4.0.0}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,322 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v4.0.0 - 2026-07-22
|
|
8
|
+
|
|
9
|
+
Grouped breaking release (#170) — freeze the declared public contract and
|
|
10
|
+
unify the CLIs — shipped together with the Round-2 roadmap work: close the
|
|
11
|
+
revocation/trust gaps the deep-logic analysis surfaced, test the fail-closed
|
|
12
|
+
guards, and speed up batch issuance and verification. Breaking changes first,
|
|
13
|
+
then the rest of the cycle.
|
|
14
|
+
|
|
15
|
+
- fix(cli)!: unify every console tool on the 0/1/2 exit-code contract (#233).
|
|
16
|
+
The 19 `sys.exit(-1)` sites (which exited 255) now exit 1, and the verifier
|
|
17
|
+
applies the same status in human mode as under --json: 1 on any failure, 2
|
|
18
|
+
when the signature is valid but the issuer is not anchored (an OB2
|
|
19
|
+
badge-embedded key or a self-asserted did:key), and 0 only when the badge is
|
|
20
|
+
valid AND trusted. A partial batch sign or publish, and a stale/missing
|
|
21
|
+
artifact under `publish --check-live`, now exit 2 in human mode too (they
|
|
22
|
+
exited 1 before). BREAKING: a wrapper that keyed on 255, or that treated a
|
|
23
|
+
valid-but-untrusted badge's exit 0 as success, must be updated.
|
|
24
|
+
- feat(cli): add a unified `openbadges <command>` front-end and centralise the
|
|
25
|
+
shared flags (#234). A new `openbadgeslib/cli_common.py` provides argparse
|
|
26
|
+
parent parsers for -c/--config, -d/--debug, --json and -v/--version, which
|
|
27
|
+
all five build_parser()s now compose — so the flags are defined once with
|
|
28
|
+
consistent help. openbadges-publish gains -d/--debug (it had none) and
|
|
29
|
+
openbadges-init gains argparse (hence -v/--version, and -h now exits 0 via
|
|
30
|
+
argparse). The new `openbadges` entry point dispatches to init/keygen/sign/
|
|
31
|
+
verify/publish, each of which stays installed under its own openbadges-<name>
|
|
32
|
+
script; a command owns everything after its name, so `openbadges sign ...`
|
|
33
|
+
behaves exactly like `openbadges-signer ...`. Minor breaking: openbadges-init
|
|
34
|
+
with a bad/missing argument now exits 2 (argparse), not 1.
|
|
35
|
+
- feat(cli): add `openbadges status [ID]`, the sixth subcommand promised by
|
|
36
|
+
the unified front-end (#170). It is the read-only registry query that until
|
|
37
|
+
now was reachable only as `openbadges-publish -V 3 --list/--status`: with no
|
|
38
|
+
ID it tabulates every issued OB3 credential and its state, with one (a jti
|
|
39
|
+
or a recipient email) it prints that credential's full record including the
|
|
40
|
+
revocation reason. Auditing a credential is a read, so it no longer requires
|
|
41
|
+
typing a publish verb, nor `-V 3` — status lists are OB3-only by
|
|
42
|
+
construction. The query itself is not duplicated: the command and publish's
|
|
43
|
+
flags both call the shared `query_ob3()`, which now takes plain arguments
|
|
44
|
+
instead of a parsed argparse Namespace. `--list`/`--status` keep working
|
|
45
|
+
unchanged. Unlike the other five commands this one has no standalone
|
|
46
|
+
`openbadges-status` script — those aliases exist only because they predate
|
|
47
|
+
the front-end.
|
|
48
|
+
- refactor(ob1)!: consolidate the OB1 compatibility surface (#235). The OB1
|
|
49
|
+
legacy name table (11 names -> leaf module) is single-sourced in
|
|
50
|
+
openbadgeslib/_ob1_api.py instead of being duplicated between the top-level
|
|
51
|
+
package and openbadgeslib.ob1. The three top-level shim modules —
|
|
52
|
+
openbadgeslib.badge, openbadgeslib.signer and openbadgeslib.verifier — are
|
|
53
|
+
removed. OpenBadges 1.0 stays fully supported and reachable via
|
|
54
|
+
openbadgeslib.ob1.* (e.g. `from openbadgeslib.ob1 import Signer`) and the
|
|
55
|
+
unprefixed openbadgeslib.Signer / .Badge / … re-exports, both of which still
|
|
56
|
+
emit the steering DeprecationWarning. BREAKING: `from openbadgeslib.badge
|
|
57
|
+
import ...` (and .signer/.verifier) no longer resolve — repoint them to
|
|
58
|
+
`openbadgeslib.ob1.<module>`.
|
|
59
|
+
- refactor(keys)!: drop the last pycryptodome / python-ecdsa soft-import
|
|
60
|
+
(#170). Both libraries stopped being dependencies in the 3.7 port (#167),
|
|
61
|
+
but `key_to_pem` kept a soft-import shim that still accepted a *live*
|
|
62
|
+
pycryptodome RSA or python-ecdsa key object. That shim (`_legacy_key_to_pem`)
|
|
63
|
+
is removed, along with the `Crypto.*`/`ecdsa.*` mypy overrides: `key_to_pem`
|
|
64
|
+
now takes only `cryptography` key objects and PEM bytes/str, raising
|
|
65
|
+
`UnknownKeyType` otherwise. Key *files* are unaffected — they were already
|
|
66
|
+
plain PEM and round-trip unchanged. BREAKING: pass a `cryptography` key
|
|
67
|
+
object or PEM instead of a live pycryptodome/python-ecdsa object.
|
|
68
|
+
|
|
69
|
+
- fix(ob3): bundle and reference the OB v3p0 extensions context so the
|
|
70
|
+
`credentialSchema` type (`1EdTechJsonSchemaValidator2019`, emitted since
|
|
71
|
+
v3.6.0) resolves to a defined JSON-LD term. Without it the term was
|
|
72
|
+
undefined: the official 1EdTech OB3 validator rejected every issued
|
|
73
|
+
credential ("Undefined JSON-LD term") and canonicalization mapped the type
|
|
74
|
+
to a base-relative IRI, so the Data Integrity proof no longer verified. The
|
|
75
|
+
offline JSON-Schema gate did not catch this (it checks the string, not
|
|
76
|
+
JSON-LD term resolution). Issued credentials now carry a third `@context`
|
|
77
|
+
entry (the official schema pins only the first two and allows extras); the
|
|
78
|
+
context is pinned in the fail-closed allowlist and covered by the drift
|
|
79
|
+
watchdog (#239).
|
|
80
|
+
- security(config): close the bypass in the userinfo guard below: checking
|
|
81
|
+
`urlsplit(...).netloc` only catches a credential whose `@` lands inside the
|
|
82
|
+
authority, and a password holding a `/`, `?` or `#` pushes it out. Both
|
|
83
|
+
layers accepted `https://user:12345/x@host/` — which `did_web_from_url`
|
|
84
|
+
then turned into the issuer id `did:web:user%3A12345:x%40host`, carried by
|
|
85
|
+
every signed credential — and `https://user:secret/x@host/`, which surfaced
|
|
86
|
+
the password through urllib's own `Port could not be cast to integer value
|
|
87
|
+
as 'secret'` (a message the redaction added below could not reach, since
|
|
88
|
+
urllib raises it). Both layers now refuse an `@` or `%40` *anywhere* in a
|
|
89
|
+
URL-shaped value, and `did_web_from_url` wraps the port parse and quotes
|
|
90
|
+
nothing at all. Values with no authority — `[issuer] email`, local paths —
|
|
91
|
+
still accept an `@`. BREAKING (narrow): a `publish_url` whose path contains
|
|
92
|
+
an `@`, e.g. `https://host/@name/`, is now refused; once parsed it is
|
|
93
|
+
indistinguishable from one hiding a credential (#255).
|
|
94
|
+
- security(config): reject a `[issuer]` / `[badge_*]` URL that embeds
|
|
95
|
+
userinfo (`https://user:password@host/`) when the config is loaded. Those
|
|
96
|
+
URLs are public identifiers: `publish_url` is printed by every publish
|
|
97
|
+
command, `urljoin`ed into the ids written to the publicly served
|
|
98
|
+
organization.json / badge.json / key.json (urljoin preserves the
|
|
99
|
+
`user:pass@`), turned into the issuer's did:web, and used to build the
|
|
100
|
+
`credentialStatus` list URLs signed into every OB3 credential — so a
|
|
101
|
+
password there leaked into user-visible output, files served on the open
|
|
102
|
+
web and the badges delivered to recipients alike. Only one narrow path
|
|
103
|
+
guarded against it (did:web derivation, reached solely by
|
|
104
|
+
`[issuer] did = auto`), and that guard echoed the password back in its own
|
|
105
|
+
error for a non-https URL. The check now lives once in
|
|
106
|
+
`ConfParser.read_conf()` — after `${...}` interpolation, so a reference
|
|
107
|
+
cannot smuggle a credential past it — and every consumer (OB1/OB2/OB3
|
|
108
|
+
publishing, DID derivation, status lists) inherits it; the ConfigError
|
|
109
|
+
names the offending section and key but never the value. `did_web_from_url`
|
|
110
|
+
keeps its own guard as defence in depth, with the userinfo check moved
|
|
111
|
+
ahead of the scheme and host checks and their messages redacted. Behaviour
|
|
112
|
+
change: a config that protected a staging host with credentials in a
|
|
113
|
+
published URL no longer loads — drop the userinfo and use an allow-list or
|
|
114
|
+
a token the library never sees. `[smtp] username` / `password` are
|
|
115
|
+
untouched; they never leave the issuer's machine (#252).
|
|
116
|
+
- security(eudi): document and pin the X.509 / eIDAS trust boundary for
|
|
117
|
+
SD-JWT VC verification. All x5c chain validation is delegated to openvc-core
|
|
118
|
+
(path building, temporal validity, SAN<->iss binding); openbadgeslib only
|
|
119
|
+
guarantees the envelope (the x5c header is present, so the anchors are never
|
|
120
|
+
bypassed by a DID fallback — fail-closed). Crucially, certificate revocation
|
|
121
|
+
(CRL/OCSP) is NOT checked by either layer — a revoked-but-unexpired leaf
|
|
122
|
+
still verifies — so a deployment must apply revocation out of band. The
|
|
123
|
+
division of responsibility is spelled out on `verify_badge_sd_jwt` and the
|
|
124
|
+
Security Model wiki page; new boundary tests assert openvc-core rejects an
|
|
125
|
+
expired/not-yet-valid leaf and pin the revocation gap, and the eudi
|
|
126
|
+
floor/latest CI job now runs them so a drift in the delegate's behaviour
|
|
127
|
+
turns the build red (#236).
|
|
128
|
+
- security(ob3): with `--check-status`, verify the status list's OWN JWT-VC
|
|
129
|
+
signature by default and bind its issuer to the badge's, so a compromised
|
|
130
|
+
status host can no longer silently un-revoke a credential. The badge's
|
|
131
|
+
verification key is reused (publish signs the list with it), or the issuer
|
|
132
|
+
DID is resolved. `verify(check_status=True)` gains `verify_status_list=True`
|
|
133
|
+
by default; the verifier CLI gains `--no-verify-status-list` to opt out for
|
|
134
|
+
issuers that serve an unsigned list (#213).
|
|
135
|
+
- security(ob3): `OB3LdpVerifier` fails closed when it has no pinned key and
|
|
136
|
+
the credential's issuer is not a DID — the proof's `verificationMethod`
|
|
137
|
+
cannot be bound to a non-DID issuer, so resolving and trusting it would
|
|
138
|
+
prove internal consistency, not identity (#214).
|
|
139
|
+
- security(ob3): the endorsement validity window fails closed on a malformed
|
|
140
|
+
`validFrom`/`validUntil` (it previously swallowed the parse error and
|
|
141
|
+
accepted the endorsement, unlike every other window) (#218).
|
|
142
|
+
- fix(ob2, ob3): apply a 60-second clock-skew leeway to every validity window
|
|
143
|
+
(vc `validFrom`/`validUntil`, the status list window, OB2 `issuedOn`/
|
|
144
|
+
`expires`), so a slightly-fast issuer clock no longer false-rejects a
|
|
145
|
+
freshly issued credential (#217).
|
|
146
|
+
- perf(ob3, ob1): load the private-key object once per signer instead of
|
|
147
|
+
re-parsing the PEM on every signature (twice per badge on the JWT-VC path).
|
|
148
|
+
An RSA batch of 1000 badges drops from ~86 s to ~1.5 s; the OB1/OB2 JWS
|
|
149
|
+
path reuses the already-loaded key too (#215).
|
|
150
|
+
- perf(baking): build the baked PNG with a single `b''.join` instead of
|
|
151
|
+
quadratic `bytes` concatenation — ~200x faster on images written with many
|
|
152
|
+
small IDAT chunks (Photoshop/libpng), e.g. ~100 ms -> ~0.5 ms for 6.6 MB
|
|
153
|
+
(#216).
|
|
154
|
+
- test: add negative tests for the fail-closed rejection guards that had none
|
|
155
|
+
— OB2 hosted `startsWith`/`allowedOrigins` reject sides, the OB3 status
|
|
156
|
+
anti-gzip-bomb cap and negative index, the LDP verificationMethod binding,
|
|
157
|
+
and a future JWT `nbf` (#219).
|
|
158
|
+
- ci: the nightly conformance job now fails if a 1EdTech validator does not
|
|
159
|
+
come up or if no conformance test actually ran (all-skipped), instead of
|
|
160
|
+
reporting green, and files (or updates) an issue on a scheduled failure
|
|
161
|
+
(#220).
|
|
162
|
+
- ci: install the `[ldp-sd]` extra explicitly and turn the ecdsa-sd-2023
|
|
163
|
+
selective-disclosure test skip into a hard failure under CI, so an
|
|
164
|
+
openvc-core packaging change can no longer silently disable that suite
|
|
165
|
+
(#221).
|
|
166
|
+
- perf(ob3): add an opt-in `CachingDownloader` so verifying a batch of
|
|
167
|
+
credentials from one issuer fetches each did:web document and status list
|
|
168
|
+
once rather than per credential. `OB3LdpVerifier` also memoizes each
|
|
169
|
+
resolved `verificationMethod` per instance, and `OB3Verifier.verify` /
|
|
170
|
+
`check_status` accept a `download=` fetcher to thread it through (#228).
|
|
171
|
+
- perf(ob3): tag the bundled JSON-LD `@context`s `static` in the pyld
|
|
172
|
+
document loader, priming pyld's process-global resolved-context cache so
|
|
173
|
+
term definitions are not rebuilt on every canonicalization (~1.6x faster
|
|
174
|
+
Data Integrity sign/verify); per-call `extra_contexts` stay untagged so a
|
|
175
|
+
changing context is never served stale (#229).
|
|
176
|
+
- feat: extract a programmatic verification facade — `openbadgeslib.verify`
|
|
177
|
+
with `verify_badge(data, ob_version, *, pubkey_pem | resolve_did,
|
|
178
|
+
expected_recipient, check_status, …) -> VerifyResult` (fields `valid`,
|
|
179
|
+
`trusted`, and the decoded `credential`/`assertion`). It runs the whole
|
|
180
|
+
verification — token extraction, JWT-VC/LDP auto-detection, issuer-DID
|
|
181
|
+
resolution, trust classification, signature/status — doing no I/O; an
|
|
182
|
+
invalid badge is a result, not an exception. `openbadges-verifier` is now a
|
|
183
|
+
thin presenter over it (byte-identical output). Part of #222.
|
|
184
|
+
- feat: extract a programmatic publication facade —
|
|
185
|
+
`openbadgeslib.ob3.publish_ob3(conf, output, *, revoke | suspend |
|
|
186
|
+
unsuspend, reason, badge, check_live) -> PublishResult`. It runs the OB3
|
|
187
|
+
issuer-artefact regeneration (the did:web document, the signed Bitstring
|
|
188
|
+
Status List credentials, the opt-in SD-JWT VC Type Metadata) and the
|
|
189
|
+
status-registry revoke/suspend/unsuspend transaction — writing the
|
|
190
|
+
artefacts but doing no printing — and raises `PublishError` /
|
|
191
|
+
`CredentialNotFound` / `AmbiguousCredential` instead of exiting.
|
|
192
|
+
`openbadges-publish -V 3` is now a thin presenter over it (byte-identical
|
|
193
|
+
output). Completes #222.
|
|
194
|
+
- fix(issue): move the version-neutral badge config model (`Badge`,
|
|
195
|
+
`BadgeImgType`) out of the legacy `ob1` package into
|
|
196
|
+
`openbadgeslib.badge_model` (re-exported from `ob1.badge` for
|
|
197
|
+
compatibility). `create_from_conf` now reads the OpenBadges 1.0-only
|
|
198
|
+
`verify_key`/`criteria` keys optionally, so issuing an OB3 badge that omits
|
|
199
|
+
them no longer blows up with a raw `KeyError('verify_key')`; a missing
|
|
200
|
+
required key surfaces as `IssuanceError` through the `issue_*` entry points,
|
|
201
|
+
and a missing image raises a typed error instead of `print` + bare
|
|
202
|
+
`IOError` (#223).
|
|
203
|
+
- refactor(errors): consolidate the exception hierarchy so a single
|
|
204
|
+
`except LibOpenBadgesException` traps every library error. `IssuanceError`
|
|
205
|
+
and `DecompressionLimitExceeded` are re-anchored under the root (still
|
|
206
|
+
importable from `openbadgeslib.issue` / `openbadgeslib.baking`); the config
|
|
207
|
+
helpers' bare `ValueError`s become a new `ConfigError` and the signers'
|
|
208
|
+
unsupported-algorithm error becomes `UnsupportedAlgorithm` — both also
|
|
209
|
+
subclass `ValueError` so `except ValueError` callers keep working.
|
|
210
|
+
`_write_badge_and_log` reads the `[logs]` path inside its `try`, so a missing
|
|
211
|
+
`[logs]` section is reported instead of crashing with a traceback after the
|
|
212
|
+
badge is already on disk. Adds singular aliases (`KeyGenException`,
|
|
213
|
+
`SignerException`, `VerifierException`) for the plural base families (#224).
|
|
214
|
+
- refactor(ob2, ob3): deduplicate the shared JSON helpers (`_iso`,
|
|
215
|
+
`_parse_iso`, `_as_dict`, `_require`) into an internal
|
|
216
|
+
`openbadgeslib._jsonmodel`. The two copies had drifted: OB2's `_iso`
|
|
217
|
+
serialised a naive datetime as *local* time (`astimezone` on a naive value)
|
|
218
|
+
while OB3 anchored it to UTC, so the same timestamp emitted a different
|
|
219
|
+
instant in OB2 than in OB3; the single implementation keeps OB3's
|
|
220
|
+
naive=>UTC rule (#225).
|
|
221
|
+
- feat(confparser): resolve config through typed dataclasses with centralized
|
|
222
|
+
defaults. `load_config(path) -> ConfigParser` is the library entry point
|
|
223
|
+
(raises `ConfigError`); `read_config_or_exit` is now a thin CLI wrapper over
|
|
224
|
+
it (and the flagship example uses `load_config`). New `IssuerConfig` /
|
|
225
|
+
`BadgeSectionConfig` dataclasses (extending the `OB3StatusConfig` pattern)
|
|
226
|
+
resolve the `[issuer]` and badge sections and their defaults once (issuer id,
|
|
227
|
+
`criteria_narrative`->`criteria` fallback, hosted-assertions base), and a
|
|
228
|
+
single `DEFAULT_KEY_TYPE` + `resolve_key_type()` centralises the key_type
|
|
229
|
+
name->KeyType mapping the keygenerator used to inline (#225).
|
|
230
|
+
- security(ob3): warn loudly when `openbadges-publish -V 3` regenerates a
|
|
231
|
+
revocable badge's status lists without a `validUntil` bound (no
|
|
232
|
+
`status_validity_days` set). An unbounded list has no anti-replay freshness —
|
|
233
|
+
an authentic older copy with fewer revocations passes the window unchecked,
|
|
234
|
+
so a revoked credential could be silently resurrected. `PublishResult` gains
|
|
235
|
+
a `no_validity_bound` list (surfaced in the CLI's human output and `--json`),
|
|
236
|
+
and the config example documents the republication cadence (#227).
|
|
237
|
+
- security(ob3): SD-JWT VC (EUDI) issuance no longer silently drops a
|
|
238
|
+
credential's `credentialStatus`. Because SD-JWT VC status carriage/checking
|
|
239
|
+
is not wired yet (both verify paths hard-set `require_status=False`), an
|
|
240
|
+
SD-JWT VC badge is irrevocable; `issue_badge_sd_jwt` now rejects a credential
|
|
241
|
+
that carries a `credentialStatus` with a clear `EudiError` (before requiring
|
|
242
|
+
the [eudi] extra) rather than issue a badge the issuer believes is revocable.
|
|
243
|
+
`badge_to_sd_jwt_claims` also carries `credentialSubject.identifier` (the
|
|
244
|
+
hashed recipient identities), so a badge whose subject travelled via
|
|
245
|
+
`identifier` (no `id`) keeps a checkable recipient. The irrevocability is
|
|
246
|
+
documented prominently on the issue/verify docstrings (#226).
|
|
247
|
+
- security(util): pin the SSRF-validated IP on the HTTPS connection to defeat
|
|
248
|
+
DNS rebinding. `_assert_public_host` validated the resolved IPs, but
|
|
249
|
+
`opener.open` then re-resolved on its own, so a hostile DNS (TTL 0) could
|
|
250
|
+
answer public during the check and internal (169.254.169.254 / loopback /
|
|
251
|
+
RFC1918) at the real connect. A new `_PinnedHTTPSConnection` re-resolves,
|
|
252
|
+
re-validates and dials the validated IP in one step (keeping the hostname
|
|
253
|
+
for TLS SNI / certificate verification and the Host header) — for the initial
|
|
254
|
+
request and every redirect. Also adds a wall-clock download budget
|
|
255
|
+
(`MAX_DOWNLOAD_SECONDS`, 60 s), since the per-socket `timeout=30` bounds each
|
|
256
|
+
read but not a slow-drip response's total time (#231).
|
|
257
|
+
- ci: harden the release-critical workflows. SHA-pin the actions that run with
|
|
258
|
+
`id-token: write` — `pypa/gh-action-pypi-publish` (PyPI Trusted Publishing)
|
|
259
|
+
and the GitHub Pages `upload-pages-artifact` / `deploy-pages` — so a
|
|
260
|
+
compromised upstream tag cannot publish or deploy under our OIDC identity
|
|
261
|
+
(Dependabot already tracks github-actions and bumps the pins). Add a
|
|
262
|
+
least-privilege `permissions: contents: read` to commit-lint.yml. Add a
|
|
263
|
+
`windows-latest` CI leg plus a monkeypatched test of the `fcntl=None`
|
|
264
|
+
StatusRegistry lock fallback, so the "OS Independent" classifier is actually
|
|
265
|
+
exercised on the non-POSIX path. The leg immediately caught a real
|
|
266
|
+
portability bug it fixes: `openbadges-publish` now records `files_written`
|
|
267
|
+
with forward slashes (a Windows `\` would have broken the artefact URLs built
|
|
268
|
+
via `urljoin`); the POSIX file-mode assertions skip on Windows and the
|
|
269
|
+
ldp-sd hard-fail guard keys on an explicit `OPENBADGES_REQUIRE_LDP_SD` flag
|
|
270
|
+
(set by the main test job) so the core-only Windows leg skips it (#230).
|
|
271
|
+
- refactor: declare the stable public surface (freeze prep for v4). The root
|
|
272
|
+
package gains an explicit `__all__` (the "public contract"), re-exports
|
|
273
|
+
`KeyEd25519` (the recommended OB 3.0 / LDP key) alongside `KeyRSA`/`KeyECC`,
|
|
274
|
+
and exposes the `ob2`/`ob3`/`errors` subpackages and the
|
|
275
|
+
`issue_from_conf`/`verify_badge` facades by name. `openbadgeslib.ob3`
|
|
276
|
+
re-exports the `eudi` SD-JWT VC track (its heavy imports stay lazy).
|
|
277
|
+
`SignResult.credential`/`.assertion` and the `_Ob3Context` fields are typed
|
|
278
|
+
with `TYPE_CHECKING` forward refs instead of `Any` (zero runtime cost,
|
|
279
|
+
correct autocompletion / pdoc). Adds a "Stable API" wiki page documenting
|
|
280
|
+
the stable-vs-internal boundary (#232).
|
|
281
|
+
- build(deps): the [dev] extra depends on gitlint-core instead of the gitlint
|
|
282
|
+
meta-package, so it stops installing vulnerable pins. `gitlint` is an empty
|
|
283
|
+
shim for `gitlint-core[trusted-deps]`, and that extra hard-pins click==8.1.3
|
|
284
|
+
and sh==1.14.3 — PYSEC-2026-2132 (command injection in `click.edit()`, fixed
|
|
285
|
+
in 8.3.3) and CVE-2026-54552 (incomplete privilege drop in sh's `_uid`, fixed
|
|
286
|
+
in 2.2.4), which had been failing the weekly Audit job. Neither is reachable
|
|
287
|
+
here — gitlint never calls `click.edit()`, and it does not import `sh` at all
|
|
288
|
+
unless GITLINT_USE_SH_LIB=1, which this repo never sets — but upstream has
|
|
289
|
+
been dormant since 2023, so the pins would keep the audit red indefinitely.
|
|
290
|
+
gitlint-core declares the same dependencies loosely and provides the same
|
|
291
|
+
`gitlint` console script; the [dev] extra now floors `click>=8.3.3` and
|
|
292
|
+
`sh>=2.2.4` (keeping gitlint-core's non-Windows marker for sh) and caps both
|
|
293
|
+
below the next major. No pip-audit ignores were added: the advisories are
|
|
294
|
+
resolved by upgrading, not suppressed. Library users are unaffected — click
|
|
295
|
+
and sh were only ever development dependencies.
|
|
296
|
+
- build(deps)!: raise the PyJWT floor to >=2.13 and the openvc-core floors to
|
|
297
|
+
>=1.21. The OB3 JWT-VC verifier decodes an untrusted badge with a
|
|
298
|
+
signature-verifying `jwt.decode()`, so the PyJWT 2.12/2.13 advisory batch is
|
|
299
|
+
on a reachable path: RFC 7515 §4.1.11 `crit` validation (2.12.0,
|
|
300
|
+
CVE-2026-32597) and the b64=false unencoded-payload DoS (2.13.0,
|
|
301
|
+
CVE-2026-48525), plus JWK/alg-binding hardening; 2.13.0 is the minimal floor
|
|
302
|
+
carrying all of them. Installing [eudi]/[ldp-sd] already pulled a safe PyJWT
|
|
303
|
+
transitively via openvc-core (whose own floor is >=2.13), so this closes the
|
|
304
|
+
gap for a base, extras-free install. The native `_jws` engine is structurally
|
|
305
|
+
immune (it never calls `jwt.decode`). The [eudi]/[ldp-sd] openvc-core floors
|
|
306
|
+
move from >=1.18 / >=1.13 to >=1.21 per the pin's stated policy (floor at the
|
|
307
|
+
version the suite is tested against): 1.21 keeps in-floor the delegated
|
|
308
|
+
SD-JWT fixes openbadgeslib relies on — the key-binding replay fix that now
|
|
309
|
+
requires a nonce AND audience (1.19.3) and unknown-`crit` rejection on the
|
|
310
|
+
SD-JWT lane (1.20.4). No openbadgeslib code changed: openvc's new typed
|
|
311
|
+
ecdsa-sd errors (ProofValueMalformed/ProofMalformed) subclass ProofError, so
|
|
312
|
+
the existing `except ProofError` in ob3/ldp.py still catches them; the full
|
|
313
|
+
suite passes against openvc-core 1.21.0. BREAKING only for a caller that
|
|
314
|
+
pinned PyJWT <2.13 or openvc-core <1.21 alongside openbadgeslib.
|
|
315
|
+
- security(cli): `openbadges-init` creates config.ini owner-only (0600). It
|
|
316
|
+
is the file that goes on to hold the SMTP password and the paths to the
|
|
317
|
+
private keys, but the copy ran outside the umask block that hardens the
|
|
318
|
+
directories, so it inherited the caller's umask — 0644 on a default shell.
|
|
319
|
+
The 0700 parent contained it; this is the second lock, matching the one
|
|
320
|
+
the keygenerator puts on the private key.
|
|
321
|
+
|
|
322
|
+
|
|
7
323
|
* v3.14.0 - 2026-07-10
|
|
8
324
|
|
|
9
325
|
A security & correctness hardening pass (audit-bot). Highlights:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Production-ready Python library & CLI for Open Badges 3.0 (W3C Verifiable Credentials): issue, verify, and revoke JWT-VC credentials with Bitstring Status Lists and did:web — plus strict OB 2.0 and legacy OB 1.0.
|
|
5
5
|
Author-email: Luis González Fernández <luisgf@luisgf.es>, Jesús Cea Avión <jcea@jcea.es>
|
|
6
6
|
License: LGPLv3
|
|
@@ -30,22 +30,24 @@ Requires-Python: >=3.10
|
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE.txt
|
|
32
32
|
Requires-Dist: pypng>=0.20220715.0
|
|
33
|
-
Requires-Dist: PyJWT[crypto]>=2.
|
|
33
|
+
Requires-Dist: PyJWT[crypto]>=2.13
|
|
34
34
|
Requires-Dist: cryptography>=42
|
|
35
35
|
Requires-Dist: defusedxml>=0.7
|
|
36
36
|
Provides-Extra: ldp
|
|
37
37
|
Requires-Dist: pyld>=2.0.4; extra == "ldp"
|
|
38
38
|
Provides-Extra: eudi
|
|
39
|
-
Requires-Dist: openvc-core<2,>=1.
|
|
39
|
+
Requires-Dist: openvc-core<2,>=1.21; extra == "eudi"
|
|
40
40
|
Provides-Extra: ldp-sd
|
|
41
|
-
Requires-Dist: openvc-core[data-integrity]<2,>=1.
|
|
41
|
+
Requires-Dist: openvc-core[data-integrity]<2,>=1.21; extra == "ldp-sd"
|
|
42
42
|
Provides-Extra: dev
|
|
43
43
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
44
44
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
45
45
|
Requires-Dist: flake8>=7.0; extra == "dev"
|
|
46
46
|
Requires-Dist: pdoc>=14; extra == "dev"
|
|
47
47
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
48
|
-
Requires-Dist: gitlint>=0.19; extra == "dev"
|
|
48
|
+
Requires-Dist: gitlint-core>=0.19; extra == "dev"
|
|
49
|
+
Requires-Dist: click<9,>=8.3.3; extra == "dev"
|
|
50
|
+
Requires-Dist: sh<3,>=2.2.4; sys_platform != "win32" and extra == "dev"
|
|
49
51
|
Requires-Dist: jsonschema[format-nongpl]>=4.20; extra == "dev"
|
|
50
52
|
Dynamic: license-file
|
|
51
53
|
|
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
+
# ── Submodule entry points ───────────────────────────────────────────────────
|
|
27
|
+
# Bind ob2 / ob3 / errors as attributes so they are part of the explicit public
|
|
28
|
+
# surface (and `from openbadgeslib import *`): ob2 (strict OB 2.0), ob3 (OB 3.0,
|
|
29
|
+
# incl. ob3.eudi and ob3.OB3Ldp*), and the errors hierarchy rooted at
|
|
30
|
+
# LibOpenBadgesException.
|
|
31
|
+
from . import ob2, ob3, errors # noqa: F401
|
|
32
|
+
|
|
26
33
|
# ── OpenBadges 2.0 (strict) ──────────────────────────────────────────────────
|
|
27
34
|
from .ob2 import ( # noqa: F401
|
|
28
35
|
OB2Signer, OB2Verifier, OB2VerificationError,
|
|
@@ -35,17 +42,26 @@ from .ob3 import ( # noqa: F401
|
|
|
35
42
|
)
|
|
36
43
|
|
|
37
44
|
# ── Shared utilities ────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
45
|
+
# KeyEd25519 is the recommended key type for OB 3.0 / LDP (eddsa-rdfc-2022).
|
|
46
|
+
from .keys import KeyFactory, KeyRSA, KeyECC, KeyEd25519 # noqa: F401
|
|
39
47
|
from .util import __version__ # noqa: F401
|
|
40
48
|
|
|
49
|
+
# The OB1 legacy name table, shared with openbadgeslib.ob1 (single source,
|
|
50
|
+
# #235). Importing it is cheap and does not pull in the ob1 subpackage, so a
|
|
51
|
+
# bare `import openbadgeslib` stays ob1-free (see __getattr__ below).
|
|
52
|
+
from ._ob1_api import OB1_API as _OB1_API # noqa: F401
|
|
53
|
+
|
|
41
54
|
|
|
42
|
-
# ── Issuance API
|
|
43
|
-
# "Issue badge X to Y per config" as
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
_ISSUE_API =
|
|
55
|
+
# ── Issuance / verification API ──────────────────────────────────────────────
|
|
56
|
+
# "Issue badge X to Y per config" / "verify this badge" as library calls — the
|
|
57
|
+
# orchestration the CLIs wrap, returning a SignResult / VerifyResult instead of
|
|
58
|
+
# doing I/O (openbadgeslib.issue, openbadgeslib.verify). Resolved lazily (PEP
|
|
59
|
+
# 562, below) so a bare `import openbadgeslib` stays lightweight;
|
|
60
|
+
# `from openbadgeslib.issue import ...` / `.verify import ...` work directly.
|
|
61
|
+
_ISSUE_API = {
|
|
62
|
+
'IssuanceError': 'issue', 'SignResult': 'issue', 'issue_from_conf': 'issue',
|
|
63
|
+
'verify_badge': 'verify',
|
|
64
|
+
}
|
|
49
65
|
|
|
50
66
|
|
|
51
67
|
# ── OpenBadges 1.0 (legacy) ──────────────────────────────────────────────────
|
|
@@ -54,22 +70,17 @@ _ISSUE_API = ('IssuanceError', 'SignResult', 'issue_from_conf')
|
|
|
54
70
|
# OpenBadges 1.0 surface: accessing one emits a DeprecationWarning steering new
|
|
55
71
|
# work to openbadgeslib.ob2 / openbadgeslib.ob3. OB 1.0 itself remains supported
|
|
56
72
|
# (no removal planned). They are resolved lazily from the ob1 leaf modules (PEP
|
|
57
|
-
# 562)
|
|
58
|
-
# package.
|
|
59
|
-
_OB1_API = {
|
|
60
|
-
'Signer': 'signer', 'Verifier': 'verifier', 'VerifyInfo': 'verifier',
|
|
61
|
-
'Badge': 'badge', 'BadgeSigned': 'badge', 'Assertion': 'badge',
|
|
62
|
-
'BadgeStatus': 'badge', 'BadgeImgType': 'badge', 'BadgeType': 'badge',
|
|
63
|
-
'extract_svg_assertion': 'badge', 'extract_png_assertion': 'badge',
|
|
64
|
-
}
|
|
73
|
+
# 562) using the shared _OB1_API table imported above, so a bare
|
|
74
|
+
# `import openbadgeslib` neither warns nor drags in the ob1 package.
|
|
65
75
|
|
|
66
76
|
|
|
67
77
|
def __getattr__(name: str) -> Any:
|
|
68
78
|
if name in _ISSUE_API:
|
|
69
|
-
# The modern issuance API — lazy so a bare import stays
|
|
70
|
-
# warning-free (unlike the OB1 names below).
|
|
79
|
+
# The modern issuance / verification API — lazy so a bare import stays
|
|
80
|
+
# lightweight, but warning-free (unlike the OB1 names below).
|
|
71
81
|
import importlib
|
|
72
|
-
return getattr(
|
|
82
|
+
return getattr(
|
|
83
|
+
importlib.import_module('.' + _ISSUE_API[name], __name__), name)
|
|
73
84
|
module = _OB1_API.get(name)
|
|
74
85
|
if module is None:
|
|
75
86
|
raise AttributeError(
|
|
@@ -83,3 +94,24 @@ def __getattr__(name: str) -> Any:
|
|
|
83
94
|
'the "OpenBadges 1.0 lifecycle" wiki page.' % name,
|
|
84
95
|
DeprecationWarning, stacklevel=2)
|
|
85
96
|
return getattr(importlib.import_module('.ob1.' + module, __name__), name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# The stable, supported top-level surface (the "public contract" — see the
|
|
100
|
+
# "Stable API vs internals" wiki/doc page). Everything else (the ob1.* legacy
|
|
101
|
+
# names resolved lazily above, and any underscore-prefixed name) is internal.
|
|
102
|
+
# The issuance/verification facades are resolved lazily by __getattr__ but are
|
|
103
|
+
# part of the contract, so they are listed here for pdoc and `import *`.
|
|
104
|
+
__all__ = [
|
|
105
|
+
'__version__',
|
|
106
|
+
# Subpackage entry points.
|
|
107
|
+
'ob2', 'ob3', 'errors',
|
|
108
|
+
# OpenBadges 2.0 (strict).
|
|
109
|
+
'OB2Signer', 'OB2Verifier', 'OB2VerificationError',
|
|
110
|
+
# OpenBadges 3.0.
|
|
111
|
+
'OB3Signer', 'OB3Verifier', 'OB3VerificationError',
|
|
112
|
+
'OpenBadgeCredential', 'Achievement', 'Issuer',
|
|
113
|
+
# Keys (KeyEd25519 is the recommended OB 3.0 / LDP key type).
|
|
114
|
+
'KeyFactory', 'KeyRSA', 'KeyECC', 'KeyEd25519',
|
|
115
|
+
# Programmatic issue / verify facades (resolved lazily).
|
|
116
|
+
'issue_from_conf', 'verify_badge', 'IssuanceError', 'SignResult',
|
|
117
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
OpenBadges Library
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
|
|
6
|
+
Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
|
|
7
|
+
|
|
8
|
+
All rights reserved.
|
|
9
|
+
|
|
10
|
+
This library is free software; you can redistribute it and/or
|
|
11
|
+
modify it under the terms of the GNU Lesser General Public
|
|
12
|
+
License as published by the Free Software Foundation; either
|
|
13
|
+
version 3.0 of the License, or (at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This library is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
18
|
+
Lesser General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU Lesser General Public
|
|
21
|
+
License along with this library.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Shared JSON <-> dataclass helpers for the OB2 and OB3 models. These lived in
|
|
25
|
+
# duplicate in openbadgeslib.ob2.models and openbadgeslib.ob3.credential and had
|
|
26
|
+
# drifted: the OB2 copy of ``_iso`` ran ``astimezone`` on a *naive* datetime,
|
|
27
|
+
# which Python interprets as **local** time — so the same value serialised to a
|
|
28
|
+
# different instant in OB2 than in OB3 (which anchors naive datetimes to UTC).
|
|
29
|
+
# This module keeps the OB3 semantics (naive => UTC) as the single source of
|
|
30
|
+
# truth, matching ob3.status_registry._iso_z and the JWT nbf/exp claims.
|
|
31
|
+
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _iso(dt: datetime) -> str:
|
|
37
|
+
"""Return a datetime as an ISO 8601 string with a ``Z`` (UTC) suffix.
|
|
38
|
+
|
|
39
|
+
A naive datetime is assumed to be UTC (not local time), so validFrom/
|
|
40
|
+
validUntil and OB2 issuedOn/expires never silently shift by the host's
|
|
41
|
+
offset and stay consistent with the JWT nbf/exp claims.
|
|
42
|
+
"""
|
|
43
|
+
if dt.tzinfo is None:
|
|
44
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
45
|
+
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_iso(value: Any, where: str = 'date') -> datetime:
|
|
49
|
+
"""Parse an ISO 8601 timestamp (with a UTC offset or trailing ``Z``).
|
|
50
|
+
|
|
51
|
+
A non-string (e.g. a bare Unix timestamp, the legacy OB 1.0 shape) is
|
|
52
|
+
rejected, as is a naive timestamp with no offset — verify() always compares
|
|
53
|
+
against an aware ``datetime.now(timezone.utc)``, so only unambiguously
|
|
54
|
+
anchored timestamps are accepted. Raises ValueError otherwise, naming
|
|
55
|
+
*where* the bad value came from.
|
|
56
|
+
"""
|
|
57
|
+
if not isinstance(value, str):
|
|
58
|
+
raise ValueError("%s must be an ISO 8601 string, got %r" % (where, value))
|
|
59
|
+
try:
|
|
60
|
+
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
raise ValueError("invalid ISO 8601 date in %s: %r" % (where, value)) from exc
|
|
63
|
+
if dt.tzinfo is None:
|
|
64
|
+
raise ValueError("%s is missing a UTC offset: %r" % (where, value))
|
|
65
|
+
return dt
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _as_dict(value: Any, where: str) -> dict[str, Any]:
|
|
69
|
+
"""Return *value* if it is a dict, else raise a clear ValueError."""
|
|
70
|
+
if not isinstance(value, dict):
|
|
71
|
+
raise ValueError("%s must be a JSON object" % where)
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _require(data: dict[str, Any], key: str, where: str) -> str:
|
|
76
|
+
"""Return ``data[key]`` as a non-empty string, raising a clear ValueError if
|
|
77
|
+
it is missing, empty, or not a string. All fields validated here are
|
|
78
|
+
identifiers consumed as strings downstream (e.g. recipient binding calls
|
|
79
|
+
``.lower()``), so a non-string value must be rejected rather than crash
|
|
80
|
+
later."""
|
|
81
|
+
value = data.get(key)
|
|
82
|
+
if value is None or value == "":
|
|
83
|
+
raise ValueError("missing required field %s.%s" % (where, key))
|
|
84
|
+
if not isinstance(value, str):
|
|
85
|
+
raise ValueError("field %s.%s must be a string" % (where, key))
|
|
86
|
+
return value
|
|
@@ -8,9 +8,46 @@ from .exceptions import SignatureError, MissingKey, MissingSigner, MissingVerifi
|
|
|
8
8
|
from jwt.algorithms import RSAAlgorithm, ECAlgorithm, OKPAlgorithm
|
|
9
9
|
from jwt.exceptions import InvalidKeyError
|
|
10
10
|
|
|
11
|
+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
13
|
+
Ed25519PrivateKey, Ed25519PublicKey)
|
|
14
|
+
|
|
11
15
|
from ..keys import KeyType, detect_key_type, key_to_pem
|
|
12
16
|
from ..errors import UnknownKeyType
|
|
13
17
|
|
|
18
|
+
# Key objects PyJWT's ``prepare_key`` accepts as-is (its own supported backends).
|
|
19
|
+
# A live one of these can be handed straight to prepare_key, skipping a PEM
|
|
20
|
+
# round-trip; a PEM str/bytes passes through too. Anything else falls to
|
|
21
|
+
# key_to_pem, which since 4.0.0 accepts only cryptography objects and PEM and so
|
|
22
|
+
# rejects it (UnknownKeyType) — including the pycryptodome / python-ecdsa
|
|
23
|
+
# objects PyJWT never recognised.
|
|
24
|
+
_CRYPTOGRAPHY_KEY_TYPES = (
|
|
25
|
+
rsa.RSAPrivateKey, rsa.RSAPublicKey,
|
|
26
|
+
ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey,
|
|
27
|
+
Ed25519PrivateKey, Ed25519PublicKey,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _prepare_key_arg(key: Any) -> Any:
|
|
32
|
+
"""Return the value to feed a PyJWT algorithm's ``prepare_key``.
|
|
33
|
+
|
|
34
|
+
``prepare_key`` accepts a PEM (str/bytes) *and* a live ``cryptography`` key
|
|
35
|
+
object directly, returning the latter untouched. Passing an already-loaded
|
|
36
|
+
object straight through skips a ``key_to_pem`` re-serialisation plus a
|
|
37
|
+
``load_pem_private_key`` re-parse on every sign: the OB1 path hands in
|
|
38
|
+
``Badge.priv_key`` — a cryptography object loaded once at construction — so
|
|
39
|
+
round-tripping it back to a PEM here only to re-parse it wasted ~45 ms per
|
|
40
|
+
RSA badge (#215). A PEM str/bytes passes through unchanged; anything else
|
|
41
|
+
falls to ``key_to_pem``, which since 4.0.0 accepts only cryptography objects
|
|
42
|
+
and PEM (a live pycryptodome/python-ecdsa object now raises UnknownKeyType).
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(key, (bytes, str)):
|
|
45
|
+
return key
|
|
46
|
+
if isinstance(key, _CRYPTOGRAPHY_KEY_TYPES):
|
|
47
|
+
return key
|
|
48
|
+
return key_to_pem(key)
|
|
49
|
+
|
|
50
|
+
|
|
14
51
|
# Each entry is (algorithm class, hash id). EdDSA's OKPAlgorithm takes no hash
|
|
15
52
|
# argument — its hash id is None and _algo_for constructs it with no args.
|
|
16
53
|
_ALGORITHMS = {
|
|
@@ -65,7 +102,7 @@ def sign(header_dict: Dict[str, Any], payload_dict: Dict[str, Any], key: Any) ->
|
|
|
65
102
|
signing_input = utils.encode(header_dict) + b'.' + utils.encode(payload_dict)
|
|
66
103
|
algo = _algo_for(alg_name)
|
|
67
104
|
try:
|
|
68
|
-
prepared = algo.prepare_key(
|
|
105
|
+
prepared = algo.prepare_key(_prepare_key_arg(key))
|
|
69
106
|
return cast(bytes, algo.sign(signing_input, prepared))
|
|
70
107
|
except (InvalidKeyError, ValueError) as exc:
|
|
71
108
|
raise SignatureError(str(exc)) from exc
|
|
@@ -121,7 +158,7 @@ def verify_block(msg: Union[str, bytes], key: Optional[Any] = None) -> bool:
|
|
|
121
158
|
|
|
122
159
|
algo = _algo_for(alg_name)
|
|
123
160
|
try:
|
|
124
|
-
prepared = algo.prepare_key(
|
|
161
|
+
prepared = algo.prepare_key(_prepare_key_arg(key))
|
|
125
162
|
valid = algo.verify(signing_input, prepared, raw_sig)
|
|
126
163
|
except (InvalidKeyError, ValueError, AttributeError, TypeError) as exc:
|
|
127
164
|
# A private key served where a public key is expected reaches
|