openbadgeslib 4.4.4__tar.gz → 4.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 (175) hide show
  1. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/CONTRIBUTING.md +15 -8
  2. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/Changelog.txt +89 -0
  3. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/GOVERNANCE.md +1 -1
  4. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/PKG-INFO +10 -6
  5. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/README.md +8 -5
  6. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/__init__.py +13 -0
  7. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_jws/__init__.py +4 -0
  8. openbadgeslib-4.6.0/openbadgeslib/_jws/utils.py +57 -0
  9. openbadgeslib-4.6.0/openbadgeslib/_python_support.py +54 -0
  10. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/confparser.py +8 -5
  11. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/mail.py +9 -2
  12. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/badge.py +2 -1
  13. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/verifier.py +82 -13
  14. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/__init__.py +13 -1
  15. openbadgeslib-4.6.0/openbadgeslib/ob3/clr.py +176 -0
  16. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/__init__.py +22 -4
  17. openbadgeslib-4.6.0/openbadgeslib/ob3/contexts/clr_context_2.0.1.json +62 -0
  18. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/eudi.py +2 -2
  19. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/status.py +64 -14
  20. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/status_list.py +20 -7
  21. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/nonce.py +3 -1
  22. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/sqlite_store.py +16 -2
  23. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_signer.py +9 -2
  24. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/util.py +10 -2
  25. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/PKG-INFO +10 -6
  26. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/SOURCES.txt +7 -0
  27. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/pyproject.toml +1 -0
  28. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/Dockerfile.ob3 +11 -1
  29. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/README.md +6 -1
  30. openbadgeslib-4.6.0/tests/conformance/relax-danubetech-checksums.awk +24 -0
  31. openbadgeslib-4.6.0/tests/test_b64url.py +179 -0
  32. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_cli_smoke.py +29 -2
  33. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_confparser.py +26 -0
  34. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_mail.py +19 -0
  35. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_deprecation.py +12 -5
  36. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_error_branches.py +2 -1
  37. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_verifier.py +56 -1
  38. openbadgeslib-4.6.0/tests/test_ob3_clr.py +144 -0
  39. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_status.py +37 -0
  40. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_status_list.py +18 -0
  41. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_store.py +40 -2
  42. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_public_api.py +6 -1
  43. openbadgeslib-4.6.0/tests/test_python_support.py +127 -0
  44. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_signer_batch.py +18 -0
  45. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_util.py +14 -0
  46. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verifier_cli_coverage.py +9 -2
  47. openbadgeslib-4.4.4/openbadgeslib/_jws/utils.py +0 -32
  48. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/LICENSE.txt +0 -0
  49. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/MANIFEST.in +0 -0
  50. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/SECURITY.md +0 -0
  51. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/docs/README.md +0 -0
  52. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_jsonmodel.py +0 -0
  53. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_jws/exceptions.py +0 -0
  54. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/_ob1_api.py +0 -0
  55. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/badge_model.py +0 -0
  56. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/baking.py +0 -0
  57. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/cli_common.py +0 -0
  58. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/config.ini.example +0 -0
  59. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/errors.py +0 -0
  60. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/issue.py +0 -0
  61. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/keys.py +0 -0
  62. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/logs.py +0 -0
  63. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/__init__.py +0 -0
  64. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/signer.py +0 -0
  65. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob1/verifier.py +0 -0
  66. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/__init__.py +0 -0
  67. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/models.py +0 -0
  68. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob2/signer.py +0 -0
  69. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
  70. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
  71. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
  72. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
  73. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +0 -0
  74. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
  75. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
  76. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/credential.py +0 -0
  77. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/did.py +0 -0
  78. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/ldp.py +0 -0
  79. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/publish.py +0 -0
  80. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/signer.py +0 -0
  81. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/status_registry.py +0 -0
  82. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/ob3/verifier.py +0 -0
  83. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/__init__.py +0 -0
  84. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/codes.py +0 -0
  85. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/errors.py +0 -0
  86. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/formats.py +0 -0
  87. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/handler.py +0 -0
  88. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/memory_store.py +0 -0
  89. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/metadata.py +0 -0
  90. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/offer.py +0 -0
  91. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/reconcile.py +0 -0
  92. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/oid4vci/store.py +0 -0
  93. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_cli.py +0 -0
  94. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_init.py +0 -0
  95. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_keygenerator.py +0 -0
  96. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_publish.py +0 -0
  97. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_status.py +0 -0
  98. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/openbadges_verifier.py +0 -0
  99. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/py.typed +0 -0
  100. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib/verify.py +0 -0
  101. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  102. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
  103. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/requires.txt +0 -0
  104. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/openbadgeslib.egg-info/top_level.txt +0 -0
  105. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/setup.cfg +0 -0
  106. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/config1.ini +0 -0
  107. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/conftest.py +0 -0
  108. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/docker-compose.yml +0 -0
  109. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conformance/test_official_validators.py +0 -0
  110. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/conftest.py +0 -0
  111. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/README.md +0 -0
  112. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
  113. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
  114. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
  115. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
  116. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
  117. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
  118. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
  119. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
  120. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
  121. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/images/sample1.png +0 -0
  122. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/images/sample1.svg +0 -0
  123. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/images/userimage01.svg +0 -0
  124. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/ldp_helpers.py +0 -0
  125. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/logo Python Espan/314/203a.svg" +0 -0
  126. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/logo Python Espa/303/261a.svg" +0 -0
  127. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_baking.py +0 -0
  128. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_cli_json.py +0 -0
  129. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_docs.py +0 -0
  130. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_eddsa.py +0 -0
  131. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_errors.py +0 -0
  132. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_examples.py +0 -0
  133. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_issue.py +0 -0
  134. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_issue_credential.py +0 -0
  135. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_jsonmodel.py +0 -0
  136. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_jws.py +0 -0
  137. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_key_operation.py +0 -0
  138. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_keys.py +0 -0
  139. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_badge_io.py +0 -0
  140. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_signer.py +0 -0
  141. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob1_verifier.py +0 -0
  142. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_cli.py +0 -0
  143. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_models.py +0 -0
  144. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob2_signer.py +0 -0
  145. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_conformance_schema.py +0 -0
  146. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_contexts.py +0 -0
  147. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_credential.py +0 -0
  148. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_did.py +0 -0
  149. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_did_doc.py +0 -0
  150. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_endorsement.py +0 -0
  151. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_oid4vci.py +0 -0
  152. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
  153. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_type_metadata.py +0 -0
  154. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_eudi_x5c.py +0 -0
  155. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp.py +0 -0
  156. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_cli.py +0 -0
  157. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_ecdsa_sd.py +0 -0
  158. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_errors.py +0 -0
  159. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_ldp_sign.py +0 -0
  160. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_publish_cli.py +0 -0
  161. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_signer.py +0 -0
  162. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
  163. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_status_registry.py +0 -0
  164. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_ob3_verifier.py +0 -0
  165. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_config.py +0 -0
  166. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_protocol.py +0 -0
  167. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_oid4vci_reservations.py +0 -0
  168. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_openbadges_cli.py +0 -0
  169. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_sign_ecc.pem +0 -0
  170. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_sign_rsa.pem +0 -0
  171. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verify.py +0 -0
  172. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verify_ecc.pem +0 -0
  173. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/test_verify_rsa.pem +0 -0
  174. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/withoutxmlheader.svg +0 -0
  175. {openbadgeslib-4.4.4 → openbadgeslib-4.6.0}/tests/withxmlheader.svg +0 -0
