openbadgeslib 4.6.1__tar.gz → 4.6.2__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 (176) hide show
  1. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/Changelog.txt +65 -0
  2. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/PKG-INFO +6 -6
  3. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/did.py +33 -1
  4. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/eudi.py +186 -10
  5. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/ldp.py +5 -1
  6. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/util.py +1 -1
  7. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/PKG-INFO +6 -6
  8. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/requires.txt +5 -5
  9. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/pyproject.toml +22 -11
  10. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_did.py +32 -0
  11. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_sd_jwt.py +316 -0
  12. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_type_metadata.py +13 -0
  13. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_x5c.py +82 -2
  14. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/CONTRIBUTING.md +0 -0
  15. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/GOVERNANCE.md +0 -0
  16. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/LICENSE.txt +0 -0
  17. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/MANIFEST.in +0 -0
  18. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/README.md +0 -0
  19. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/SECURITY.md +0 -0
  20. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/docs/README.md +0 -0
  21. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/__init__.py +0 -0
  22. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jsonmodel.py +0 -0
  23. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jws/__init__.py +0 -0
  24. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jws/exceptions.py +0 -0
  25. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_jws/utils.py +0 -0
  26. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_ob1_api.py +0 -0
  27. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/_python_support.py +0 -0
  28. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/badge_model.py +0 -0
  29. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/baking.py +0 -0
  30. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/cli_common.py +0 -0
  31. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/config.ini.example +0 -0
  32. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/confparser.py +0 -0
  33. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/errors.py +0 -0
  34. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/issue.py +0 -0
  35. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/keys.py +0 -0
  36. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/logs.py +0 -0
  37. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/mail.py +0 -0
  38. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/__init__.py +0 -0
  39. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/badge.py +0 -0
  40. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/signer.py +0 -0
  41. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob1/verifier.py +0 -0
  42. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/__init__.py +0 -0
  43. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/models.py +0 -0
  44. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/signer.py +0 -0
  45. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob2/verifier.py +0 -0
  46. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/__init__.py +0 -0
  47. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/_context_validation.py +0 -0
  48. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/clr.py +0 -0
  49. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/__init__.py +0 -0
  50. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/clr_context_2.0.1.json +0 -0
  51. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
  52. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
  53. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
  54. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
  55. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +0 -0
  56. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
  57. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
  58. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/credential.py +0 -0
  59. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/publish.py +0 -0
  60. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/signer.py +0 -0
  61. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/status.py +0 -0
  62. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/status_list.py +0 -0
  63. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/status_registry.py +0 -0
  64. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/ob3/verifier.py +0 -0
  65. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/__init__.py +0 -0
  66. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/codes.py +0 -0
  67. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/errors.py +0 -0
  68. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/formats.py +0 -0
  69. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/handler.py +0 -0
  70. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/memory_store.py +0 -0
  71. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/metadata.py +0 -0
  72. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/nonce.py +0 -0
  73. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/offer.py +0 -0
  74. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/reconcile.py +0 -0
  75. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/sqlite_store.py +0 -0
  76. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/oid4vci/store.py +0 -0
  77. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_cli.py +0 -0
  78. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_init.py +0 -0
  79. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_keygenerator.py +0 -0
  80. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_publish.py +0 -0
  81. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_signer.py +0 -0
  82. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_status.py +0 -0
  83. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/openbadges_verifier.py +0 -0
  84. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/py.typed +0 -0
  85. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib/verify.py +0 -0
  86. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/SOURCES.txt +0 -0
  87. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  88. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/entry_points.txt +0 -0
  89. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/openbadgeslib.egg-info/top_level.txt +0 -0
  90. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/setup.cfg +0 -0
  91. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/config1.ini +0 -0
  92. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/Dockerfile.ob3 +0 -0
  93. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/README.md +0 -0
  94. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/conftest.py +0 -0
  95. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/docker-compose.yml +0 -0
  96. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/relax-danubetech-checksums.awk +0 -0
  97. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conformance/test_official_validators.py +0 -0
  98. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/conftest.py +0 -0
  99. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/README.md +0 -0
  100. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
  101. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
  102. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/ob_schemas/refresh.sh +0 -0
  103. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/README.md +0 -0
  104. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
  105. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
  106. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
  107. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
  108. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
  109. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/images/sample1.png +0 -0
  110. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/images/sample1.svg +0 -0
  111. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/images/userimage01.svg +0 -0
  112. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/ldp_helpers.py +0 -0
  113. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/logo Python Espan/314/203a.svg" +0 -0
  114. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/logo Python Espa/303/261a.svg" +0 -0
  115. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_b64url.py +0 -0
  116. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_baking.py +0 -0
  117. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_cli_json.py +0 -0
  118. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_cli_smoke.py +0 -0
  119. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_confparser.py +0 -0
  120. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_docs.py +0 -0
  121. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_eddsa.py +0 -0
  122. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_errors.py +0 -0
  123. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_examples.py +0 -0
  124. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_issue.py +0 -0
  125. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_issue_credential.py +0 -0
  126. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_jsonmodel.py +0 -0
  127. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_jws.py +0 -0
  128. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_key_operation.py +0 -0
  129. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_keys.py +0 -0
  130. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_mail.py +0 -0
  131. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_badge_io.py +0 -0
  132. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_deprecation.py +0 -0
  133. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_signer.py +0 -0
  134. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob1_verifier.py +0 -0
  135. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_cli.py +0 -0
  136. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_error_branches.py +0 -0
  137. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_models.py +0 -0
  138. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_signer.py +0 -0
  139. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob2_verifier.py +0 -0
  140. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_clr.py +0 -0
  141. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_conformance_schema.py +0 -0
  142. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_contexts.py +0 -0
  143. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_credential.py +0 -0
  144. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_did_doc.py +0 -0
  145. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_endorsement.py +0 -0
  146. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_eudi_oid4vci.py +0 -0
  147. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp.py +0 -0
  148. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_cli.py +0 -0
  149. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_context_validation.py +0 -0
  150. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_ecdsa_sd.py +0 -0
  151. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_errors.py +0 -0
  152. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_ldp_sign.py +0 -0
  153. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_publish_cli.py +0 -0
  154. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_signer.py +0 -0
  155. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_signer_cli_ldp.py +0 -0
  156. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_status.py +0 -0
  157. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_status_list.py +0 -0
  158. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_status_registry.py +0 -0
  159. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_ob3_verifier.py +0 -0
  160. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_config.py +0 -0
  161. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_protocol.py +0 -0
  162. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_reservations.py +0 -0
  163. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_oid4vci_store.py +0 -0
  164. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_openbadges_cli.py +0 -0
  165. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_public_api.py +0 -0
  166. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_python_support.py +0 -0
  167. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_sign_ecc.pem +0 -0
  168. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_sign_rsa.pem +0 -0
  169. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_signer_batch.py +0 -0
  170. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_util.py +0 -0
  171. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verifier_cli_coverage.py +0 -0
  172. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verify.py +0 -0
  173. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verify_ecc.pem +0 -0
  174. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/test_verify_rsa.pem +0 -0
  175. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/withoutxmlheader.svg +0 -0
  176. {openbadgeslib-4.6.1 → openbadgeslib-4.6.2}/tests/withxmlheader.svg +0 -0
