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.
Files changed (122) hide show
  1. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/Changelog.txt +23 -0
  2. {openbadgeslib-3.5.1/openbadgeslib.egg-info → openbadgeslib-3.6.0}/PKG-INFO +1 -1
  3. openbadgeslib-3.6.0/SECURITY.md +54 -0
  4. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/_jws/__init__.py +2 -2
  5. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/baking.py +4 -4
  6. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/keys.py +12 -12
  7. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/badge.py +5 -5
  8. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/verifier.py +2 -2
  9. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/models.py +14 -14
  10. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/verifier.py +3 -3
  11. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/__init__.py +10 -6
  12. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/credential.py +44 -16
  13. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/did.py +7 -7
  14. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/eudi.py +5 -5
  15. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/ldp.py +13 -13
  16. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/signer.py +4 -4
  17. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status.py +2 -2
  18. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status_list.py +3 -3
  19. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/status_registry.py +6 -6
  20. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/verifier.py +3 -3
  21. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_publish.py +2 -1
  22. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/util.py +1 -1
  23. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0/openbadgeslib.egg-info}/PKG-INFO +1 -1
  24. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/SOURCES.txt +1 -0
  25. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/pyproject.toml +8 -6
  26. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_credential.py +8 -0
  27. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_ldp_sign.py +3 -0
  28. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_verifier.py +36 -0
  29. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/LICENSE.txt +0 -0
  30. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/MANIFEST.in +0 -0
  31. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/README.md +0 -0
  32. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/docs/README.md +0 -0
  33. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/__init__.py +0 -0
  34. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/_jws/exceptions.py +0 -0
  35. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/_jws/utils.py +0 -0
  36. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/badge.py +0 -0
  37. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/config.ini.example +0 -0
  38. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/confparser.py +0 -0
  39. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/errors.py +0 -0
  40. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/logs.py +0 -0
  41. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/mail.py +0 -0
  42. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/__init__.py +0 -0
  43. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob1/signer.py +0 -0
  44. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/__init__.py +0 -0
  45. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob2/signer.py +0 -0
  46. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/__init__.py +0 -0
  47. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
  48. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
  49. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
  50. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
  51. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
  52. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
  53. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_init.py +0 -0
  54. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_keygenerator.py +0 -0
  55. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_signer.py +0 -0
  56. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/openbadges_verifier.py +0 -0
  57. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/py.typed +0 -0
  58. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/signer.py +0 -0
  59. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib/verifier.py +0 -0
  60. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  61. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
  62. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/requires.txt +0 -0
  63. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/openbadgeslib.egg-info/top_level.txt +0 -0
  64. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/setup.cfg +0 -0
  65. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/config1.ini +0 -0
  66. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/Dockerfile.ob3 +0 -0
  67. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/README.md +0 -0
  68. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/conftest.py +0 -0
  69. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/docker-compose.yml +0 -0
  70. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conformance/test_official_validators.py +0 -0
  71. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/conftest.py +0 -0
  72. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/README.md +0 -0
  73. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
  74. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
  75. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
  76. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
  77. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
  78. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
  79. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
  80. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
  81. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
  82. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/images/sample1.png +0 -0
  83. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/images/sample1.svg +0 -0
  84. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/images/userimage01.svg +0 -0
  85. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/ldp_helpers.py +0 -0
  86. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/logo Python Espan/314/203a.svg" +0 -0
  87. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/logo Python Espa/303/261a.svg" +0 -0
  88. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/runtests.sh +0 -0
  89. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_cli_json.py +0 -0
  90. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_cli_smoke.py +0 -0
  91. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_confparser.py +0 -0
  92. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_docs.py +0 -0
  93. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_eddsa.py +0 -0
  94. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_jws.py +0 -0
  95. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_key_operation.py +0 -0
  96. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob1_badge_io.py +0 -0
  97. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob1_signer.py +0 -0
  98. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob1_verifier.py +0 -0
  99. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_cli.py +0 -0
  100. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_models.py +0 -0
  101. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_signer.py +0 -0
  102. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob2_verifier.py +0 -0
  103. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_conformance_schema.py +0 -0
  104. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_contexts.py +0 -0
  105. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_did.py +0 -0
  106. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_did_doc.py +0 -0
  107. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
  108. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_ldp.py +0 -0
  109. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_ldp_cli.py +0 -0
  110. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_publish_cli.py +0 -0
  111. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_signer.py +0 -0
  112. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
  113. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_status.py +0 -0
  114. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_status_list.py +0 -0
  115. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_ob3_status_registry.py +0 -0
  116. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_sign_ecc.pem +0 -0
  117. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_sign_rsa.pem +0 -0
  118. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_util.py +0 -0
  119. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_verify_ecc.pem +0 -0
  120. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/test_verify_rsa.pem +0 -0
  121. {openbadgeslib-3.5.1 → openbadgeslib-3.6.0}/tests/withoutxmlheader.svg +0 -0
  122. {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.5.1
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 serves the latest published revision.
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
- return cls(
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))