openbadgeslib 3.5.0__tar.gz → 3.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/Changelog.txt +64 -0
- {openbadgeslib-3.5.0/openbadgeslib.egg-info → openbadgeslib-3.6.0}/PKG-INFO +1 -1
- openbadgeslib-3.6.0/SECURITY.md +54 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/_jws/__init__.py +2 -2
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/baking.py +4 -4
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/keys.py +12 -12
- openbadgeslib-3.6.0/openbadgeslib/logs.py +41 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob1/badge.py +5 -5
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob1/verifier.py +2 -2
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob2/models.py +36 -17
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob2/verifier.py +3 -3
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/__init__.py +10 -6
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/credential.py +60 -17
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/did.py +17 -7
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/eudi.py +5 -5
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/ldp.py +13 -13
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/signer.py +4 -4
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status.py +2 -2
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status_list.py +3 -3
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status_registry.py +6 -6
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/verifier.py +12 -3
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_keygenerator.py +16 -11
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_publish.py +15 -3
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_signer.py +18 -15
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/util.py +1 -1
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0/openbadgeslib.egg-info}/PKG-INFO +1 -1
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/SOURCES.txt +1 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/pyproject.toml +8 -6
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob2_models.py +13 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_credential.py +24 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_did.py +6 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_ldp_sign.py +3 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_signer_cli_ldp.py +4 -3
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_verifier.py +59 -0
- openbadgeslib-3.5.0/openbadgeslib/logs.py +0 -85
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/LICENSE.txt +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/MANIFEST.in +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/README.md +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/docs/README.md +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/__init__.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/badge.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/config.ini.example +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/confparser.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/mail.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob1/__init__.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/__init__.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_verifier.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/signer.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib/verifier.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/requires.txt +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/setup.cfg +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/config1.ini +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/conformance/README.md +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/conftest.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/runtests.sh +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_cli_json.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_cli_smoke.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_confparser.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_docs.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_jws.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_key_operation.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob1_badge_io.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob1_verifier.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob2_verifier.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_publish_cli.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_status.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_ob3_status_registry.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_util.py +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-3.5.0 → openbadgeslib-3.6.0}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,70 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v3.6.0 - 2026-07-08
|
|
8
|
+
|
|
9
|
+
- feat(ob3): the verifier now exposes the raw, already-validated VC document
|
|
10
|
+
on the returned credential (`credential.raw`) — on both the JWT-VC and the
|
|
11
|
+
Data Integrity path — so an integrator can read spec fields the dataclass
|
|
12
|
+
does not map (alignment, results, multiple evidence, endorsements, …)
|
|
13
|
+
without re-parsing the token. It is None for an in-memory credential built
|
|
14
|
+
to issue, and excluded from equality/repr so it never affects comparisons.
|
|
15
|
+
|
|
16
|
+
- feat(ob3): issued OB 3.0 credentials now carry a `credentialSchema` entry
|
|
17
|
+
(`1EdTechJsonSchemaValidator2019`) pointing at 1EdTech's published
|
|
18
|
+
AchievementCredential JSON Schema, so validators and wallets can
|
|
19
|
+
self-check them. The offline conformance gate proves our issued
|
|
20
|
+
credentials satisfy that exact schema.
|
|
21
|
+
|
|
22
|
+
- refactor(typing): the library now type-checks clean under `mypy --strict`
|
|
23
|
+
(73 bare `dict` annotations parameterised as `dict[str, Any]`, 20 Any
|
|
24
|
+
returns narrowed with `typing.cast`), and `strict = true` is enabled in
|
|
25
|
+
the mypy config so CI enforces it. This makes the README's long-standing
|
|
26
|
+
`mypy --strict` claim true and improves the shipped `py.typed` annotations
|
|
27
|
+
for downstream type-checkers. Annotations only — no runtime change.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
* v3.5.1 - 2026-07-07
|
|
31
|
+
|
|
32
|
+
- fix(ob3): a naive datetime passed as the issuance/expiration date is now
|
|
33
|
+
assumed UTC (matching status_registry._iso_z), not local time — so
|
|
34
|
+
validFrom/validUntil no longer silently shift by the host's offset and
|
|
35
|
+
stay consistent with the nbf/exp JWT claims.
|
|
36
|
+
|
|
37
|
+
- fix(ob3): the JWT verifier now cross-checks the `jti` claim against the
|
|
38
|
+
credential `id` (OB3 §8.2 binds them). A validly signed token whose jti
|
|
39
|
+
disagrees with — or omits — the credential id is rejected, alongside the
|
|
40
|
+
existing iss/nbf/sub checks.
|
|
41
|
+
|
|
42
|
+
- fix(ob3): `_b58btc_decode` bounds its input to 128 characters before the
|
|
43
|
+
O(n^2) base58 decode, so a hostile did:key / publicKeyMultibase in a
|
|
44
|
+
fetched did:web or Data Integrity document (up to 5 MiB / 256 KiB) can no
|
|
45
|
+
longer burn CPU. No real multikey exceeds ~70 characters.
|
|
46
|
+
|
|
47
|
+
- fix(ob2): an `evidence` array (which OB 2.0 permits) is preserved when
|
|
48
|
+
parsing an Assertion instead of being silently dropped — `_iri_or_none`
|
|
49
|
+
returned None for a list. A single value still collapses to its IRI.
|
|
50
|
+
|
|
51
|
+
- fix(cli): `openbadges-signer -M/--mail-badge` now prints a clear notice
|
|
52
|
+
on the default OB3 path (it was a silent no-op there); the stale
|
|
53
|
+
`openbadges-keygenerator -t ED25519` hint on the LDP path is corrected to
|
|
54
|
+
the real `key_type = ED25519` config key; and the OB1 mail path reads its
|
|
55
|
+
`[smtp]` config inside the try, so a missing `[smtp]` section is reported
|
|
56
|
+
cleanly after signing instead of raising a traceback.
|
|
57
|
+
|
|
58
|
+
- fix(publish): `openbadges-publish -V 3` no longer aborts mid-loop when a
|
|
59
|
+
badge has an unreadable key or corrupt registry — it skips that badge
|
|
60
|
+
with a notice (like the did.json path already did), regenerates the
|
|
61
|
+
others, and exits non-zero with a summary. An urgent revocation is no
|
|
62
|
+
longer masked by an unrelated half-configured badge.
|
|
63
|
+
|
|
64
|
+
- refactor(keygen): `openbadges-keygenerator` uses `enable_debug_logging`
|
|
65
|
+
instead of the legacy `Logger` class (now removed from logs.py), so key
|
|
66
|
+
generation no longer requires the `[paths]`/`[logs]` config sections just
|
|
67
|
+
to emit console lines (they raised a raw KeyError/FileNotFoundError), and
|
|
68
|
+
a missing `[issuer] name` degrades gracefully.
|
|
69
|
+
|
|
70
|
+
|
|
7
71
|
* v3.5.0 - 2026-07-07
|
|
8
72
|
|
|
9
73
|
- fix(status): serialise status-registry writes with an exclusive
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: Production-ready Python library & CLI for Open Badges 3.0 (W3C Verifiable Credentials): issue, verify, and revoke JWT-VC credentials with Bitstring Status Lists and did:web — plus strict OB 2.0 and legacy OB 1.0.
|
|
5
5
|
Author-email: Luis González Fernández <luisgf@luisgf.es>, Jesús Cea Avión <jcea@jcea.es>
|
|
6
6
|
License: LGPLv3
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
`openbadgeslib` issues and verifies cryptographically-signed credentials, so we
|
|
4
|
+
take reports seriously and aim to respond quickly.
|
|
5
|
+
|
|
6
|
+
## Supported versions
|
|
7
|
+
|
|
8
|
+
Security fixes land in a new release on the latest minor line and are published
|
|
9
|
+
to [PyPI](https://pypi.org/project/openbadgeslib/). Always run the latest
|
|
10
|
+
release; older versions are not patched in place.
|
|
11
|
+
|
|
12
|
+
| Version | Supported |
|
|
13
|
+
| -------------- | ----------------------------- |
|
|
14
|
+
| Latest release | ✅ security fixes |
|
|
15
|
+
| Older releases | ❌ upgrade to the latest |
|
|
16
|
+
|
|
17
|
+
## Reporting a vulnerability
|
|
18
|
+
|
|
19
|
+
**Please do not open a public issue for a suspected vulnerability.** Report it
|
|
20
|
+
privately instead:
|
|
21
|
+
|
|
22
|
+
- **Preferred:** GitHub private vulnerability reporting — the **“Report a
|
|
23
|
+
vulnerability”** button under this repository’s
|
|
24
|
+
[Security tab](https://github.com/luisgf/openbadgeslib/security/advisories).
|
|
25
|
+
- **Email:** `luisgf@luisgf.es` (you may encrypt to the same address’s public
|
|
26
|
+
key if you have it).
|
|
27
|
+
|
|
28
|
+
Please include enough to reproduce: the affected version, a minimal example or
|
|
29
|
+
proof of concept, and the impact you observed. If it touches signature
|
|
30
|
+
verification, revocation, DID/key resolution, or the handling of untrusted
|
|
31
|
+
input (downloaded documents, baked images, status lists), say so — those are the
|
|
32
|
+
highest-priority areas.
|
|
33
|
+
|
|
34
|
+
## What to expect
|
|
35
|
+
|
|
36
|
+
- Acknowledgement of your report, normally within a few days.
|
|
37
|
+
- An assessment of severity and scope, and a fix or mitigation plan.
|
|
38
|
+
- Coordinated disclosure: we will agree a timeline with you and credit you in
|
|
39
|
+
the release notes and advisory unless you prefer to stay anonymous.
|
|
40
|
+
- A patched release on PyPI and a published GitHub Security Advisory once the
|
|
41
|
+
fix is available.
|
|
42
|
+
|
|
43
|
+
## Scope and threat model
|
|
44
|
+
|
|
45
|
+
What the library defends against, and what it delegates to the operator (key
|
|
46
|
+
custody, DNS/TLS for `did:web`, hosting of published documents), is documented
|
|
47
|
+
in the [Security Model](https://github.com/luisgf/openbadgeslib/wiki/Security-Model)
|
|
48
|
+
wiki page. Reports that fall within that model — a bypass of algorithm pinning,
|
|
49
|
+
signature or recipient-binding verification, the SSRF/decompression guards, the
|
|
50
|
+
context allowlist, or the status-list checks — are especially welcome.
|
|
51
|
+
|
|
52
|
+
Out of scope: issues that require the operator’s own private keys or config to
|
|
53
|
+
already be compromised, and hardening we already document as a deliberate,
|
|
54
|
+
human-owned decision (for example, passphrase-encrypted private keys at rest).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""JWS sign/verify backed by PyJWT algorithm implementations (RS256/384/512, ES256/384/512)."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Dict, Optional, Set, Union
|
|
3
|
+
from typing import Any, Dict, Optional, Set, Union, cast
|
|
4
4
|
|
|
5
5
|
from . import utils
|
|
6
6
|
from .exceptions import SignatureError, MissingKey, MissingSigner, MissingVerifier, RouteMissingError
|
|
@@ -66,7 +66,7 @@ def sign(header_dict: Dict[str, Any], payload_dict: Dict[str, Any], key: Any) ->
|
|
|
66
66
|
algo = _algo_for(alg_name)
|
|
67
67
|
try:
|
|
68
68
|
prepared = algo.prepare_key(key_to_pem(key))
|
|
69
|
-
return algo.sign(signing_input, prepared)
|
|
69
|
+
return cast(bytes, algo.sign(signing_input, prepared))
|
|
70
70
|
except (InvalidKeyError, ValueError) as exc:
|
|
71
71
|
raise SignatureError(str(exc)) from exc
|
|
72
72
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
from struct import pack
|
|
30
30
|
from zlib import crc32
|
|
31
31
|
|
|
32
|
-
from typing import List, Optional, Tuple, Union
|
|
32
|
+
from typing import List, Optional, Tuple, Union, cast
|
|
33
33
|
|
|
34
34
|
from defusedxml.minidom import parseString
|
|
35
35
|
from png import Reader, signature as _png_signature
|
|
@@ -98,7 +98,7 @@ def bake_svg(image_bytes: bytes, token: str, comment: Optional[str] = None, *,
|
|
|
98
98
|
svg_tag.appendChild(node)
|
|
99
99
|
if comment:
|
|
100
100
|
svg_tag.appendChild(svg_doc.createComment(comment))
|
|
101
|
-
return svg_doc.toxml().encode('utf-8')
|
|
101
|
+
return cast(bytes, svg_doc.toxml().encode('utf-8'))
|
|
102
102
|
finally:
|
|
103
103
|
svg_doc.unlink()
|
|
104
104
|
|
|
@@ -133,7 +133,7 @@ def extract_svg(image_bytes: bytes, *, element: str = SVG_ELEMENT,
|
|
|
133
133
|
return None
|
|
134
134
|
attrs = nodes[0].attributes
|
|
135
135
|
if 'verify' in attrs:
|
|
136
|
-
return attrs['verify'].nodeValue
|
|
136
|
+
return cast(Optional[str], attrs['verify'].nodeValue)
|
|
137
137
|
if not text_fallback:
|
|
138
138
|
return None
|
|
139
139
|
text = ''.join(
|
|
@@ -158,7 +158,7 @@ def _serialize_png(chunks: List[Tuple[Union[str, bytes], bytes]]) -> bytes:
|
|
|
158
158
|
checksum = crc32(tag)
|
|
159
159
|
checksum = crc32(data, checksum) & 0xFFFFFFFF
|
|
160
160
|
out += pack("!I", checksum)
|
|
161
|
-
return out
|
|
161
|
+
return cast(bytes, out)
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
def bake_png(image_bytes: bytes, token: str, text_comment: Optional[str] = None, *,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
License along with this library.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
from typing import Any, Optional, Tuple, Union
|
|
24
|
+
from typing import Any, Optional, Tuple, Union, cast
|
|
25
25
|
from .errors import PublicKeyReadError, UnknownKeyType
|
|
26
26
|
from ecdsa import SigningKey, VerifyingKey, NIST256p
|
|
27
27
|
from Crypto.PublicKey import RSA
|
|
@@ -91,10 +91,10 @@ class KeyRSA(KeyBase):
|
|
|
91
91
|
self.pub_key = RSA.import_key(key_pem)
|
|
92
92
|
|
|
93
93
|
def get_priv_key_pem(self) -> bytes:
|
|
94
|
-
return self.priv_key.export_key('PEM')
|
|
94
|
+
return cast(bytes, self.priv_key.export_key('PEM'))
|
|
95
95
|
|
|
96
96
|
def get_pub_key_pem(self) -> bytes:
|
|
97
|
-
return self.pub_key.export_key('PEM')
|
|
97
|
+
return cast(bytes, self.pub_key.export_key('PEM'))
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
class KeyECC(KeyBase):
|
|
@@ -126,10 +126,10 @@ class KeyECC(KeyBase):
|
|
|
126
126
|
self.pub_key = VerifyingKey.from_pem(key_pem)
|
|
127
127
|
|
|
128
128
|
def get_priv_key_pem(self) -> bytes:
|
|
129
|
-
return self.priv_key.to_pem()
|
|
129
|
+
return cast(bytes, self.priv_key.to_pem())
|
|
130
130
|
|
|
131
131
|
def get_pub_key_pem(self) -> bytes:
|
|
132
|
-
return self.pub_key.to_pem()
|
|
132
|
+
return cast(bytes, self.pub_key.to_pem())
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
def _pem_bytes(key_pem: Union[str, bytes]) -> bytes:
|
|
@@ -177,15 +177,15 @@ class KeyEd25519(KeyBase):
|
|
|
177
177
|
self.pub_key = _load_ed25519_public_key(key_pem)
|
|
178
178
|
|
|
179
179
|
def get_priv_key_pem(self) -> bytes:
|
|
180
|
-
return self.priv_key.private_bytes(
|
|
180
|
+
return cast(bytes, self.priv_key.private_bytes(
|
|
181
181
|
_crypto_serialization.Encoding.PEM,
|
|
182
182
|
_crypto_serialization.PrivateFormat.PKCS8,
|
|
183
|
-
_crypto_serialization.NoEncryption())
|
|
183
|
+
_crypto_serialization.NoEncryption()))
|
|
184
184
|
|
|
185
185
|
def get_pub_key_pem(self) -> bytes:
|
|
186
|
-
return self.pub_key.public_bytes(
|
|
186
|
+
return cast(bytes, self.pub_key.public_bytes(
|
|
187
187
|
_crypto_serialization.Encoding.PEM,
|
|
188
|
-
_crypto_serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
188
|
+
_crypto_serialization.PublicFormat.SubjectPublicKeyInfo))
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
def alg_for_key_type(key_type: 'KeyType') -> str:
|
|
@@ -209,7 +209,7 @@ def key_to_pem(key: Any) -> Union[str, bytes]:
|
|
|
209
209
|
if isinstance(key, RSA.RsaKey):
|
|
210
210
|
return key.export_key('PEM')
|
|
211
211
|
if isinstance(key, (SigningKey, VerifyingKey)):
|
|
212
|
-
return key.to_pem()
|
|
212
|
+
return cast(Union[str, bytes], key.to_pem())
|
|
213
213
|
if isinstance(key, Ed25519PrivateKey):
|
|
214
214
|
return key.private_bytes(
|
|
215
215
|
_crypto_serialization.Encoding.PEM,
|
|
@@ -224,7 +224,7 @@ def key_to_pem(key: Any) -> Union[str, bytes]:
|
|
|
224
224
|
raise UnknownKeyType('Unsupported key object type: %r' % type(key))
|
|
225
225
|
|
|
226
226
|
|
|
227
|
-
def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict:
|
|
227
|
+
def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict[str, Any]:
|
|
228
228
|
"""Serialise a public key PEM as a public JWK dict.
|
|
229
229
|
|
|
230
230
|
Counterpart of the OB3 signer's private-key-based JWK derivation, for
|
|
@@ -250,7 +250,7 @@ def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict:
|
|
|
250
250
|
except Exception as exc:
|
|
251
251
|
raise PublicKeyReadError(
|
|
252
252
|
'could not read public key PEM: %s' % exc) from exc
|
|
253
|
-
return json.loads(jwk_json)
|
|
253
|
+
return cast(dict[str, Any], json.loads(jwk_json))
|
|
254
254
|
|
|
255
255
|
|
|
256
256
|
def ec_curve_from_pem(pem_data: Union[str, bytes]) -> Optional[str]:
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
import logging
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def enable_debug_logging(debug: bool = False) -> None:
|
|
29
|
+
"""Route library log messages to the console for the CLI tools.
|
|
30
|
+
|
|
31
|
+
With ``debug=True`` DEBUG-level messages are shown; otherwise only INFO and
|
|
32
|
+
above. Idempotent — safe to call once per CLI invocation (it will not stack
|
|
33
|
+
duplicate console handlers).
|
|
34
|
+
"""
|
|
35
|
+
root = logging.getLogger()
|
|
36
|
+
if not any(getattr(h, '_obl_console', False) for h in root.handlers):
|
|
37
|
+
handler: Any = logging.StreamHandler()
|
|
38
|
+
handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s"))
|
|
39
|
+
handler._obl_console = True
|
|
40
|
+
root.addHandler(handler)
|
|
41
|
+
root.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import os
|
|
25
25
|
from enum import Enum
|
|
26
|
-
from typing import Any, Optional, Union
|
|
26
|
+
from typing import Any, Optional, Union, cast
|
|
27
27
|
|
|
28
28
|
from Crypto.PublicKey import RSA
|
|
29
29
|
from ecdsa import SigningKey, VerifyingKey
|
|
@@ -78,7 +78,7 @@ class Assertion():
|
|
|
78
78
|
return jws_utils.decode(self.body)
|
|
79
79
|
|
|
80
80
|
def get_assertion(self) -> bytes:
|
|
81
|
-
return self.header + b'.' + self.body + b'.' + self.signature
|
|
81
|
+
return cast(bytes, self.header + b'.' + self.body + b'.' + self.signature)
|
|
82
82
|
|
|
83
83
|
def encode_header(self, header: Any) -> None:
|
|
84
84
|
self.header = jws_utils.encode(header)
|
|
@@ -350,13 +350,13 @@ class BadgeSigned():
|
|
|
350
350
|
self.file_out = file_name
|
|
351
351
|
|
|
352
352
|
def get_identity(self) -> str:
|
|
353
|
-
return self.identity.decode('utf-8')
|
|
353
|
+
return cast(str, self.identity.decode('utf-8'))
|
|
354
354
|
|
|
355
355
|
def get_identity_hashed(self) -> str:
|
|
356
356
|
return (b'sha256$' + hash_email(self.identity, self.salt)).decode('utf-8')
|
|
357
357
|
|
|
358
358
|
def get_salt(self) -> str:
|
|
359
|
-
return self.salt.decode('utf-8')
|
|
359
|
+
return cast(str, self.salt.decode('utf-8'))
|
|
360
360
|
|
|
361
361
|
def get_assertion(self) -> Optional[str]:
|
|
362
362
|
if self.assertion:
|
|
@@ -380,7 +380,7 @@ class BadgeSigned():
|
|
|
380
380
|
def get_signkey_pem(self) -> Union[str, bytes]:
|
|
381
381
|
""" Return the public key pem used to sign the openbadge """
|
|
382
382
|
|
|
383
|
-
return self.source.pubkey_pem
|
|
383
|
+
return cast(Union[str, bytes], self.source.pubkey_pem)
|
|
384
384
|
|
|
385
385
|
|
|
386
386
|
def extract_svg_assertion(file_data: bytes) -> Assertion:
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
License along with this library.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
from typing import Any, Optional
|
|
24
|
+
from typing import Any, Optional, cast
|
|
25
25
|
|
|
26
26
|
from .badge import BadgeStatus
|
|
27
27
|
from ..util import hash_email, download_file, show_ecc_disclaimer
|
|
@@ -225,7 +225,7 @@ class Verifier():
|
|
|
225
225
|
email_salt = badge.salt if badge.salt else b''
|
|
226
226
|
email_hashed = b'sha256$' + hash_email(self.identity, email_salt)
|
|
227
227
|
|
|
228
|
-
return email_hashed == badge.identity
|
|
228
|
+
return cast(bool, email_hashed == badge.identity)
|
|
229
229
|
except Exception:
|
|
230
230
|
raise NotIdentityInAssertion('The assertion doesn\'t have an identify ')
|
|
231
231
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import uuid
|
|
33
33
|
from dataclasses import dataclass, field
|
|
34
34
|
from datetime import datetime, timezone
|
|
35
|
-
from typing import Any, List, Optional
|
|
35
|
+
from typing import Any, List, Optional, Union
|
|
36
36
|
|
|
37
37
|
from ..util import hash_email
|
|
38
38
|
|
|
@@ -95,13 +95,13 @@ def _validate_type(value: Any, expected: str, where: str) -> None:
|
|
|
95
95
|
raise ValueError("%s.type must include %r, got %r" % (where, expected, value))
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def _as_dict(value: Any, where: str) -> dict:
|
|
98
|
+
def _as_dict(value: Any, where: str) -> dict[str, Any]:
|
|
99
99
|
if not isinstance(value, dict):
|
|
100
100
|
raise ValueError("%s must be a JSON object" % where)
|
|
101
101
|
return value
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def _require(data: dict, key: str, where: str) -> str:
|
|
104
|
+
def _require(data: dict[str, Any], key: str, where: str) -> str:
|
|
105
105
|
"""Return ``data[key]`` as a non-empty string, else raise a clear error."""
|
|
106
106
|
value = data.get(key)
|
|
107
107
|
if value is None or value == "":
|
|
@@ -122,6 +122,25 @@ def _iri_or_none(value: Any) -> Optional[str]:
|
|
|
122
122
|
return None
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def _iri_or_iris(value: Any) -> Optional[Union[str, List[str]]]:
|
|
126
|
+
"""Like :func:`_iri_or_none`, but keep an array instead of dropping it.
|
|
127
|
+
|
|
128
|
+
OB 2.0 lets ``evidence`` be a bare IRI, an object with an ``id``, or an
|
|
129
|
+
array of either. A single value collapses to its IRI string (unchanged
|
|
130
|
+
behaviour); an array yields the list of IRIs it carries. None when nothing
|
|
131
|
+
usable is present — so a conformant multi-evidence assertion is no longer
|
|
132
|
+
silently discarded on parse.
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(value, list):
|
|
135
|
+
iris = []
|
|
136
|
+
for item in value:
|
|
137
|
+
iri = _iri_or_none(item)
|
|
138
|
+
if iri is not None:
|
|
139
|
+
iris.append(iri)
|
|
140
|
+
return iris or None
|
|
141
|
+
return _iri_or_none(value)
|
|
142
|
+
|
|
143
|
+
|
|
125
144
|
def hash_identity(email: str, salt: Optional[str]) -> str:
|
|
126
145
|
"""Return the ``sha256$<hex>`` IdentityHash for an email plus optional salt."""
|
|
127
146
|
return "sha256$" + hash_email(email, salt if salt is not None else "").decode("ascii")
|
|
@@ -143,8 +162,8 @@ class IdentityObject:
|
|
|
143
162
|
"""Build a hashed email IdentityObject from a plaintext email + salt."""
|
|
144
163
|
return cls(identity=hash_identity(email, salt), hashed=True, salt=salt, type="email")
|
|
145
164
|
|
|
146
|
-
def to_dict(self) -> dict:
|
|
147
|
-
d: dict = {"type": self.type, "hashed": self.hashed, "identity": self.identity}
|
|
165
|
+
def to_dict(self) -> dict[str, Any]:
|
|
166
|
+
d: dict[str, Any] = {"type": self.type, "hashed": self.hashed, "identity": self.identity}
|
|
148
167
|
if self.salt is not None:
|
|
149
168
|
d["salt"] = self.salt
|
|
150
169
|
return d
|
|
@@ -174,8 +193,8 @@ class Verification:
|
|
|
174
193
|
type: str # "SignedBadge" or "HostedBadge"
|
|
175
194
|
creator: Optional[str] = None # IRI of the issuer's CryptographicKey (signed)
|
|
176
195
|
|
|
177
|
-
def to_dict(self) -> dict:
|
|
178
|
-
d: dict = {"type": self.type}
|
|
196
|
+
def to_dict(self) -> dict[str, Any]:
|
|
197
|
+
d: dict[str, Any] = {"type": self.type}
|
|
179
198
|
if self.creator:
|
|
180
199
|
d["creator"] = self.creator
|
|
181
200
|
return d
|
|
@@ -209,7 +228,7 @@ class Assertion:
|
|
|
209
228
|
issued_on: Optional[datetime] = None # defaults to now (UTC)
|
|
210
229
|
expires: Optional[datetime] = None
|
|
211
230
|
image: Optional[str] = None
|
|
212
|
-
evidence: Optional[str] = None
|
|
231
|
+
evidence: Optional[Union[str, List[str]]] = None
|
|
213
232
|
narrative: Optional[str] = None
|
|
214
233
|
|
|
215
234
|
def __post_init__(self) -> None:
|
|
@@ -218,9 +237,9 @@ class Assertion:
|
|
|
218
237
|
if self.issued_on is None:
|
|
219
238
|
self.issued_on = datetime.now(timezone.utc)
|
|
220
239
|
|
|
221
|
-
def to_dict(self) -> dict:
|
|
240
|
+
def to_dict(self) -> dict[str, Any]:
|
|
222
241
|
assert self.issued_on is not None # set by __post_init__
|
|
223
|
-
d: dict = {
|
|
242
|
+
d: dict[str, Any] = {
|
|
224
243
|
"@context": OB2_CONTEXT,
|
|
225
244
|
"type": "Assertion",
|
|
226
245
|
"id": self.id,
|
|
@@ -265,7 +284,7 @@ class Assertion:
|
|
|
265
284
|
issued_on=issued_on,
|
|
266
285
|
expires=expires,
|
|
267
286
|
image=_iri_or_none(d.get("image")),
|
|
268
|
-
evidence=
|
|
287
|
+
evidence=_iri_or_iris(d.get("evidence")),
|
|
269
288
|
narrative=d.get("narrative") if isinstance(d.get("narrative"), str) else None,
|
|
270
289
|
)
|
|
271
290
|
|
|
@@ -278,7 +297,7 @@ class CryptographicKey:
|
|
|
278
297
|
owner: str # IRI of the owning issuer Profile (bidirectional)
|
|
279
298
|
public_key_pem: str
|
|
280
299
|
|
|
281
|
-
def to_dict(self) -> dict:
|
|
300
|
+
def to_dict(self) -> dict[str, Any]:
|
|
282
301
|
return {
|
|
283
302
|
"@context": OB2_CONTEXT,
|
|
284
303
|
"type": "CryptographicKey",
|
|
@@ -310,8 +329,8 @@ class Profile:
|
|
|
310
329
|
public_key: List[str] = field(default_factory=list) # CryptographicKey IRIs
|
|
311
330
|
revocation_list: Optional[str] = None
|
|
312
331
|
|
|
313
|
-
def to_dict(self) -> dict:
|
|
314
|
-
d: dict = {
|
|
332
|
+
def to_dict(self) -> dict[str, Any]:
|
|
333
|
+
d: dict[str, Any] = {
|
|
315
334
|
"@context": OB2_CONTEXT,
|
|
316
335
|
"type": "Issuer",
|
|
317
336
|
"id": self.id,
|
|
@@ -342,8 +361,8 @@ class BadgeClass:
|
|
|
342
361
|
issuer: str # issuer Profile IRI
|
|
343
362
|
tags: List[str] = field(default_factory=list)
|
|
344
363
|
|
|
345
|
-
def to_dict(self) -> dict:
|
|
346
|
-
d: dict = {
|
|
364
|
+
def to_dict(self) -> dict[str, Any]:
|
|
365
|
+
d: dict[str, Any] = {
|
|
347
366
|
"@context": OB2_CONTEXT,
|
|
348
367
|
"type": "BadgeClass",
|
|
349
368
|
"id": self.id,
|
|
@@ -366,7 +385,7 @@ class RevocationList:
|
|
|
366
385
|
issuer: str
|
|
367
386
|
revoked_assertions: List[Any] = field(default_factory=list)
|
|
368
387
|
|
|
369
|
-
def to_dict(self) -> dict:
|
|
388
|
+
def to_dict(self) -> dict[str, Any]:
|
|
370
389
|
return {
|
|
371
390
|
"@context": OB2_CONTEXT,
|
|
372
391
|
"type": "RevocationList",
|
|
@@ -215,7 +215,7 @@ class OB2Verifier:
|
|
|
215
215
|
"hosted trust comes from the HTTPS fetch, not the signature.",
|
|
216
216
|
assertion.id, exc)
|
|
217
217
|
|
|
218
|
-
def _check_hosted_scope(self, assertion_id: str, issuer: dict) -> None:
|
|
218
|
+
def _check_hosted_scope(self, assertion_id: str, issuer: dict[str, Any]) -> None:
|
|
219
219
|
"""Enforce the OB 2.0 hosted-verification scope for ``assertion_id``."""
|
|
220
220
|
verification = issuer.get("verification")
|
|
221
221
|
if isinstance(verification, dict):
|
|
@@ -287,7 +287,7 @@ class OB2Verifier:
|
|
|
287
287
|
|
|
288
288
|
# ── network / decode helpers ─────────────────────────────────────────────────
|
|
289
289
|
|
|
290
|
-
def _fetch_issuer(self, assertion: Assertion) -> dict:
|
|
290
|
+
def _fetch_issuer(self, assertion: Assertion) -> dict[str, Any]:
|
|
291
291
|
"""Resolve the issuer Profile via the assertion's BadgeClass ``badge`` URL."""
|
|
292
292
|
badge = self._fetch_json(assertion.badge, "BadgeClass")
|
|
293
293
|
issuer_url = badge.get("issuer")
|
|
@@ -299,7 +299,7 @@ class OB2Verifier:
|
|
|
299
299
|
"BadgeClass at %s has no valid 'issuer'" % assertion.badge)
|
|
300
300
|
return self._fetch_json(issuer_url, "issuer Profile")
|
|
301
301
|
|
|
302
|
-
def _fetch_json(self, url: str, where: str) -> dict:
|
|
302
|
+
def _fetch_json(self, url: str, where: str) -> dict[str, Any]:
|
|
303
303
|
try:
|
|
304
304
|
raw = download_file(url)
|
|
305
305
|
except Exception as exc:
|
|
@@ -79,7 +79,11 @@ _URL_TO_RESOURCE: Dict[str, str] = {
|
|
|
79
79
|
'ob-v3p0-context-3.0.2.json',
|
|
80
80
|
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json':
|
|
81
81
|
'ob-v3p0-context-3.0.3.json',
|
|
82
|
-
# The unversioned OB3 context URL
|
|
82
|
+
# The unversioned OB3 context URL is an internal alias to our latest
|
|
83
|
+
# bundled revision (3.0.3). We deliberately pin it here rather than mirror
|
|
84
|
+
# 1EdTech's moving unversioned endpoint (which currently serves an older
|
|
85
|
+
# revision matching none we ship); the context-drift check skips it for
|
|
86
|
+
# that reason.
|
|
83
87
|
'https://purl.imsglobal.org/spec/ob/v3p0/context.json':
|
|
84
88
|
'ob-v3p0-context-3.0.3.json',
|
|
85
89
|
'https://w3id.org/security/data-integrity/v2':
|
|
@@ -89,7 +93,7 @@ _URL_TO_RESOURCE: Dict[str, str] = {
|
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
@functools.lru_cache(maxsize=None)
|
|
92
|
-
def load_context(url: str) -> dict:
|
|
96
|
+
def load_context(url: str) -> dict[str, Any]:
|
|
93
97
|
"""Return the bundled JSON-LD context document for an allowlisted URL.
|
|
94
98
|
|
|
95
99
|
Raises UnknownContextError for any URL outside the allowlist — the match
|
|
@@ -109,8 +113,8 @@ def load_context(url: str) -> dict:
|
|
|
109
113
|
|
|
110
114
|
|
|
111
115
|
def document_loader(
|
|
112
|
-
extra_contexts: Optional[Mapping[str, dict]] = None,
|
|
113
|
-
) -> Callable[[str, Any], dict]:
|
|
116
|
+
extra_contexts: Optional[Mapping[str, dict[str, Any]]] = None,
|
|
117
|
+
) -> Callable[[str, Any], dict[str, Any]]:
|
|
114
118
|
"""Build a static documentLoader in the shape pyld expects.
|
|
115
119
|
|
|
116
120
|
*extra_contexts* extends the allowlist for one loader instance (e.g. a
|
|
@@ -118,9 +122,9 @@ def document_loader(
|
|
|
118
122
|
global allowlist. Every other URL raises UnknownContextError, which the
|
|
119
123
|
Data Integrity verifier surfaces as a clean verification failure.
|
|
120
124
|
"""
|
|
121
|
-
extras: Dict[str, dict] = dict(extra_contexts) if extra_contexts else {}
|
|
125
|
+
extras: Dict[str, dict[str, Any]] = dict(extra_contexts) if extra_contexts else {}
|
|
122
126
|
|
|
123
|
-
def _loader(url: str, options: Any = None) -> dict:
|
|
127
|
+
def _loader(url: str, options: Any = None) -> dict[str, Any]:
|
|
124
128
|
document = extras[url] if url in extras else load_context(url)
|
|
125
129
|
return {'contextUrl': None, 'documentUrl': url, 'document': document}
|
|
126
130
|
|