@@ -4,6 +4,71 @@ OpenBadgesLib - Changelog
4
4
  Newest first. Dates are ISO 8601 (YYYY-MM-DD).
5
5
 
6
6
 
7
+ * v4.6.2 - 2026-10-16
8
+
9
+ - security(deps): the openvc-core floor of `[eudi]`, `[ldp-sd]` and
10
+ `[oid4vci]` rises to 1.35.0. Two behaviours below it reach this
11
+ library's public API. An SD-JWT VC whose issuer JWT carries `aud`
12
+ verified for any relying party, so a badge its issuer restricted to one
13
+ verifier was honoured by all (RFC 7519 §4.1.3); 1.35.0 enforces it, and
14
+ `verify_badge_sd_jwt(audience=…)` now also gates it. And
15
+ `status_list_token_resolver(leeway_s=…)` took any number, so an infinite
16
+ leeway made an expired status-list token verify as current; 1.35.0
17
+ accepts whole seconds from 0 to 86400 only. Neither is an advisory in
18
+ openvc-core. The run-time guard of the ecdsa-sd-2023 verify path stays at
19
+ 1.33.2, the security minimum of that path. `[ldp]` and `[ldp-sd]` also cap
20
+ pyld below 4: pyld 4.0 (unreleased, breaking) drops statements from the
21
+ signed dataset that 3.x keeps, an IRI with two `#` or a literal with an
22
+ invalid `@language`, which context validation does not refuse, so the
23
+ credential would still show what the proof no longer covers.
24
+
25
+ - security(eudi): `verify_badge_sd_jwt` enforces the badge's own
26
+ `validFrom` and `validUntil` on both trust paths (pinned key and X.509
27
+ trust anchors), and `issue_badge_sd_jwt` writes the credential's
28
+ `validUntil` as the issuer JWT's `exp`, and a `validFrom` still ahead as
29
+ `nbf`. A badge is a flat IETF SD-JWT VC, and openvc-core reads
30
+ `validFrom` / `validUntil` as a validity window only for a W3C VCDM
31
+ payload, so a badge past its `validUntil` verified as unexpired unless
32
+ `expires_in_s` happened to be passed; badges issued by other
33
+ implementations with only a `validUntil` were affected the same way. The
34
+ earlier of `validUntil` and `expires_in_s` wins, a window value that is
35
+ not an ISO 8601 timestamp with an offset (as `datetime.fromisoformat`
36
+ reads it, which is stricter before Python 3.11) fails closed, and the
37
+ leeway is the 60 seconds the other OB 3.0 verifiers use.
38
+ `issue_badge_sd_jwt` now refuses, with `EudiError`, to make `name`,
39
+ `achievement`, `validFrom` or `validUntil` selectively disclosable: a
40
+ holder could withhold the disclosure and present an expired badge as one
41
+ that never expires, and the stripped presentation still verified. Only
42
+ the recipient (`credentialSubject`) is meant to be withheld. A bare string
43
+ for `disclosable=` is refused too (it was read one character at a time,
44
+ which left the recipient in the clear), and `expires_in_s` must be a
45
+ number within about 317 years of now. Nothing can detect a withheld
46
+ disclosure on a badge that already exists, and an existing badge has no
47
+ `exp`/`nbf` for other verifiers: re-issue badges issued with a
48
+ `disclosable=` that named one of those claims, and any badge that must
49
+ stop verifying at its `validUntil` everywhere. The Type Metadata document
50
+ is unchanged, byte for byte (a change would break the `vct#integrity` pin
51
+ of every badge already issued), and a test now pins its hash.
52
+ With `x5c_trust_anchors`, `verify_badge_sd_jwt` no longer accepts a plain
53
+ JWT (a token with no `~`): openvc-core's `verify_credential` takes one
54
+ and then ignored `expected_vct` and `require_key_binding` and returned
55
+ another type, so a JWT signed by an anchored issuer passed as a badge;
56
+ the pinned-key path always refused it. `status_list_token_resolver`
57
+ validates `leeway_s` when it is built, so a bad value fails there rather
58
+ than on the first status check of a later badge. A hostile issuer JWT
59
+ header nested deeper than the JSON parser recurses now fails as
60
+ `EudiError` instead of escaping `verify_badge_sd_jwt(x5c_trust_anchors=…)`
61
+ as a bare `RecursionError`, and a window value too long to be a timestamp
62
+ is not echoed back in the error.
63
+
64
+ - fix(did): a `did:web` document nested deeper than the JSON parser
65
+ recurses, or deeper than 64 levels (a DID document is a handful), is
66
+ reported as the usual `OB3VerificationError` instead of escaping
67
+ `resolve_did`, `fetch_did_web_document` and the verifiers that resolve an
68
+ issuer's DID as a bare `RecursionError`. CPython 3.14 and later parse
69
+ nesting far past the recursion limit, so the parser alone no longer bounds
70
+ what the later `repr()` and `json.dumps` of the document survive.
71
+
7
72
  * v4.6.1 - 2026-10-14
