openbadgeslib 3.5.1__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.1 → openbadgeslib-3.6.0}/Changelog.txt +23 -0
- {openbadgeslib-3.5.1/openbadgeslib.egg-info → openbadgeslib-3.6.0}/PKG-INFO +1 -1
- openbadgeslib-3.6.0/SECURITY.md +54 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/_jws/__init__.py +2 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/baking.py +4 -4
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/keys.py +12 -12
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/badge.py +5 -5
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/verifier.py +2 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/models.py +14 -14
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/verifier.py +3 -3
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/__init__.py +10 -6
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/credential.py +44 -16
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/did.py +7 -7
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/eudi.py +5 -5
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/ldp.py +13 -13
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/signer.py +4 -4
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status.py +2 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status_list.py +3 -3
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status_registry.py +6 -6
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/verifier.py +3 -3
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_publish.py +2 -1
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/util.py +1 -1
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0/openbadgeslib.egg-info}/PKG-INFO +1 -1
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/SOURCES.txt +1 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/pyproject.toml +8 -6
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_credential.py +8 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_ldp_sign.py +3 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_verifier.py +36 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/LICENSE.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/MANIFEST.in +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/docs/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/__init__.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/badge.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/config.ini.example +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/confparser.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/mail.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/__init__.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/__init__.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_keygenerator.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_verifier.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/verifier.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/requires.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/setup.cfg +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/config1.ini +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conftest.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/runtests.sh +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_cli_json.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_cli_smoke.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_confparser.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_docs.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_jws.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_key_operation.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob1_badge_io.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob1_verifier.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_verifier.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_did.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_publish_cli.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_status.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_status_registry.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_util.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,29 @@ 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
|
+
|
|
7
30
|
* v3.5.1 - 2026-07-07
|
|
8
31
|
|
|
9
32
|
- fix(ob3): a naive datetime passed as the issuance/expiration date is now
|
|
@@ -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]:
|
|
@@ -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
|
|
|
@@ -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 == "":
|
|
@@ -162,8 +162,8 @@ class IdentityObject:
|
|
|
162
162
|
"""Build a hashed email IdentityObject from a plaintext email + salt."""
|
|
163
163
|
return cls(identity=hash_identity(email, salt), hashed=True, salt=salt, type="email")
|
|
164
164
|
|
|
165
|
-
def to_dict(self) -> dict:
|
|
166
|
-
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}
|
|
167
167
|
if self.salt is not None:
|
|
168
168
|
d["salt"] = self.salt
|
|
169
169
|
return d
|
|
@@ -193,8 +193,8 @@ class Verification:
|
|
|
193
193
|
type: str # "SignedBadge" or "HostedBadge"
|
|
194
194
|
creator: Optional[str] = None # IRI of the issuer's CryptographicKey (signed)
|
|
195
195
|
|
|
196
|
-
def to_dict(self) -> dict:
|
|
197
|
-
d: dict = {"type": self.type}
|
|
196
|
+
def to_dict(self) -> dict[str, Any]:
|
|
197
|
+
d: dict[str, Any] = {"type": self.type}
|
|
198
198
|
if self.creator:
|
|
199
199
|
d["creator"] = self.creator
|
|
200
200
|
return d
|
|
@@ -237,9 +237,9 @@ class Assertion:
|
|
|
237
237
|
if self.issued_on is None:
|
|
238
238
|
self.issued_on = datetime.now(timezone.utc)
|
|
239
239
|
|
|
240
|
-
def to_dict(self) -> dict:
|
|
240
|
+
def to_dict(self) -> dict[str, Any]:
|
|
241
241
|
assert self.issued_on is not None # set by __post_init__
|
|
242
|
-
d: dict = {
|
|
242
|
+
d: dict[str, Any] = {
|
|
243
243
|
"@context": OB2_CONTEXT,
|
|
244
244
|
"type": "Assertion",
|
|
245
245
|
"id": self.id,
|
|
@@ -297,7 +297,7 @@ class CryptographicKey:
|
|
|
297
297
|
owner: str # IRI of the owning issuer Profile (bidirectional)
|
|
298
298
|
public_key_pem: str
|
|
299
299
|
|
|
300
|
-
def to_dict(self) -> dict:
|
|
300
|
+
def to_dict(self) -> dict[str, Any]:
|
|
301
301
|
return {
|
|
302
302
|
"@context": OB2_CONTEXT,
|
|
303
303
|
"type": "CryptographicKey",
|
|
@@ -329,8 +329,8 @@ class Profile:
|
|
|
329
329
|
public_key: List[str] = field(default_factory=list) # CryptographicKey IRIs
|
|
330
330
|
revocation_list: Optional[str] = None
|
|
331
331
|
|
|
332
|
-
def to_dict(self) -> dict:
|
|
333
|
-
d: dict = {
|
|
332
|
+
def to_dict(self) -> dict[str, Any]:
|
|
333
|
+
d: dict[str, Any] = {
|
|
334
334
|
"@context": OB2_CONTEXT,
|
|
335
335
|
"type": "Issuer",
|
|
336
336
|
"id": self.id,
|
|
@@ -361,8 +361,8 @@ class BadgeClass:
|
|
|
361
361
|
issuer: str # issuer Profile IRI
|
|
362
362
|
tags: List[str] = field(default_factory=list)
|
|
363
363
|
|
|
364
|
-
def to_dict(self) -> dict:
|
|
365
|
-
d: dict = {
|
|
364
|
+
def to_dict(self) -> dict[str, Any]:
|
|
365
|
+
d: dict[str, Any] = {
|
|
366
366
|
"@context": OB2_CONTEXT,
|
|
367
367
|
"type": "BadgeClass",
|
|
368
368
|
"id": self.id,
|
|
@@ -385,7 +385,7 @@ class RevocationList:
|
|
|
385
385
|
issuer: str
|
|
386
386
|
revoked_assertions: List[Any] = field(default_factory=list)
|
|
387
387
|
|
|
388
|
-
def to_dict(self) -> dict:
|
|
388
|
+
def to_dict(self) -> dict[str, Any]:
|
|
389
389
|
return {
|
|
390
390
|
"@context": OB2_CONTEXT,
|
|
391
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
|
|
|
@@ -40,6 +40,16 @@ _OB_CONTEXT_RE = re.compile(
|
|
|
40
40
|
|
|
41
41
|
_SUPPORTED_ALGORITHMS = {'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'}
|
|
42
42
|
|
|
43
|
+
# 1EdTech's published JSON Schema for an AchievementCredential. Issued badges
|
|
44
|
+
# carry a credentialSchema pointing at it (§8, 1EdTechJsonSchemaValidator2019),
|
|
45
|
+
# so validators and wallets can self-check them; the offline conformance gate
|
|
46
|
+
# proves our credentials satisfy this exact schema.
|
|
47
|
+
_OB3_CREDENTIAL_SCHEMA = {
|
|
48
|
+
"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/"
|
|
49
|
+
"ob_v3p0_achievementcredential_schema.json",
|
|
50
|
+
"type": "1EdTechJsonSchemaValidator2019",
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
|
|
44
54
|
def _validate_context(ctx: Any) -> None:
|
|
45
55
|
"""Validate an OpenBadgeCredential ``@context`` per the schema: an array
|
|
@@ -77,8 +87,8 @@ class Issuer:
|
|
|
77
87
|
email: Optional[str] = None
|
|
78
88
|
image_url: Optional[str] = None
|
|
79
89
|
|
|
80
|
-
def to_dict(self) -> dict:
|
|
81
|
-
d: dict = {"id": self.id, "type": ["Profile"], "name": self.name}
|
|
90
|
+
def to_dict(self) -> dict[str, Any]:
|
|
91
|
+
d: dict[str, Any] = {"id": self.id, "type": ["Profile"], "name": self.name}
|
|
82
92
|
if self.url:
|
|
83
93
|
d["url"] = self.url
|
|
84
94
|
if self.email:
|
|
@@ -99,8 +109,8 @@ class Achievement:
|
|
|
99
109
|
image_url: Optional[str] = None
|
|
100
110
|
tags: List[str] = field(default_factory=list)
|
|
101
111
|
|
|
102
|
-
def to_dict(self) -> dict:
|
|
103
|
-
d: dict = {
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
d: dict[str, Any] = {
|
|
104
114
|
"id": self.id,
|
|
105
115
|
"type": ["Achievement"],
|
|
106
116
|
"name": self.name,
|
|
@@ -131,7 +141,19 @@ class OpenBadgeCredential:
|
|
|
131
141
|
# Raw credentialStatus entries (Bitstring Status List / StatusList2021),
|
|
132
142
|
# normalised to a list of objects. Consumed by ob3.status to check
|
|
133
143
|
# revocation; empty when the credential carries no status.
|
|
134
|
-
credential_status: List[dict] = field(default_factory=list)
|
|
144
|
+
credential_status: List[dict[str, Any]] = field(default_factory=list)
|
|
145
|
+
# A read-only view of the already-validated document this credential was
|
|
146
|
+
# parsed from (set by the verifiers via _from_vc); None when built in-memory
|
|
147
|
+
# to issue. Lets a caller read spec fields the model does not map —
|
|
148
|
+
# alignment, results, multiple evidence, endorsements, … — without
|
|
149
|
+
# re-parsing the token. Do NOT mutate it: on the from_*() classmethods it
|
|
150
|
+
# may alias the caller-supplied dict. Its shape is the document as parsed,
|
|
151
|
+
# so it reflects parse time (not later edits to the model fields) and is
|
|
152
|
+
# path-dependent — the JWT-VC payload (with the registered iss/sub/jti/nbf/
|
|
153
|
+
# exp claims alongside) or the Data Integrity document (with its proof).
|
|
154
|
+
# Excluded from equality/repr so it never affects comparisons.
|
|
155
|
+
raw: Optional[dict[str, Any]] = field(default=None, compare=False,
|
|
156
|
+
repr=False)
|
|
135
157
|
|
|
136
158
|
def __post_init__(self) -> None:
|
|
137
159
|
if self.id is None:
|
|
@@ -151,11 +173,11 @@ class OpenBadgeCredential:
|
|
|
151
173
|
|
|
152
174
|
# ── serialisation ──────────────────────────────────────────────────────────
|
|
153
175
|
|
|
154
|
-
def to_vc(self) -> dict:
|
|
176
|
+
def to_vc(self) -> dict[str, Any]:
|
|
155
177
|
"""Return the Verifiable Credential JSON object (no JWT wrapper)."""
|
|
156
178
|
# __post_init__ guarantees issuance_date is set.
|
|
157
179
|
assert self.issuance_date is not None
|
|
158
|
-
vc: dict = {
|
|
180
|
+
vc: dict[str, Any] = {
|
|
159
181
|
"@context": OB3_CONTEXT,
|
|
160
182
|
"id": self.id,
|
|
161
183
|
"type": ["VerifiableCredential", "OpenBadgeCredential"],
|
|
@@ -166,6 +188,7 @@ class OpenBadgeCredential:
|
|
|
166
188
|
"type": ["AchievementSubject"],
|
|
167
189
|
"achievement": self.achievement.to_dict(),
|
|
168
190
|
},
|
|
191
|
+
"credentialSchema": [dict(_OB3_CREDENTIAL_SCHEMA)],
|
|
169
192
|
}
|
|
170
193
|
# credentialSubject.id is optional; emit it only when present.
|
|
171
194
|
if self.recipient_id is not None:
|
|
@@ -180,7 +203,7 @@ class OpenBadgeCredential:
|
|
|
180
203
|
else self.credential_status)
|
|
181
204
|
return vc
|
|
182
205
|
|
|
183
|
-
def to_jwt_payload(self) -> dict:
|
|
206
|
+
def to_jwt_payload(self) -> dict[str, Any]:
|
|
184
207
|
"""Return the JWT payload for a JWT-VC signed credential.
|
|
185
208
|
|
|
186
209
|
OB 3.0 §8.2.4.1 (native VC-JWT): the JWT payload **is** the
|
|
@@ -192,7 +215,7 @@ class OpenBadgeCredential:
|
|
|
192
215
|
"""
|
|
193
216
|
# __post_init__ guarantees issuance_date is set.
|
|
194
217
|
assert self.issuance_date is not None
|
|
195
|
-
payload: dict = dict(self.to_vc()) # credential at the payload top level
|
|
218
|
+
payload: dict[str, Any] = dict(self.to_vc()) # credential at the payload top level
|
|
196
219
|
payload["iss"] = self.issuer.id
|
|
197
220
|
if self.recipient_id is not None:
|
|
198
221
|
payload["sub"] = self.recipient_id
|
|
@@ -205,7 +228,7 @@ class OpenBadgeCredential:
|
|
|
205
228
|
# ── deserialisation ────────────────────────────────────────────────────────
|
|
206
229
|
|
|
207
230
|
@classmethod
|
|
208
|
-
def from_jwt_payload(cls, payload: dict) -> "OpenBadgeCredential":
|
|
231
|
+
def from_jwt_payload(cls, payload: dict[str, Any]) -> "OpenBadgeCredential":
|
|
209
232
|
"""Reconstruct an OpenBadgeCredential from a decoded JWT payload.
|
|
210
233
|
|
|
211
234
|
OB 3.0 native VC-JWT: the payload IS the credential (its members at the
|
|
@@ -216,7 +239,7 @@ class OpenBadgeCredential:
|
|
|
216
239
|
return cls._from_vc(payload)
|
|
217
240
|
|
|
218
241
|
@classmethod
|
|
219
|
-
def from_vc_document(cls, document: dict) -> "OpenBadgeCredential":
|
|
242
|
+
def from_vc_document(cls, document: dict[str, Any]) -> "OpenBadgeCredential":
|
|
220
243
|
"""Reconstruct an OpenBadgeCredential from a JSON-LD VC document.
|
|
221
244
|
|
|
222
245
|
The shape a credential secured with an embedded Data Integrity proof
|
|
@@ -227,7 +250,7 @@ class OpenBadgeCredential:
|
|
|
227
250
|
return cls._from_vc(document)
|
|
228
251
|
|
|
229
252
|
@classmethod
|
|
230
|
-
def _from_vc(cls, payload: dict) -> "OpenBadgeCredential":
|
|
253
|
+
def _from_vc(cls, payload: dict[str, Any]) -> "OpenBadgeCredential":
|
|
231
254
|
"""Shared constructor from an untrusted credential body.
|
|
232
255
|
|
|
233
256
|
Every required object/field is checked and a clear ``ValueError`` is
|
|
@@ -311,7 +334,7 @@ class OpenBadgeCredential:
|
|
|
311
334
|
"vc.credentialSubject must have an 'id' or an 'identifier'")
|
|
312
335
|
recipient_id = None
|
|
313
336
|
|
|
314
|
-
|
|
337
|
+
credential = cls(
|
|
315
338
|
id=_require(vc, "id", "vc"),
|
|
316
339
|
issuer=issuer,
|
|
317
340
|
recipient_id=recipient_id,
|
|
@@ -322,21 +345,26 @@ class OpenBadgeCredential:
|
|
|
322
345
|
evidence_url=evidence_url,
|
|
323
346
|
credential_status=credential_status,
|
|
324
347
|
)
|
|
348
|
+
# Keep the validated document so the caller can read fields the model
|
|
349
|
+
# does not map (see the ``raw`` field). This is the JWT-VC payload or
|
|
350
|
+
# the Data Integrity document, exactly as verified.
|
|
351
|
+
credential.raw = vc
|
|
352
|
+
return credential
|
|
325
353
|
|
|
326
354
|
|
|
327
|
-
def _as_dict(value: Any, where: str) -> dict:
|
|
355
|
+
def _as_dict(value: Any, where: str) -> dict[str, Any]:
|
|
328
356
|
"""Return value if it is a dict, else raise a clear ValueError."""
|
|
329
357
|
if not isinstance(value, dict):
|
|
330
358
|
raise ValueError("%s must be a JSON object" % where)
|
|
331
359
|
return value
|
|
332
360
|
|
|
333
361
|
|
|
334
|
-
def _as_dict_or_empty(value: Any) -> dict:
|
|
362
|
+
def _as_dict_or_empty(value: Any) -> dict[str, Any]:
|
|
335
363
|
"""Return value if it is a dict, else an empty dict (optional sub-objects)."""
|
|
336
364
|
return value if isinstance(value, dict) else {}
|
|
337
365
|
|
|
338
366
|
|
|
339
|
-
def _require(data: dict, key: str, where: str) -> str:
|
|
367
|
+
def _require(data: dict[str, Any], key: str, where: str) -> str:
|
|
340
368
|
"""Return data[key] as a string, raising a clear ValueError if missing,
|
|
341
369
|
empty, or not a string. All fields validated here (id/name) are identifiers
|
|
342
370
|
consumed as strings downstream (e.g. recipient binding calls .lower()), so a
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import json
|
|
38
38
|
|
|
39
|
-
from typing import Any, Sequence, Tuple
|
|
39
|
+
from typing import Any, Sequence, Tuple, cast
|
|
40
40
|
|
|
41
41
|
from cryptography.hazmat.primitives import serialization as _serialization
|
|
42
42
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
@@ -189,7 +189,7 @@ def _resolve_did_web(did: str, fetch: Any) -> bytes:
|
|
|
189
189
|
return _pem_from_did_document(_fetch_did_web_document(did, fetch))
|
|
190
190
|
|
|
191
191
|
|
|
192
|
-
def _fetch_did_web_document(did: str, fetch: Any) -> dict:
|
|
192
|
+
def _fetch_did_web_document(did: str, fetch: Any) -> dict[str, Any]:
|
|
193
193
|
"""Fetch and parse the DID document a did:web identifier resolves to."""
|
|
194
194
|
from urllib.parse import unquote
|
|
195
195
|
ident = did[len('did:web:'):]
|
|
@@ -290,8 +290,8 @@ def did_web_from_url(url: str) -> str:
|
|
|
290
290
|
|
|
291
291
|
|
|
292
292
|
def build_did_document(did: str,
|
|
293
|
-
verification_methods: Sequence[Tuple[str, dict]]
|
|
294
|
-
) -> dict:
|
|
293
|
+
verification_methods: Sequence[Tuple[str, dict[str, Any]]]
|
|
294
|
+
) -> dict[str, Any]:
|
|
295
295
|
"""Build a DID document publishing *verification_methods*, given as
|
|
296
296
|
``(fragment, public JWK)`` pairs (see keys.public_jwk_from_pem).
|
|
297
297
|
|
|
@@ -344,7 +344,7 @@ def _pem_from_vm(vm: Any) -> bytes:
|
|
|
344
344
|
"(publicKeyJwk or z-base58 publicKeyMultibase)")
|
|
345
345
|
|
|
346
346
|
|
|
347
|
-
def _jwk_to_pem(jwk: dict) -> bytes:
|
|
347
|
+
def _jwk_to_pem(jwk: dict[str, Any]) -> bytes:
|
|
348
348
|
kty = jwk.get('kty')
|
|
349
349
|
jwk_json = json.dumps(jwk)
|
|
350
350
|
key: Any
|
|
@@ -368,6 +368,6 @@ def _jwk_to_pem(jwk: dict) -> bytes:
|
|
|
368
368
|
|
|
369
369
|
|
|
370
370
|
def _public_key_to_pem(key: Any) -> bytes:
|
|
371
|
-
return key.public_bytes(
|
|
371
|
+
return cast(bytes, key.public_bytes(
|
|
372
372
|
_serialization.Encoding.PEM,
|
|
373
|
-
_serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
373
|
+
_serialization.PublicFormat.SubjectPublicKeyInfo))
|