@@ -34,7 +34,7 @@ pip install -e ".[dev]" # add ,ldp and ,eudi to exercise those tracks
34
34
 
35
35
  ## The checks a pull request must pass
36
36
 
37
- CI runs these on Python 3.10–3.14; run them locally before opening a PR (via
37
+ CI runs these on Python 3.10–3.15; run them locally before opening a PR (via
38
38
  your `.venv`):
39
39
 
40
40
  ```sh
@@ -73,14 +73,21 @@ page for the full convention.
73
73
  openbadgeslib supports **every CPython release that is not end-of-life**. The
74
74
  policy is deliberately simple and predictable:
75
75
 
76
+ - A version is **deprecated** once it is in its final months before
77
+ end-of-life (or past it): importing openbadgeslib on it emits a
78
+ `DeprecationWarning`, and `Changelog.txt` announces the major that drops it.
79
+ It stays fully supported — `requires-python`, classifier and CI leg
80
+ unchanged — until that major.
76
81
  - A version is **dropped** — from `requires-python`, the classifiers, and the CI
77
- matrix — at the next **major** release after it reaches end-of-life. For
78
- example, Python **3.10 (EOL 2026-10)** stays supported through 4.x and is
79
- dropped at the next major after that EOL (5.0). Grouping drops into majors
80
- keeps breaking changes rare.
81
- - A new CPython minor is **added** to the CI matrix once it has a stable
82
- (non-pre) release, so downstreams can adopt it early.
83
- - The current matrix is **3.10–3.14**, and `requires-python = ">=3.10"`.
82
+ matrix — at the next **major** release after it reaches end-of-life (for
83
+ example, 3.10 in 5.0; see below). Grouping drops into majors keeps breaking
84
+ changes rare.
85
+ - A new CPython minor is **added** to the CI matrix and the classifiers once it
86
+ has a stable (non-pre) release, so downstreams can adopt it early.
87
+ - The current matrix is **3.10–3.15**, and `requires-python = ">=3.10"`.
88
+ Python **3.10 is deprecated** since 4.6.0 (EOL 2026-10-31) and is dropped in
89
+ 5.0, which will require Python 3.11+
90
+ ([#242](https://github.com/luisgf/openbadgeslib/issues/242)).
84
91
 
85
92
  Security fixes always target the latest release only (see
86
93
  [SECURITY.md](SECURITY.md)).
@@ -4,6 +4,95 @@ OpenBadgesLib - Changelog
4
4
  Newest first. Dates are ISO 8601 (YYYY-MM-DD).
5
5
 
6
6
 
7
+ * v4.6.0 - 2026-10-13
8
+
9
+ - feat: Python 3.15 is officially supported (#243). It joins the
10
+ publish-gating CI test matrix (lint + mypy + the full suite with every
11
+ optional extra now run on 3.10–3.15) and gets its trove classifier, so it
12
+ shows as supported on PyPI. Until now 3.15 only ran as an advisory,
13
+ non-gating pre-release job, which is retired.
14
+
15
+ - feat: Python 3.10 is deprecated (CPython end of life: 2026-10-31).
16
+ Importing openbadgeslib on 3.10 now emits a `DeprecationWarning` (shown
17
+ by default only for scripts and the installed `openbadges*` console
18
+ tools, once, on stderr). Nothing else changes in 4.x: `requires-python`,
19
+ the 3.10 classifier and CI leg stay. 5.0 drops 3.10 and requires Python
20
+ 3.11+ (#242). To silence it, including in a test suite run with
21
+ `filterwarnings = error`, which otherwise fails at import, add the
22
+ message filter
23
+ `ignore:Python 3.10 is deprecated in openbadgeslib:DeprecationWarning`
24
+ (a module-based filter does not match; details on the Installation
25
+ wiki page).
26
+
27
+ - fix: configuration files and mail templates are read as UTF-8 (a leading
28
+ BOM is skipped) and `signer.log` is written as UTF-8 on every platform,
29
+ instead of in the locale encoding. Python 3.15 made UTF-8 the default
30
+ (PEP 686), so on Windows the same non-ASCII config used to be read
31
+ differently (and baked into badges garbled) on 3.10–3.14; a config that
32
+ is not valid UTF-8 now fails on every version with a `ConfigError` that
33
+ says so, and such a mail template makes `-M` report a mail error naming
34
+ the file. `--recipients-file`, already read as UTF-8, now also skips a
35
+ BOM (it used to stick to the first recipient) and reports a file that is
36
+ not valid UTF-8 instead of failing with a traceback.
37
+
38
+ - fix(oid4vci): `SqliteOID4VCIStore` closes the SQLite connection it opened
39
+ when configuring or migrating it fails (e.g. an unknown schema version),
40
+ instead of leaking the handle until garbage collection.
41
+
42
+ - fix: `download_file` closes the response held by an `HTTPError` before
43
+ re-raising it, so a caller that tolerates a failed fetch (the OB1 key and
44
+ revocation lookups) no longer leaves the socket to garbage collection
45
+ (a `ResourceWarning` on recent Pythons). The error's body is therefore
46
+ no longer readable once it propagates; its `.code`, `.headers` and
47
+ message are unaffected.
48
+
49
+ - security(jws): base64url is decoded strictly and canonically (RFC 4648
50
+ §5): the URL-safe alphabet, then no `=` padding or exactly the canonical
51
+ amount, with zero unused trailing bits (§3.5). `urlsafe_b64decode`
52
+ dropped characters outside the alphabet, took `+`/`/` for `-`/`_` and
53
+ ignored extra `=` and the trailing bits, so the bytes of one signature
54
+ had many accepted encodings. A JWS (OB1, OB2) whose signature segment is
55
+ spelled any other way no longer verifies; whitespace around the whole
56
+ token, such as a trailing newline, is ignored, but not inside a segment.
57
+ The library's other base64url decode sites reject the same input as
58
+ malformed: a status list's JWT payload or multibase (`u`) `encodedList`,
59
+ the SD-JWT VC issuer header read to route `x5c` (`EudiError`), and an
60
+ OID4VCI `c_nonce` (invalid, not spent). A bare StatusList2021
61
+ `encodedList` in the standard base64 alphabet (`+`, `/`, `=`), which
62
+ that spec allows, still decodes. OB3 JWT-VC tokens and status-list
63
+ proofs are verified by PyJWT and are unchanged. Prompted by Python 3.15,
64
+ whose `urlsafe_b64decode` warns (`FutureWarning`) that a future version
65
+ will discard `+` and `/`, so the same input would decode differently
66
+ across Python versions.
67
+
68
+
69
+ * v4.5.0 - 2026-09-12
70
+
71
+ - feat(ob3): public `decode_encoded_list(encoded) -> bytes` and
72
+ `set_indices(bitstring) -> frozenset[int]` for a published Bitstring
73
+ Status List `encodedList` (#322). Same multibase `u` / base64url /
74
+ bounded-gzip (5 MiB) / MSB-first decode the verifier already uses;
75
+ the private `_decode_encoded_list` name remains as a thin alias.
76
+
77
+ - feat(ob3): `build_status_list_credential(..., ttl=)` writes Bitstring
78
+ Status List §2.2 `ttl` (milliseconds) on `credentialSubject`, never at
79
+ the top level — `ttl` is a JSON-LD term scoped to `BitstringStatusList`,
80
+ so a top-level copy would not be signed (#321). Negative values raise
81
+ `ValueError`.
82
+
83
+ - feat(ob3): native CLR 2.0 `ClrCredential` builder (`build_clr_credential`,
84
+ `sign_clr_credential`) with the versioned `context-2.0.1.json` bundled
85
+ and hash-pinned (`CLR_CONTEXT_SHA256`) (#323). Identifier objects and
86
+ member credentials are copied as given — email hashing and member-count
87
+ caps stay downstream. Signing injects the pinned context via
88
+ `extra_contexts` (requires `[ldp]`).
89
+
90
+ - feat(ob2): `OB2Verifier.verify_hosted(document)` verifies a hosted
91
+ assertion JSON document (dict or JSON string) without synthesizing a
92
+ dummy JWS. The HTTPS fetcher is injectable via
93
+ `OB2Verifier(..., download=)`.
94
+
95
+
7
96
  * v4.4.4 - 2026-09-10
8
97
 
9
98
  - security(baking): SVG baking strips ``<script>`` / ``foreignObject`` /
@@ -25,7 +25,7 @@ Releases are **CI-driven and credential-less to publish**:
25
25
 
26
26
  - The version is single-sourced in `openbadgeslib/util.py`; a pushed `vX.Y.Z`
27
27
  tag triggers the CI workflow, which runs the full test matrix (Python
28
- 3.10–3.14) and then publishes to
28
+ 3.10–3.15) and then publishes to
29
29
  [PyPI](https://pypi.org/project/openbadgeslib/).
30
30
  - Publishing uses **PyPI Trusted Publishing (OIDC)** — there is **no long-lived
31
31
  PyPI API token** stored anywhere. The publish job authenticates to PyPI from
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 4.4.4
3
+ Version: 4.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
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Programming Language :: Python :: 3.12
23
23
  Classifier: Programming Language :: Python :: 3.13
24
24
  Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: 3.15
25
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
27
  Classifier: Natural Language :: English
27
28
  Classifier: Natural Language :: Spanish
@@ -57,7 +58,7 @@ Dynamic: license-file
57
58
 
58
59
  [![CI](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml/badge.svg)](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
59
60
  [![PyPI](https://img.shields.io/pypi/v/openbadgeslib.svg)](https://pypi.org/project/openbadgeslib/)
60
- [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue.svg)](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
61
+ [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.15-blue.svg)](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
61
62
  [![License](https://img.shields.io/badge/license-LGPLv3%20%2F%20BSD-blue.svg)](#license)
62
63
 
63
64
  **A production-ready Python library & CLI for the full Open Badges 3.0 issuer
@@ -68,7 +69,7 @@ with W3C Bitstring Status Lists and `did:web`. It also ships strict
68
69
  **OpenBadges 2.0** (JWS / hosted assertions) and a frozen **OpenBadges 1.0**
69
70
  legacy format, selected with `-V {1,2,3}` (default `3`).
70
71
 
71
- > **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10 is still supported** (`requires-python >= 3.10`, tested in CI on 3.10–3.14); dropping it is deferred to its end of life. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
72
+ > **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10 stays supported throughout 4.x** (`requires-python >= 3.10`; CI tests 3.10–3.15) but is deprecated since 4.6.0, ahead of its 2026-10-31 end of life; 5.0 drops it and requires Python 3.11+. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
72
73
 
73
74
  ## Features
74
75
 
@@ -96,7 +97,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
96
97
  (EdDSA) — *plus* native **W3C Data Integrity / LDP** signing
97
98
  (`eddsa-rdfc-2022`, Ed25519, optional `[ldp]` extra). Both proof formats
98
99
  issue *and* verify offline, no external service.
99
- - **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.14, a small
100
+ - **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.15, a small
100
101
  dependency set, dataclasses + explicit validation (no Pydantic).
101
102
  - **Dual-licensed** LGPLv3 (library) / BSD-2-Clause (CLI tools).
102
103
 
@@ -122,7 +123,7 @@ additive EUDI SD-JWT VC wallet track.
122
123
  | `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
123
124
  | Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
124
125
  | Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
125
- | Typing / CI | `mypy --strict`, CI 3.10–3.14 | — | — | Pydantic | — |
126
+ | Typing / CI | `mypy --strict`, CI 3.10–3.15 | — | — | Pydantic | — |
126
127
  | License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
127
128
 
128
129
  (a) The actively-maintained community fork (open-educational-badges): it
@@ -141,7 +142,10 @@ all three OB versions.
141
142
 
142
143
  ## Requirements
143
144
 
144
- - Python >= 3.10 (tested on 3.10–3.14)
145
+ - Python >= 3.10 (tested on 3.10–3.15). Python 3.10 is deprecated since
146
+ 4.6.0 — importing openbadgeslib on it emits a `DeprecationWarning` — and
147
+ openbadgeslib 5.0 will require Python 3.11 or newer
148
+ ([#242](https://github.com/luisgf/openbadgeslib/issues/242)).
145
149
  - [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
146
150
  - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
147
151
  - [cryptography](https://pypi.org/project/cryptography/) >= 45
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml/badge.svg)](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/openbadgeslib.svg)](https://pypi.org/project/openbadgeslib/)
5
- [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue.svg)](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
5
+ [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.15-blue.svg)](https://github.com/luisgf/openbadgeslib/actions/workflows/ci.yml)
6
6
  [![License](https://img.shields.io/badge/license-LGPLv3%20%2F%20BSD-blue.svg)](#license)
7
7
 
8
8
  **A production-ready Python library & CLI for the full Open Badges 3.0 issuer
@@ -13,7 +13,7 @@ with W3C Bitstring Status Lists and `did:web`. It also ships strict
13
13
  **OpenBadges 2.0** (JWS / hosted assertions) and a frozen **OpenBadges 1.0**
14
14
  legacy format, selected with `-V {1,2,3}` (default `3`).
15
15
 
16
- > **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10 is still supported** (`requires-python >= 3.10`, tested in CI on 3.10–3.14); dropping it is deferred to its end of life. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
16
+ > **4.0.0 is released** (2026-07-22). It groups the breaking changes so upgrades stay rare: every CLI unifies on one `0/1/2` exit-code contract (a valid-but-untrusted badge now exits `2`, and the 255s are gone), and the legacy pycryptodome/python-ecdsa compat shim is removed — a live key **object** from either is no longer accepted, though key *files* are plain PEM and round-trip unchanged. OpenBadges 1.0 and the `openbadges-<command>` scripts stay, and a new unified `openbadges <command>` front-end joins them. **Python 3.10 stays supported throughout 4.x** (`requires-python >= 3.10`; CI tests 3.10–3.15) but is deprecated since 4.6.0, ahead of its 2026-10-31 end of life; 5.0 drops it and requires Python 3.11+. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
17
17
 
18
18
  ## Features
19
19
 
@@ -41,7 +41,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
41
41
  (EdDSA) — *plus* native **W3C Data Integrity / LDP** signing
42
42
  (`eddsa-rdfc-2022`, Ed25519, optional `[ldp]` extra). Both proof formats
43
43
  issue *and* verify offline, no external service.
44
- - **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.14, a small
44
+ - **Lean and typed** — `mypy --strict`, CI on Python 3.10–3.15, a small
45
45
  dependency set, dataclasses + explicit validation (no Pydantic).
46
46
  - **Dual-licensed** LGPLv3 (library) / BSD-2-Clause (CLI tools).
47
47
 
@@ -67,7 +67,7 @@ additive EUDI SD-JWT VC wallet track.
67
67
  | `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
68
68
  | Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
69
69
  | Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
70
- | Typing / CI | `mypy --strict`, CI 3.10–3.14 | — | — | Pydantic | — |
70
+ | Typing / CI | `mypy --strict`, CI 3.10–3.15 | — | — | Pydantic | — |
71
71
  | License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
72
72
 
73
73
  (a) The actively-maintained community fork (open-educational-badges): it
@@ -86,7 +86,10 @@ all three OB versions.
86
86
 
87
87
  ## Requirements
88
88
 
89
- - Python >= 3.10 (tested on 3.10–3.14)
89
+ - Python >= 3.10 (tested on 3.10–3.15). Python 3.10 is deprecated since
90
+ 4.6.0 — importing openbadgeslib on it emits a `DeprecationWarning` — and
91
+ openbadgeslib 5.0 will require Python 3.11 or newer
92
+ ([#242](https://github.com/luisgf/openbadgeslib/issues/242)).
90
93
  - [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
91
94
  - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
92
95
  - [cryptography](https://pypi.org/project/cryptography/) >= 45
@@ -72,6 +72,19 @@ from .issue import ( # noqa: F401
72
72
  from .verify import verify_badge # noqa: F401
73
73
 
74
74
 
75
+ # ── Supported Python versions ────────────────────────────────────────────────
76
+ # On a deprecated CPython (3.10, dropped in 5.0 — #242) the package's first
77
+ # import emits a DeprecationWarning, whichever entry point triggers it (import
78
+ # openbadgeslib, a submodule, an installed console script). PEP 565 shows it by
79
+ # default only when __main__ is the importer: a script or a console script,
80
+ # not `python -m openbadgeslib.<tool>` or an importlib-based loader (attributed
81
+ # to runpy/importlib, so hidden unless warnings are enabled: -W, -X dev,
82
+ # pytest). It runs after the imports above because a call placed before them
83
+ # would trip flake8's E402.
84
+ from . import _python_support
85
+ _python_support.warn_if_deprecated()
86
+
87
+
75
88
  # ── OpenBadges 1.0 (legacy) ──────────────────────────────────────────────────
76
89
  # The unprefixed OB1 names (Signer, Verifier, Badge, …) stay importable from
77
90
  # the top-level package for backward compatibility. They are the legacy
@@ -127,6 +127,10 @@ def verify_block(msg: Union[str, bytes], key: Optional[Any] = None) -> bool:
127
127
  """Verify a JWS compact serialization (bytes or str). Returns True or raises SignatureError."""
128
128
  if isinstance(msg, str):
129
129
  msg = msg.encode('utf-8')
130
+ # Transport whitespace around the whole token (a trailing newline from a
131
+ # file or a third-party baker) is not part of any signed segment; inside
132
+ # a segment it still fails the strict base64url decode.
133
+ msg = msg.strip()
130
134
 
131
135
  try:
132
136
  head_b64, payload_b64, sig_b64 = msg.split(b'.')
@@ -0,0 +1,57 @@
1
+ import base64
2
+ import binascii
3
+ import json
4
+ import re
5
+
6
+ from typing import Any, Union
7
+
8
+ _B64URL_ALPHABET = re.compile(rb'[A-Za-z0-9_-]*')
9
+
10
+
11
+ def base64url_decode(data: Union[str, bytes]) -> bytes:
12
+ # Strict, canonical RFC 4648 §5: the URL-safe alphabet, then either no
13
+ # padding (JWS/JWT strip it) or exactly the canonical (-len) % 4 '='
14
+ # characters, with zero unused trailing bits (§3.5). urlsafe_b64decode()
15
+ # silently drops other characters, takes '+'/'/' as aliases of '-'/'_'
16
+ # (Python 3.15 warns that this will change), and ignores extra '=' and
17
+ # the trailing bits, so the same bytes had many accepted encodings. Every
18
+ # caller feeds it untrusted input, so anything else raises binascii.Error
19
+ # (a ValueError); a non-str/bytes argument raises TypeError.
20
+ if isinstance(data, str):
21
+ if not data.isascii():
22
+ raise binascii.Error('base64url data must be ASCII')
23
+ data = data.encode('ascii')
24
+ elif isinstance(data, (bytearray, memoryview)):
25
+ data = bytes(data)
26
+ elif not isinstance(data, bytes):
27
+ raise TypeError('base64url data must be str or bytes, not %s'
28
+ % type(data).__name__)
29
+ stripped = data.rstrip(b'=')
30
+ pad = -len(stripped) % 4
31
+ if (not _B64URL_ALPHABET.fullmatch(stripped) or len(stripped) % 4 == 1
32
+ or len(data) - len(stripped) not in (0, pad)):
33
+ raise binascii.Error('invalid base64url data')
34
+ raw = base64.urlsafe_b64decode(stripped + b'=' * pad)
35
+ if base64.urlsafe_b64encode(raw).rstrip(b'=') != stripped:
36
+ raise binascii.Error('non-canonical base64url data (non-zero pad bits)')
37
+ return raw
38
+
39
+
40
+ def base64url_encode(input: bytes) -> bytes:
41
+ return base64.urlsafe_b64encode(input).replace(b'=', b'')
42
+
43
+
44
+ def to_json(a: Any) -> bytes:
45
+ return json.dumps(a).encode('utf-8')
46
+
47
+
48
+ def from_json(a: Union[str, bytes]) -> Any:
49
+ if isinstance(a, (bytes, bytearray)):
50
+ a = a.decode('utf-8')
51
+ return json.loads(a)
52
+
53
+
54
+ def to_base64(a: bytes) -> bytes: return base64url_encode(a)
55
+ def from_base64(a: bytes) -> bytes: return base64url_decode(a)
56
+ def encode(a: Any) -> bytes: return to_base64(to_json(a))
57
+ def decode(a: bytes) -> Any: return from_json(from_base64(a))
@@ -0,0 +1,54 @@
1
+ """
2
+ OpenBadges Library
3
+
4
+ Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
5
+
6
+ All rights reserved.
7
+
8
+ This library is free software; you can redistribute it and/or
9
+ modify it under the terms of the GNU Lesser General Public
10
+ License as published by the Free Software Foundation; either
11
+ version 3.0 of the License, or (at your option) any later version.
12
+
13
+ This library is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
+ Lesser General Public License for more details.
17
+
18
+ You should have received a copy of the GNU Lesser General Public
19
+ License along with this library.
20
+ """
21
+
22
+ # The runtime side of the "Supported Python versions" policy (CONTRIBUTING.md):
23
+ # a CPython that is still supported but nearing (or past) end of life is
24
+ # deprecated — importing openbadgeslib on it emits a DeprecationWarning — and
25
+ # is dropped only at the next major release.
26
+ #
27
+ # Python 3.10 is deprecated since 4.6.0 and dropped in 5.0 (#242). That drop
28
+ # raises requires-python, the mypy python_version and DEPRECATED_BELOW together,
29
+ # rewording DEPRECATION_MESSAGE for the next deprecated version — or deletes
30
+ # this module (and its call in __init__) if none is pending removal.
31
+
32
+ import sys
33
+ import warnings
34
+ from typing import Tuple
35
+
36
+ #: Importing openbadgeslib on a Python older than this emits the warning below.
37
+ DEPRECATED_BELOW = (3, 11)
38
+
39
+ DEPRECATION_MESSAGE = (
40
+ 'Python 3.10 is deprecated in openbadgeslib (CPython end of life: '
41
+ '2026-10-31); openbadgeslib 5.0 will require Python 3.11 or newer.')
42
+
43
+
44
+ def warn_if_deprecated(version_info: Tuple[object, ...] = sys.version_info) -> None:
45
+ """Emit DEPRECATION_MESSAGE if *version_info* is a deprecated Python.
46
+
47
+ Called once from the package ``__init__``; tests pass a fake version.
48
+ """
49
+ if version_info < DEPRECATED_BELOW:
50
+ # stacklevel=3 skips this helper and the package __init__ (warnings
51
+ # also skips importlib's bootstrap frames, bpo-24305), so the warning
52
+ # is attributed to the code that imported openbadgeslib: PEP 565 then
53
+ # shows it for a script (__main__) and hides it for a library import.
54
+ warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=3)
@@ -593,11 +593,14 @@ class ConfParser():
593
593
  self.parser = ConfigParser(interpolation=ExtendedInterpolation())
594
594
 
595
595
  try:
596
- self.parser.read(self.config_file)
597
- except UnicodeDecodeError:
598
- # We should raise an UnicodeDecodeError, but the error message is too cryptic.#
599
- raise ConfigError("The encoding of the configuration file and the default encoding of "
600
- "the operating system mismatch") from None
596
+ # Always UTF-8 (a BOM, as some Windows editors write, is skipped):
597
+ # the locale encoding would make the same file read differently
598
+ # across platforms and across 3.10-3.15 (PEP 686).
599
+ self.parser.read(self.config_file, encoding='utf-8-sig')
600
+ except UnicodeDecodeError as exc:
601
+ # The raw UnicodeDecodeError message is too cryptic.
602
+ raise ConfigError("Configuration file %s is not valid UTF-8: %s"
603
+ % (self.config_file, exc)) from None
601
604
  except ConfigParserError as exc:
602
605
  # Malformed INI syntax (duplicate section/option, a value line
603
606
  # with no section header before it, ...) raises directly from
@@ -126,8 +126,15 @@ class BadgeMail():
126
126
  """ Return the Subject and Body of the Email. The first line of the file
127
127
  is used as Subject """
128
128
 
129
- with open(file, 'r') as f:
130
- data = f.readlines()
129
+ # UTF-8 on every platform and Python (PEP 686); a BOM is skipped so it
130
+ # cannot end up in the Subject.
131
+ try:
132
+ with open(file, 'r', encoding='utf-8-sig') as f:
133
+ data = f.readlines()
134
+ except UnicodeDecodeError as exc:
135
+ # Name the file: the bare codec message does not say what failed.
136
+ raise ValueError('Mail template %s is not valid UTF-8: %s'
137
+ % (file, exc)) from exc
131
138
 
132
139
  if data:
133
140
  return data[0].strip('\n'), ''.join(data[1:])
@@ -65,7 +65,8 @@ class Assertion():
65
65
  @staticmethod
66
66
  def decode(data: bytes) -> 'Assertion':
67
67
  try:
68
- header, body, signature = data.split(b'.')
68
+ # Whitespace around the token is transport noise (see verify_block).
69
+ header, body, signature = data.strip().split(b'.')
69
70
  return Assertion(header, body, signature)
70
71
  except Exception:
71
72
  raise AssertionFormatIncorrect()
@@ -22,7 +22,7 @@
22
22
 
23
23
  import logging
24
24
  from datetime import datetime, timezone
25
- from typing import Any, List, Optional, Union
25
+ from typing import Any, Callable, List, Optional, Union
26
26
  from urllib.parse import urlparse
27
27
 
28
28
  from .models import Assertion, CryptographicKey, hash_identity, _parse_iso
@@ -67,10 +67,16 @@ class OB2Verifier:
67
67
  pubkey_pem: Optional PEM-encoded trusted public key. When supplied it is
68
68
  used to verify SignedBadge assertions and skips resolving the
69
69
  badge-declared key.
70
+ download: Optional ``url -> bytes`` fetcher used by hosted verification
71
+ (assertion id, BadgeClass, issuer Profile, revocation list).
72
+ Defaults to :func:`download_file` (HTTPS-only, size-capped).
70
73
  """
71
74
 
72
- def __init__(self, pubkey_pem: Optional[Any] = None) -> None:
75
+ def __init__(self, pubkey_pem: Optional[Any] = None, *,
76
+ download: Optional[Callable[[str], bytes]] = None) -> None:
73
77
  self.trusted_pubkey_pem: Optional[Union[str, bytes]] = None
78
+ self._download: Callable[[str], bytes] = (
79
+ download if download is not None else download_file)
74
80
  if pubkey_pem is not None:
75
81
  pem = key_to_pem(pubkey_pem)
76
82
  try:
@@ -122,6 +128,67 @@ class OB2Verifier:
122
128
 
123
129
  return assertion
124
130
 
131
+ def verify_hosted(self, document: Union[str, dict[str, Any]], *,
132
+ expected_recipient: Optional[str] = None,
133
+ check_revocation: bool = False) -> Assertion:
134
+ """Verify a hosted OB 2.0 assertion JSON document (not a compact JWS).
135
+
136
+ *document* is the assertion as a dict or a JSON string. The hosted
137
+ model is fetch-from-id: the copy served at ``assertion.id`` is the
138
+ trust anchor (scoped to the issuer Profile), so this does not
139
+ synthesize a dummy JWS. Defence-in-depth JWS verification inside
140
+ :meth:`_verify_hosted` is skipped when there is no token.
141
+
142
+ ``expected_recipient`` and ``check_revocation`` behave as in
143
+ :meth:`verify`. Raises :class:`OB2VerificationError` if the document
144
+ is not a HostedBadge assertion or if any check fails.
145
+ """
146
+ parsed = self._hosted_document(document)
147
+ try:
148
+ assertion = Assertion.from_dict(parsed)
149
+ except ValueError as exc:
150
+ raise OB2VerificationError(
151
+ "Malformed OB 2.0 assertion: %s" % exc) from exc
152
+ if assertion.verification.type != "HostedBadge":
153
+ raise OB2VerificationError(
154
+ "verify_hosted() requires verification.type HostedBadge, got %r"
155
+ % assertion.verification.type)
156
+
157
+ assertion = self._verify_hosted(assertion, "")
158
+
159
+ self._check_expiration(assertion)
160
+
161
+ if expected_recipient is not None:
162
+ self._check_identity(assertion, expected_recipient)
163
+
164
+ if check_revocation:
165
+ self._check_revocation(assertion)
166
+
167
+ return assertion
168
+
169
+ @staticmethod
170
+ def _hosted_document(document: Union[str, dict[str, Any]]) -> dict[str, Any]:
171
+ """Parse a hosted assertion from a dict or JSON string (never a JWS)."""
172
+ if isinstance(document, str):
173
+ text = document.strip()
174
+ if text.count('.') == 2 and not text.startswith('{'):
175
+ raise OB2VerificationError(
176
+ "verify_hosted() expects a hosted assertion JSON document, "
177
+ "not a compact JWS; use verify() for SignedBadge tokens")
178
+ try:
179
+ parsed = jws_utils.from_json(text)
180
+ except Exception as exc:
181
+ raise OB2VerificationError(
182
+ "hosted assertion is not valid JSON: %s" % exc) from exc
183
+ elif isinstance(document, dict):
184
+ parsed = document
185
+ else:
186
+ raise OB2VerificationError(
187
+ "hosted assertion must be a dict or JSON string")
188
+ if not isinstance(parsed, dict):
189
+ raise OB2VerificationError("hosted assertion is not a JSON object")
190
+ return parsed
191
+
125
192
  # ── signed path ──────────────────────────────────────────────────────────────
126
193
 
127
194
  def _verify_signed(self, assertion: Assertion, token: str) -> None:
@@ -255,16 +322,18 @@ class OB2Verifier:
255
322
  # verify it, but never fail the hosted verdict on it (a hosted badge is
256
323
  # not required to be signed at all). The creator comes from the
257
324
  # authoritative copy, so this never dereferences a holder-chosen URL.
258
- try:
259
- if self.trusted_pubkey_pem is not None:
260
- self._verify_jws(token, self.trusted_pubkey_pem)
261
- elif authoritative.verification.creator:
262
- key = self._resolve_creator(authoritative.verification.creator)
263
- self._verify_jws(token, key.public_key_pem.encode('utf-8'))
264
- except OB2VerificationError as exc:
265
- logger.debug("Hosted assertion %s: baked JWS did not verify (%s); "
266
- "hosted trust comes from the HTTPS fetch, not the signature.",
267
- assertion.id, exc)
325
+ # verify_hosted() passes an empty token (there is no JWS); skip then.
326
+ if token:
327
+ try:
328
+ if self.trusted_pubkey_pem is not None:
329
+ self._verify_jws(token, self.trusted_pubkey_pem)
330
+ elif authoritative.verification.creator:
331
+ key = self._resolve_creator(authoritative.verification.creator)
332
+ self._verify_jws(token, key.public_key_pem.encode('utf-8'))
333
+ except OB2VerificationError as exc:
334
+ logger.debug("Hosted assertion %s: baked JWS did not verify (%s); "
335
+ "hosted trust comes from the HTTPS fetch, not the signature.",
336
+ assertion.id, exc)
268
337
  return authoritative
269
338
 
270
339
  def _check_hosted_scope(self, assertion_id: str, issuer: dict[str, Any]) -> None:
@@ -373,7 +442,7 @@ class OB2Verifier:
373
442
 
374
443
  def _fetch_json(self, url: str, where: str) -> dict[str, Any]:
375
444
  try:
376
- raw = download_file(url)
445
+ raw = self._download(url)
377
446
  except Exception as exc:
378
447
  raise OB2VerificationError(
379
448
  "Could not fetch %s at %s: %s" % (where, url, exc)) from exc
@@ -27,9 +27,12 @@ from .verifier import (OB3VerificationError, OB3Verifier,
27
27
  verify_endorsement_jwt)
28
28
  from .ldp import (OB3LdpSigner, OB3LdpVerifier, add_data_integrity_proof,
29
29
  verify_data_integrity_proof)
30
- from .status import check_credential_status
30
+ from .status import check_credential_status, decode_encoded_list, set_indices
31
31
  from .status_list import (build_status_list_credential, encode_bitstring,
32
32
  sign_status_list_credential, status_entry)
33
+ from .clr import (CLR_CONTEXT_SHA256, CLR_CONTEXT_URL, OB_V3_CONTEXT,
34
+ VCDM_V2_CONTEXT, build_clr_credential, bundled_clr_contexts,
35
+ sign_clr_credential)
33
36
  from .status_registry import StatusRegistry
34
37
  from .did import (build_did_document, did_jwk_from_jwk, did_key_from_pem,
35
38
  did_web_from_url, multikey_from_pem, resolve_did,
@@ -68,16 +71,25 @@ __all__ = [
68
71
  'verify_data_integrity_proof',
69
72
  'StatusRegistry',
70
73
  'build_did_document',
74
+ 'build_clr_credential',
71
75
  'build_status_list_credential',
76
+ 'bundled_clr_contexts',
72
77
  'check_credential_status',
78
+ 'CLR_CONTEXT_SHA256',
79
+ 'CLR_CONTEXT_URL',
80
+ 'decode_encoded_list',
73
81
  'did_jwk_from_jwk',
74
82
  'did_key_from_pem',
75
83
  'did_web_from_url',
76
84
  'encode_bitstring',
77
85
  'multikey_from_pem',
86
+ 'OB_V3_CONTEXT',
78
87
  'resolve_did',
79
88
  'resolve_verification_method',
89
+ 'set_indices',
90
+ 'sign_clr_credential',
80
91
  'sign_status_list_credential',
81
92
  'status_entry',
93
+ 'VCDM_V2_CONTEXT',
82
94
  'verify_endorsement_jwt',
83
95
  ]