8
73
 
9
74
  - security(ldp): `OB3LdpVerifier` reads only JSON its Data Integrity proof
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 4.6.1
3
+ Version: 4.6.2
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
@@ -35,14 +35,14 @@ Requires-Dist: PyJWT[crypto]>=2.15.1
35
35
  Requires-Dist: cryptography>=45
36
36
  Requires-Dist: defusedxml>=0.7
37
37
  Provides-Extra: ldp
38
- Requires-Dist: pyld>=3.3.0; extra == "ldp"
38
+ Requires-Dist: pyld<4,>=3.3.0; extra == "ldp"
39
39
  Provides-Extra: eudi
40
- Requires-Dist: openvc-core<2,>=1.33.2; extra == "eudi"
40
+ Requires-Dist: openvc-core<2,>=1.35.0; extra == "eudi"
41
41
  Provides-Extra: ldp-sd
42
- Requires-Dist: openvc-core[data-integrity]<2,>=1.33.2; extra == "ldp-sd"
43
- Requires-Dist: pyld>=3.3.0; extra == "ldp-sd"
42
+ Requires-Dist: openvc-core[data-integrity]<2,>=1.35.0; extra == "ldp-sd"
43
+ Requires-Dist: pyld<4,>=3.3.0; extra == "ldp-sd"
44
44
  Provides-Extra: oid4vci
45
- Requires-Dist: openvc-core<2,>=1.33.2; extra == "oid4vci"
45
+ Requires-Dist: openvc-core<2,>=1.35.0; extra == "oid4vci"
46
46
  Provides-Extra: dev
47
47
  Requires-Dist: pytest>=8.0; extra == "dev"
48
48
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
@@ -258,14 +258,46 @@ def _fetch_did_web_document(did: str, fetch: Any) -> dict[str, Any]:
258
258
  "could not fetch DID document %s: %s" % (url, exc)) from exc
259
259
  try:
260
260
  doc = json.loads(raw.decode('utf-8'))
261
- except (ValueError, UnicodeDecodeError) as exc:
261
+ except (ValueError, UnicodeDecodeError, RecursionError) as exc:
262
+ # RecursionError: the host serves a document nested deeper than the
263
+ # parser recurses, which is untrusted input like any other malformed one.
262
264
  raise OB3VerificationError(
263
265
  "malformed DID document at %s: %s" % (url, exc)) from exc
264
266
  if not isinstance(doc, dict):
265
267
  raise OB3VerificationError("DID document is not a JSON object")
268
+ if _nested_deeper_than(doc, MAX_DID_DOCUMENT_DEPTH):
269
+ raise OB3VerificationError(
270
+ "DID document at %s is nested more than %d levels deep"
271
+ % (url, MAX_DID_DOCUMENT_DEPTH))
266
272
  return doc
267
273
 
268
274
 
275
+ #: A DID document is shallow (a handful of levels). json.loads accepts nesting
276
+ #: far past what anything that later walks, repr()s or re-serialises the document
277
+ #: survives (on CPython 3.14 and later its limit is a C stack budget, well beyond
278
+ #: the recursion limit), and the host serving it is the one party the verifier
279
+ #: does not control.
280
+ MAX_DID_DOCUMENT_DEPTH = 64
281
+
282
+
283
+ def _nested_deeper_than(value: Any, limit: int) -> bool:
284
+ """True if *value* (parsed JSON) has containers nested more than *limit*
285
+ deep. Iterative: the point is to survive input a recursive walk would not."""
286
+ stack: list[Tuple[Any, int]] = [(value, 1)]
287
+ while stack:
288
+ node, depth = stack.pop()
289
+ if isinstance(node, dict):
290
+ children: Any = node.values()
291
+ elif isinstance(node, list):
292
+ children = node
293
+ else:
294
+ continue
295
+ if depth > limit:
296
+ return True
297
+ stack.extend((child, depth + 1) for child in children)
298
+ return False
299
+
300
+
269
301
  def fetch_did_web_document(did: str, download: Any = None) -> dict[str, Any]:
270
302
  """Fetch and parse the DID document a did:web identifier resolves to.
271
303
 
@@ -41,15 +41,19 @@
41
41
  import base64
42
42
  import hashlib
43
43
  import json
44
+ import math
45
+ import time
44
46
 
47
+ from datetime import datetime, timezone
45
48
  from typing import Any, Callable, Iterable, Mapping, Optional, Union, cast
46
49
 
47
50
  from .credential import OpenBadgeCredential
51
+ from .._jsonmodel import _parse_iso
48
52
  from .._jws.utils import base64url_decode
49
53
  from ..errors import LibOpenBadgesException, UnknownKeyType
50
54
  from ..keys import (
51
55
  KeyType, detect_key_type, ec_curve_from_pem, public_jwk_from_pem)
52
- from ..util import download_file
56
+ from ..util import CLOCK_SKEW_LEEWAY, download_file
53
57
 
54
58
  # A vct (Verifiable Credential Type) for Open Badges expressed as SD-JWT VC.
55
59
  OB3_SD_JWT_VCT = "https://purl.imsglobal.org/spec/ob/v3p0#OpenBadgeCredential"
@@ -61,6 +65,25 @@ OB3_SD_JWT_FORMAT = "dc+sd-jwt"
61
65
  # holder can present the achievement while withholding who they are.
62
66
  DEFAULT_DISCLOSABLE = ("credentialSubject",)
63
67
 
68
+ # Claims issue_badge_sd_jwt refuses to make selectively disclosable. A holder
69
+ # may withhold any disclosure and a withheld one is indistinguishable from a
70
+ # decoy (RFC 9901 §9.7), so a disclosable validFrom/validUntil lets the holder
71
+ # present an expired or not-yet-valid badge as one with no validity window; the
72
+ # achievement and its name are what the badge IS, and the Type Metadata marks
73
+ # them mandatory. Only the recipient identity is meant to be withheld.
74
+ _NEVER_DISCLOSABLE = ("name", "achievement", "validFrom", "validUntil")
75
+
76
+ # The largest leeway_s openvc-core accepts for a status-list token (one day).
77
+ _MAX_LEEWAY_S = 86400
78
+
79
+ # The longest validity timestamp worth parsing ("2026-10-16T12:34:56.123456+00:00"
80
+ # is 32); anything longer is not one and is not echoed back in an error message.
81
+ _MAX_TIMESTAMP_CHARS = 64
82
+
83
+ # A bound on expires_in_s (about 317 years): enough for any real credential, and
84
+ # it keeps the exp a number every verifier can read.
85
+ _MAX_EXPIRES_IN_S = 10 ** 10
86
+
64
87
  _INSTALL_HINT = (
65
88
  "SD-JWT VC support needs the [eudi] extra: pip install openbadgeslib[eudi]")
66
89
 
@@ -183,8 +206,12 @@ def badge_type_metadata(vct: str = OB3_SD_JWT_VCT, *, name: str = "Open Badge 3.
183
206
 
184
207
  Its ``claims`` describe the payload :func:`badge_to_sd_jwt_claims` emits:
185
208
  ``name`` / ``achievement`` / ``validFrom`` are always disclosed (``mandatory``),
186
- the recipient ``credentialSubject`` and ``validUntil`` are optional (selective
187
- disclosure). ``vct`` is the type identifier a wallet resolves this from — it
209
+ the recipient ``credentialSubject`` and ``validUntil`` are optional (``sd:
210
+ allowed``). The document is deliberately left as published: :func:`issue_badge_sd_jwt`
211
+ never makes ``validUntil`` disclosable even though the document permits it
212
+ (nothing that reads Type Metadata enforces ``sd``), because a byte change here
213
+ would break the ``vct#integrity`` pin of every badge already issued against
214
+ the old bytes. ``vct`` is the type identifier a wallet resolves this from — it
188
215
  must equal the issued badge's ``vct`` (openvc-core enforces that identity
189
216
  check). Serve :func:`type_metadata_document_bytes` of this document at *vct*
190
217
  and pin it with :func:`type_metadata_integrity`.
@@ -428,6 +455,60 @@ def badge_credential_configuration(
428
455
  return configuration
429
456
 
430
457
 
458
+ def _timestamp(value: Any, name: str) -> datetime:
459
+ """Parse a badge's validity timestamp (ISO 8601 with a UTC offset, as
460
+ ``datetime.fromisoformat`` reads it: stricter before Python 3.11), raising
461
+ ValueError with a short message whatever the value: it came from the
462
+ token and is never echoed back whole."""
463
+ if not isinstance(value, str):
464
+ raise ValueError("%s must be an ISO 8601 string, got a %s"
465
+ % (name, type(value).__name__))
466
+ if len(value) > _MAX_TIMESTAMP_CHARS:
467
+ raise ValueError("%s is not an ISO 8601 timestamp (%d characters)"
468
+ % (name, len(value)))
469
+ return _parse_iso(value, name)
470
+
471
+
472
+ def _expiry(claims: Mapping[str, Any], expires_in_s: Optional[float]) -> Optional[int]:
473
+ """The issuer JWT ``exp`` for a badge: its ``validUntil`` and/or now plus
474
+ *expires_in_s*, whichever is earlier; None when it has neither."""
475
+ candidates: list[int] = []
476
+ if "validUntil" in claims:
477
+ try:
478
+ candidates.append(int(_timestamp(claims["validUntil"], "validUntil").timestamp()))
479
+ except (ValueError, OverflowError) as exc:
480
+ raise EudiError("cannot derive the expiry of this badge: %s" % exc) from exc
481
+ if expires_in_s is not None:
482
+ if (isinstance(expires_in_s, bool) or not isinstance(expires_in_s, (int, float))
483
+ or (isinstance(expires_in_s, float) and not math.isfinite(expires_in_s))
484
+ or abs(expires_in_s) > _MAX_EXPIRES_IN_S):
485
+ raise EudiError(
486
+ "expires_in_s must be a number of seconds within %d of now, got %r"
487
+ % (_MAX_EXPIRES_IN_S, expires_in_s))
488
+ candidates.append(int(time.time() + expires_in_s))
489
+ return min(candidates) if candidates else None
490
+
491
+
492
+ def _not_before(claims: Mapping[str, Any]) -> Optional[int]:
493
+ """The issuer JWT ``nbf`` for a badge whose ``validFrom`` is still ahead:
494
+ the claim every SD-JWT VC verifier enforces, so a verifier that does not
495
+ know ``validFrom`` refuses it too. None for a badge valid from now or
496
+ earlier — setting it there would only turn a verifier's clock skew into a
497
+ refusal of a freshly issued badge."""
498
+ if "validFrom" not in claims:
499
+ return None
500
+ try:
501
+ starts = _timestamp(claims["validFrom"], "validFrom")
502
+ except ValueError as exc:
503
+ raise EudiError("cannot derive the start of this badge: %s" % exc) from exc
504
+ if starts <= datetime.now(timezone.utc):
505
+ return None
506
+ try:
507
+ return int(starts.timestamp())
508
+ except OverflowError as exc:
509
+ raise EudiError("cannot derive the start of this badge: %s" % exc) from exc
510
+
511
+
431
512
  def issue_badge_sd_jwt(
432
513
  credential: OpenBadgeCredential,
433
514
  *,
@@ -444,10 +525,22 @@ def issue_badge_sd_jwt(
444
525
  """Issue *credential* as an SD-JWT VC.
445
526
 
446
527
  Returns the compact SD-JWT (``<issuer-jwt>~<disclosure>~…``). Only claims in
447
- *disclosable* that are actually present are made selectively disclosable.
448
- Pass *holder_jwk* to bind the credential to a holder key (``cnf``) for a later
528
+ *disclosable* that are actually present are made selectively disclosable, and
529
+ ``name``, ``achievement``, ``validFrom`` and ``validUntil`` are not eligible:
530
+ naming one raises :class:`EudiError`, because a holder could withhold it
531
+ undetected (an expired badge would present as one with no expiry). Pass
532
+ *holder_jwk* to bind the credential to a holder key (``cnf``) for a later
449
533
  Key-Binding presentation. Ed25519, P-256 or P-384 keys only.
450
534
 
535
+ EXPIRY: the credential's ``validUntil`` is also written as the issuer JWT's
536
+ ``exp``, the claim every SD-JWT VC verifier enforces (a flat IETF payload's
537
+ ``validUntil`` is not read as a bound by the delegate). *expires_in_s* sets an
538
+ earlier expiry still; when both are present the earlier one wins, so neither
539
+ can lengthen the other. A credential whose ``validUntil`` is already past is
540
+ issued as it stands and fails verification as expired. A ``validFrom`` still
541
+ ahead is written as ``nbf`` for the same reason (not when it is now or past:
542
+ that would only turn a verifier's clock skew into a refusal).
543
+
451
544
  Pass *vct_integrity* (from :func:`type_metadata_integrity`) to embed a
452
545
  ``vct#integrity`` claim that pins the Type Metadata served at *vct*, so a
453
546
  wallet resolving it fails closed on any tampering — always disclosed,
@@ -492,6 +585,20 @@ def issue_badge_sd_jwt(
492
585
  "issue this copy from a credential without credentialStatus and pass "
493
586
  "status=build_token_status_reference(uri=…, index=…) with its own "
494
587
  "index in the status-list token.")
588
+ if isinstance(disclosable, (str, bytes)):
589
+ raise EudiError(
590
+ "disclosable must be an iterable of claim names, not a single "
591
+ "string (it would be read one character at a time)")
592
+ requested = list(disclosable)
593
+ refused = [name for name in requested if name in _NEVER_DISCLOSABLE]
594
+ if refused:
595
+ raise EudiError(
596
+ "%s cannot be selectively disclosable: a holder could withhold %s "
597
+ "undetected, so a verifier could not tell a badge without %s from "
598
+ "one that hides it. Only the recipient (credentialSubject) is "
599
+ "meant to be withheld."
600
+ % (", ".join(refused), "it" if len(refused) == 1 else "them",
601
+ "it" if len(refused) == 1 else "them"))
495
602
  status_claim = _status_claim(status) if status is not None else None
496
603
  _, _, _, SdJwtVcProofSuite = _require_openvc()
497
604
  signing_key = _signing_key(privkey_pem, kid or ("%s#key-1" % credential.issuer.id))
@@ -500,13 +607,19 @@ def issue_badge_sd_jwt(
500
607
  claims["vct#integrity"] = vct_integrity
501
608
  if status_claim is not None:
502
609
  claims["status"] = status_claim
610
+ expiry = _expiry(claims, expires_in_s)
611
+ if expiry is not None:
612
+ claims["exp"] = expiry
613
+ not_before = _not_before(claims)
614
+ if not_before is not None:
615
+ claims["nbf"] = not_before
503
616
  # "status" is never made selectively disclosable, whatever the caller passed
504
617
  # in *disclosable* — see the docstring.
505
- present = [name for name in disclosable if name in claims and name != "status"]
618
+ present = [name for name in requested if name in claims and name != "status"]
506
619
  try:
507
620
  return cast(str, SdJwtVcProofSuite().issue(
508
621
  claims, signing_key=signing_key, disclosable=present, vct=vct,
509
- holder_jwk=holder_jwk, expires_in_s=expires_in_s, x5c=x5c))
622
+ holder_jwk=holder_jwk, x5c=x5c))
510
623
  except EudiError:
511
624
  raise
512
625
  except Exception as exc:
@@ -536,6 +649,14 @@ def status_list_token_resolver(
536
649
  key, as ``openbadges-publish`` does for the Bitstring track). Writing your
537
650
  own is fine — it must return the verified token's claims dict.
538
651
  """
652
+ # The delegate validates this too (a whole number of seconds, 0 to 86400,
653
+ # from openvc-core 1.35.0; older releases clamped or took any number, so an
654
+ # infinite leeway made an expired list verify). Fail when the resolver is
655
+ # built, not on the first status check of some later badge.
656
+ if (isinstance(leeway_s, bool) or not isinstance(leeway_s, int)
657
+ or not 0 <= leeway_s <= _MAX_LEEWAY_S):
658
+ raise EudiError("leeway_s must be a whole number of seconds from 0 to %d, "
659
+ "got %r" % (_MAX_LEEWAY_S, leeway_s))
539
660
  fetch = download if download is not None else download_file
540
661
 
541
662
  def resolve(uri: str) -> dict[str, Any]:
@@ -610,6 +731,32 @@ def _check_sd_jwt_status(claims: Mapping[str, Any], *, require_status: bool,
610
731
  return result
611
732
 
612
733
 
734
+ def _check_validity_window(claims: Mapping[str, Any]) -> None:
735
+ """Enforce the badge's own ``validFrom`` / ``validUntil`` on verified claims.
736
+
737
+ The delegate reads them as a validity window only for a W3C VCDM payload
738
+ (one with a VCDM ``@context``); this track's payload is a flat IETF SD-JWT
739
+ VC, so on its own a badge past its ``validUntil`` verifies as long as its
740
+ ``exp`` (which :func:`issue_badge_sd_jwt` derives from it) is absent or
741
+ unexpired. Third-party badges, and badges issued before ``exp`` was
742
+ derived, rely on this. Same window and the same leeway as the other OB 3.0
743
+ verifiers (``CLOCK_SKEW_LEEWAY``); a value that is present but not a
744
+ timestamp with an offset fails closed.
745
+ """
746
+ now = datetime.now(timezone.utc)
747
+ for name in ("validFrom", "validUntil"):
748
+ if name not in claims:
749
+ continue
750
+ try:
751
+ when = _timestamp(claims[name], name)
752
+ except ValueError as exc:
753
+ raise EudiError("badge has a malformed %s: %s" % (name, exc)) from exc
754
+ if name == "validUntil" and when < now - CLOCK_SKEW_LEEWAY:
755
+ raise EudiError("badge has expired (validUntil %s)" % claims[name])
756
+ if name == "validFrom" and when > now + CLOCK_SKEW_LEEWAY:
757
+ raise EudiError("badge is not yet valid (validFrom %s)" % claims[name])
758
+
759
+
613
760
  def verify_badge_sd_jwt(
614
761
  token: str,
615
762
  *,
@@ -627,7 +774,20 @@ def verify_badge_sd_jwt(
627
774
  Returns openvc-core's ``VerifiedSdJwt`` (``.claims``, ``.issuer``, ``.vct``,
628
775
  ``.key_bound``, ``.confirmation``). Raises :class:`EudiError` on any failure.
629
776
  Pass *audience*/*nonce* (and ``require_key_binding=True``) to check a Key
630
- Binding JWT from a holder presentation.
777
+ Binding JWT from a holder presentation. *audience* also gates the issuer
778
+ JWT's own ``aud`` when it has one (openvc-core >=1.35.0, RFC 7519 §4.1.3): a
779
+ badge its issuer restricted to a relying party is refused by every other
780
+ verifier, and by this one when *audience* is unset or different.
781
+
782
+ The badge's validity window is enforced on both trust paths: a ``validUntil``
783
+ in the past (beyond the usual clock-skew leeway) or a ``validFrom`` in the
784
+ future raises :class:`EudiError`, as does a value that is not an ISO 8601
785
+ timestamp with an offset (as ``datetime.fromisoformat`` reads it, which is
786
+ stricter before Python 3.11), alongside the issuer JWT's ``exp``/``nbf`` that
787
+ the delegate checks. A holder can still hide a ``validUntil`` that a *third-party*
788
+ issuer made selectively disclosable and the holder withheld; that cannot be
789
+ detected, so badges from issuers that do so must be treated accordingly
790
+ (this library never issues one: see :func:`issue_badge_sd_jwt`).
631
791
 
632
792
  Revocation (IETF Token Status List) is checked when you supply
633
793
  *resolve_status_list_token* — the callable
@@ -704,6 +864,7 @@ def verify_badge_sd_jwt(
704
864
  raise
705
865
  except Exception as exc:
706
866
  raise EudiError("SD-JWT VC badge verification failed: %s" % exc) from exc
867
+ _check_validity_window(verified.claims)
707
868
  # The suite verifies the signature and the disclosures but has no status
708
869
  # stage (the delegate does status one layer up, in verify_credential — which
709
870
  # is the x5c path). Check it here so both paths honour the same seam.
@@ -722,7 +883,9 @@ def _issuer_jwt_has_x5c(token: str) -> bool:
722
883
  header_b64 = token.split("~", 1)[0].split(".", 1)[0]
723
884
  try:
724
885
  header = json.loads(base64url_decode(header_b64))
725
- except (ValueError, TypeError) as exc:
886
+ except (ValueError, TypeError, RecursionError) as exc:
887
+ # RecursionError: a hostile header nested deep enough to blow the JSON
888
+ # decoder's stack; it is not a ValueError and must not escape raw.
726
889
  raise EudiError(
727
890
  "could not parse the SD-JWT issuer JWT header: %s" % exc) from exc
728
891
  return bool(isinstance(header, dict) and header.get("x5c"))
@@ -751,6 +914,15 @@ def _verify_sd_jwt_x5c(token: str, x5c_trust_anchors: Any, *,
751
914
  from openvc import VerificationPolicy, verify_credential
752
915
  except ImportError as exc:
753
916
  raise EudiError(_INSTALL_HINT) from exc
917
+ # This is the SD-JWT VC verifier. openvc-core's verify_credential also takes
918
+ # a plain VC-JWT (no "~") and then ignores expected_vct and
919
+ # require_key_binding and hands back a different result type, so a JWT an
920
+ # anchored issuer signed would pass here as a badge; the pinned-key path
921
+ # refuses it for lacking the separator, and so must this one.
922
+ if not isinstance(token, str) or "~" not in token:
923
+ raise EudiError(
924
+ "not an SD-JWT VC (no ~ separator): verify_badge_sd_jwt does not "
925
+ "accept a plain JWT on the X.509 trust path either")
754
926
  # X.509 trust MUST anchor to the chain in the issuer JWT's x5c header.
755
927
  # openvc-core silently falls back to DID / issuer-URL key resolution when the
756
928
  # token carries no x5c, which would bypass x5c_trust_anchors entirely (a
@@ -774,4 +946,8 @@ def _verify_sd_jwt_x5c(token: str, x5c_trust_anchors: Any, *,
774
946
  raise EudiError(
775
947
  "SD-JWT VC badge verification against the X.509 trust anchors "
776
948
  "failed: %s" % exc) from exc
777
- return result.raw
949
+ verified = result.raw
950
+ if not isinstance(getattr(verified, "claims", None), Mapping):
951
+ raise EudiError("the delegate returned something other than an SD-JWT VC")
952
+ _check_validity_window(verified.claims)
953
+ return verified
@@ -88,7 +88,11 @@ MAX_LDP_DOCUMENT_BYTES = 256 * 1024
88
88
  #: let nested arrays, re-spelled policy terms or a derived proof that merges
89
89
  #: blank nodes through (GHSA-vrxg-57v4-85mc, GHSA-h9rq-wxcw-4rjc; see
90
90
  #: ob3/_context_validation.py), so the JSON they report could differ from
91
- #: what the issuer signed.
91
+ #: what the issuer signed. This is the security minimum of the delegated
92
+ #: verify path, deliberately below the ``>=1.35.0`` the extras declare (that
93
+ #: floor adds the SD-JWT VC issuer-JWT ``aud`` check, which this path does not
94
+ #: use): it refuses a hand-installed release that is unsafe here, not one that
95
+ #: is merely older than the tested pin.
92
96
  _OPENVC_MIN_VERSION = (1, 33, 2)
93
97
 
94
98
 
@@ -21,7 +21,7 @@
21
21
  License along with this library.
22
22
  """
23
23
 
24
- __version__ = '4.6.1'
24
+ __version__ = '4.6.2'
25
25
 
26
26
  import contextlib
27
27
  import hashlib
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 4.6.1
3
+ Version: 4.6.2
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
@@ -35,14 +35,14 @@ Requires-Dist: PyJWT[crypto]>=2.15.1
35
35
  Requires-Dist: cryptography>=45
36
36
  Requires-Dist: defusedxml>=0.7
37
37
  Provides-Extra: ldp
38
- Requires-Dist: pyld>=3.3.0; extra == "ldp"
38
+ Requires-Dist: pyld<4,>=3.3.0; extra == "ldp"
39
39
  Provides-Extra: eudi
40
- Requires-Dist: openvc-core<2,>=1.33.2; extra == "eudi"
40
+ Requires-Dist: openvc-core<2,>=1.35.0; extra == "eudi"
41
41
  Provides-Extra: ldp-sd
42
- Requires-Dist: openvc-core[data-integrity]<2,>=1.33.2; extra == "ldp-sd"
43
- Requires-Dist: pyld>=3.3.0; extra == "ldp-sd"
42
+ Requires-Dist: openvc-core[data-integrity]<2,>=1.35.0; extra == "ldp-sd"
43
+ Requires-Dist: pyld<4,>=3.3.0; extra == "ldp-sd"
44
44
  Provides-Extra: oid4vci
45
- Requires-Dist: openvc-core<2,>=1.33.2; extra == "oid4vci"
45
+ Requires-Dist: openvc-core<2,>=1.35.0; extra == "oid4vci"
46
46
  Provides-Extra: dev
47
47
  Requires-Dist: pytest>=8.0; extra == "dev"
48
48
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
@@ -17,14 +17,14 @@ jsonschema[format-nongpl]>=4.20
17
17
  sh<3,>=2.2.4
18
18
 
19
19
  [eudi]
20
- openvc-core<2,>=1.33.2
20
+ openvc-core<2,>=1.35.0
21
21
 
22
22
  [ldp]
23
- pyld>=3.3.0
23
+ pyld<4,>=3.3.0
24
24
 
25
25
  [ldp-sd]
26
- openvc-core[data-integrity]<2,>=1.33.2
27
- pyld>=3.3.0
26
+ openvc-core[data-integrity]<2,>=1.35.0
27
+ pyld<4,>=3.3.0
28
28
 
29
29
  [oid4vci]
30
- openvc-core<2,>=1.33.2
30
+ openvc-core<2,>=1.35.0
@@ -94,8 +94,14 @@ dependencies = [
94
94
  # refused. pyld before 3.3 also canonicalizes an integral float (3.0) as
95
95
  # "3.0E0" where 3.3 and jsonld.js give "3", so a proof made on one did not
96
96
  # verify on the other.
97
+ # Ceiling below 4: pyld 4.0 is unreleased and breaking, and its toRDF drops
98
+ # statements 3.x keeps (an IRI with two `#`, a literal with an invalid
99
+ # @language). Context validation is written against 3.x's absolute-IRI test and
100
+ # reads the JSON, not the dataset, so a statement pyld 4 drops would still read
101
+ # as present in the credential while the proof never signed it
102
+ # (GHSA-rp5q-966c-5592). Lift the ceiling only with that reworked and tested.
97
103
  ldp = [
98
- "pyld>=3.3.0",
104
+ "pyld>=3.3.0,<4",
99
105
  ]
100
106
  # EUDI track: issue/verify badges as SD-JWT VC (the EU wallet / ARF format) via
101
107
  # the generic openvc-core library. openvc-core is stable (1.x) with a published
@@ -110,13 +116,17 @@ ldp = [
110
116
  # only one of them came back key_bound=True although it was never bound to
111
117
  # this verifier. 1.30.0 refuses that half-configured check, and it also
112
118
  # bounds untrusted JSON nesting so a deep document fails with a typed error,
113
- # not a bare RecursionError. Per the rule above, the pin itself sits at
114
- # 1.33.2, the release the suite is tested against, which also carries
115
- # every openvc fix in between (GHSA-h9rq-wxcw-4rjc among them). Every extra that pulls openvc-core
119
+ # not a bare RecursionError. 1.35.0 starts enforcing the issuer JWT's own
120
+ # `aud` (RFC 7519 §4.1.3): before it, a third-party SD-JWT VC whose issuer
121
+ # restricted it to another relying party verified for everyone, and a status
122
+ # list token verified as unexpired under a hostile leeway_s. Per the rule
123
+ # above, the pin itself sits at 1.35.0, the release the suite is tested
124
+ # against, which also carries every openvc fix in between
125
+ # (GHSA-h9rq-wxcw-4rjc among them). Every extra that pulls openvc-core
116
126
  # shares this one floor; the CI `eudi` job checks that they agree and runs
117
127
  # the [eudi] and [ldp-sd] suites against it.
118
128
  eudi = [
119
- "openvc-core>=1.33.2,<2",
129
+ "openvc-core>=1.35.0,<2",
120
130
  ]
121
131
  # ecdsa-sd-2023 (selective disclosure) VERIFY for the OB 3.0 Data Integrity
122
132
  # track: credentials secured with the ecdsa-sd-2023 cryptosuite are verified by
@@ -131,13 +141,14 @@ eudi = [
131
141
  # of what was signed, so a member re-keyed to its full IRI or moved under
132
142
  # @included escaped them; 1.33.2 closes GHSA-h9rq-wxcw-4rjc (nested arrays,
133
143
  # re-spelled policy terms, a derived proof merging blank nodes). The pin sits
134
- # at 1.33.2, the shared tested floor
144
+ # at 1.35.0, the shared tested floor
135
145
  # described under [eudi]. OB3LdpVerifier also runs its own context validation
136
146
  # (with pyld) before it delegates, and openvc-core[data-integrity] admits
137
- # pyld 2.x, so this extra carries the same pyld floor as [ldp].
147
+ # pyld 2.x (and, unbounded above, pyld 4), so this extra carries the same pyld
148
+ # range as [ldp].
138
149
  ldp-sd = [
139
- "openvc-core[data-integrity]>=1.33.2,<2",
140
- "pyld>=3.3.0",
150
+ "openvc-core[data-integrity]>=1.35.0,<2",
151
+ "pyld>=3.3.0,<4",
141
152
  ]
142
153
  # OID4VCI issuance: hand a badge to a wallet over the pre-authorized code flow.
143
154
  # openbadgeslib owns the offers, the state (codes, tokens, nonces, grants) and
@@ -150,7 +161,7 @@ ldp-sd = [
150
161
  # require_key_attestation= on verify_credential_request_proofs, so an
151
162
  # issuer that advertises key_attestations_required can refuse a proof
152
163
  # without one before the signature and before the nonce is spent. Those are
153
- # the feature minimums; the pin is the shared 1.33.2 floor described under
164
+ # the feature minimums; the pin is the shared 1.35.0 floor described under
154
165
  # [eudi], which also applies 1.30.0's bound on untrusted JSON nesting to the
155
166
  # Credential Requests and key proofs this extra hands to openvc.
156
167
  #
@@ -159,7 +170,7 @@ ldp-sd = [
159
170
  # so an install that does not ask for [oid4vci] is never broken by its absence.
160
171
  # See openbadgeslib/oid4vci/ and wiki/Issuing-to-Wallets-with-OID4VCI.md.
161
172
  oid4vci = [
162
- "openvc-core>=1.33.2,<2",
173
+ "openvc-core>=1.35.0,<2",
163
174
  ]
164
175
  dev = [
165
176
  "pytest>=8.0",
@@ -495,3 +495,35 @@ class TestForIssuerDidKid:
495
495
  with pytest.raises(OB3VerificationError, match='different DID'):
496
496
  OB3Verifier.for_issuer_did(
497
497
  did, download=_doc_download(doc)).verify(token)
498
+
499
+
500
+ def test_a_did_document_nested_past_the_parser_is_a_typed_failure():
501
+ # 200 000 `[`: json.loads raises RecursionError, which is neither a
502
+ # ValueError nor caught as one, and used to escape the fetch raw.
503
+ from openbadgeslib.ob3.did import fetch_did_web_document
504
+ with pytest.raises(OB3VerificationError, match='malformed DID document'):
505
+ fetch_did_web_document('did:web:example.com',
506
+ download=lambda url: b'[' * 200000)
507
+
508
+
509
+ @pytest.mark.parametrize('depth', [65, 5000, 90000])
510
+ def test_a_did_document_nested_past_the_bound_is_refused(depth):
511
+ # Balanced and valid JSON. Depends on the interpreter whether json.loads
512
+ # takes it (3.14 parses ~100 000 levels, older ones stop near the recursion
513
+ # limit); either way it must come out as a typed failure, never as the
514
+ # RecursionError that repr() or json.dumps raise later on what it parsed.
515
+ from openbadgeslib.ob3.did import fetch_did_web_document
516
+ body = b'{"a":' * depth + b'1' + b'}' * depth
517
+ with pytest.raises(OB3VerificationError, match='nested|malformed DID document'):
518
+ fetch_did_web_document('did:web:example.com', download=lambda url: body)
519
+
520
+
521
+ def test_an_ordinary_did_document_is_far_below_the_depth_bound():
522
+ from openbadgeslib.ob3.did import (MAX_DID_DOCUMENT_DEPTH,
523
+ _nested_deeper_than)
524
+ doc = {'id': 'did:web:example.com',
525
+ 'verificationMethod': [{'id': '#k', 'publicKeyJwk': {'kty': 'OKP'}}],
526
+ 'service': [{'serviceEndpoint': {'uris': ['https://a', ['b']]}}]}
527
+ assert not _nested_deeper_than(doc, MAX_DID_DOCUMENT_DEPTH)
528
+ assert _nested_deeper_than(doc, 3)
529
+ assert not _nested_deeper_than([], 1) and not _nested_deeper_than('x', 0)