openbadgeslib 4.2.0__tar.gz → 4.3.1__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 (167) hide show
  1. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/Changelog.txt +125 -0
  2. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/PKG-INFO +8 -6
  3. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/README.md +3 -3
  4. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/__init__.py +7 -3
  5. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jws/__init__.py +36 -11
  6. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/config.ini.example +45 -0
  7. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/confparser.py +187 -2
  8. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/errors.py +5 -21
  9. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/issue.py +187 -2
  10. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/keys.py +0 -2
  11. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/badge.py +24 -8
  12. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/verifier.py +14 -9
  13. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/__init__.py +4 -2
  14. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/credential.py +21 -0
  15. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/did.py +47 -0
  16. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/eudi.py +8 -2
  17. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/status_registry.py +91 -7
  18. openbadgeslib-4.3.1/openbadgeslib/oid4vci/__init__.py +110 -0
  19. openbadgeslib-4.3.1/openbadgeslib/oid4vci/codes.py +138 -0
  20. openbadgeslib-4.3.1/openbadgeslib/oid4vci/errors.py +128 -0
  21. openbadgeslib-4.3.1/openbadgeslib/oid4vci/formats.py +61 -0
  22. openbadgeslib-4.3.1/openbadgeslib/oid4vci/handler.py +348 -0
  23. openbadgeslib-4.3.1/openbadgeslib/oid4vci/memory_store.py +209 -0
  24. openbadgeslib-4.3.1/openbadgeslib/oid4vci/metadata.py +239 -0
  25. openbadgeslib-4.3.1/openbadgeslib/oid4vci/nonce.py +138 -0
  26. openbadgeslib-4.3.1/openbadgeslib/oid4vci/offer.py +260 -0
  27. openbadgeslib-4.3.1/openbadgeslib/oid4vci/reconcile.py +135 -0
  28. openbadgeslib-4.3.1/openbadgeslib/oid4vci/sqlite_store.py +630 -0
  29. openbadgeslib-4.3.1/openbadgeslib/oid4vci/store.py +318 -0
  30. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_keygenerator.py +26 -5
  31. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_publish.py +114 -1
  32. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_signer.py +57 -11
  33. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_verifier.py +62 -14
  34. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/util.py +8 -5
  35. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/PKG-INFO +8 -6
  36. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/SOURCES.txt +17 -0
  37. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/requires.txt +5 -2
  38. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/pyproject.toml +16 -2
  39. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_cli_json.py +30 -0
  40. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_cli_smoke.py +80 -0
  41. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_errors.py +28 -0
  42. openbadgeslib-4.3.1/tests/test_issue_credential.py +288 -0
  43. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_jws.py +79 -0
  44. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_key_operation.py +8 -0
  45. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_badge_io.py +32 -4
  46. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_verifier.py +33 -0
  47. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_sd_jwt.py +14 -0
  48. openbadgeslib-4.3.1/tests/test_oid4vci_config.py +265 -0
  49. openbadgeslib-4.3.1/tests/test_oid4vci_protocol.py +897 -0
  50. openbadgeslib-4.3.1/tests/test_oid4vci_reservations.py +295 -0
  51. openbadgeslib-4.3.1/tests/test_oid4vci_store.py +632 -0
  52. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_signer_batch.py +36 -0
  53. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verifier_cli_coverage.py +58 -0
  54. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/CONTRIBUTING.md +0 -0
  55. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/GOVERNANCE.md +0 -0
  56. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/LICENSE.txt +0 -0
  57. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/MANIFEST.in +0 -0
  58. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/SECURITY.md +0 -0
  59. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/docs/README.md +0 -0
  60. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jsonmodel.py +0 -0
  61. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jws/exceptions.py +0 -0
  62. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_jws/utils.py +0 -0
  63. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/_ob1_api.py +0 -0
  64. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/badge_model.py +0 -0
  65. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/baking.py +0 -0
  66. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/cli_common.py +0 -0
  67. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/logs.py +0 -0
  68. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/mail.py +0 -0
  69. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/__init__.py +0 -0
  70. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob1/signer.py +0 -0
  71. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/__init__.py +0 -0
  72. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/models.py +0 -0
  73. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/signer.py +0 -0
  74. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob2/verifier.py +0 -0
  75. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/__init__.py +0 -0
  76. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
  77. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
  78. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
  79. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
  80. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +0 -0
  81. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
  82. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
  83. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/ldp.py +0 -0
  84. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/publish.py +0 -0
  85. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/signer.py +0 -0
  86. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/status.py +0 -0
  87. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/status_list.py +0 -0
  88. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/ob3/verifier.py +0 -0
  89. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_cli.py +0 -0
  90. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_init.py +0 -0
  91. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/openbadges_status.py +0 -0
  92. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/py.typed +0 -0
  93. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib/verify.py +0 -0
  94. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  95. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/entry_points.txt +0 -0
  96. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/openbadgeslib.egg-info/top_level.txt +0 -0
  97. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/setup.cfg +0 -0
  98. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/config1.ini +0 -0
  99. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/Dockerfile.ob3 +0 -0
  100. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/README.md +0 -0
  101. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/conftest.py +0 -0
  102. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/docker-compose.yml +0 -0
  103. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conformance/test_official_validators.py +0 -0
  104. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/conftest.py +0 -0
  105. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/README.md +0 -0
  106. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
  107. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
  108. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/ob_schemas/refresh.sh +0 -0
  109. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/README.md +0 -0
  110. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
  111. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
  112. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
  113. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
  114. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
  115. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/images/sample1.png +0 -0
  116. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/images/sample1.svg +0 -0
  117. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/images/userimage01.svg +0 -0
  118. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/ldp_helpers.py +0 -0
  119. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/logo Python Espan/314/203a.svg" +0 -0
  120. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/logo Python Espa/303/261a.svg" +0 -0
  121. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_baking.py +0 -0
  122. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_confparser.py +0 -0
  123. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_docs.py +0 -0
  124. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_eddsa.py +0 -0
  125. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_examples.py +0 -0
  126. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_issue.py +0 -0
  127. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_jsonmodel.py +0 -0
  128. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_keys.py +0 -0
  129. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_mail.py +0 -0
  130. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_deprecation.py +0 -0
  131. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob1_signer.py +0 -0
  132. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_cli.py +0 -0
  133. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_error_branches.py +0 -0
  134. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_models.py +0 -0
  135. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_signer.py +0 -0
  136. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob2_verifier.py +0 -0
  137. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_conformance_schema.py +0 -0
  138. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_contexts.py +0 -0
  139. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_credential.py +0 -0
  140. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_did.py +0 -0
  141. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_did_doc.py +0 -0
  142. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_endorsement.py +0 -0
  143. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_oid4vci.py +0 -0
  144. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_type_metadata.py +0 -0
  145. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_eudi_x5c.py +0 -0
  146. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp.py +0 -0
  147. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_cli.py +0 -0
  148. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_ecdsa_sd.py +0 -0
  149. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_errors.py +0 -0
  150. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_ldp_sign.py +0 -0
  151. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_publish_cli.py +0 -0
  152. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_signer.py +0 -0
  153. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_signer_cli_ldp.py +0 -0
  154. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_status.py +0 -0
  155. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_status_list.py +0 -0
  156. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_status_registry.py +0 -0
  157. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_ob3_verifier.py +0 -0
  158. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_openbadges_cli.py +0 -0
  159. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_public_api.py +0 -0
  160. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_sign_ecc.pem +0 -0
  161. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_sign_rsa.pem +0 -0
  162. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_util.py +0 -0
  163. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verify.py +0 -0
  164. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verify_ecc.pem +0 -0
  165. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/test_verify_rsa.pem +0 -0
  166. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/withoutxmlheader.svg +0 -0
  167. {openbadgeslib-4.2.0 → openbadgeslib-4.3.1}/tests/withxmlheader.svg +0 -0
@@ -4,6 +4,131 @@ OpenBadgesLib - Changelog
4
4
  Newest first. Dates are ISO 8601 (YYYY-MM-DD).
5
5
 
6
6
 
7
+ * v4.3.1 - 2026-08-02
8
+
9
+ Audit hardening: security fixes in the JWS engine and keygenerator, CLI
10
+ contract repairs, and documentation that matches the code.
11
+
12
+ - security(jws): bind ES* algorithms to their NIST curves (ES256→P-256,
13
+ ES384→P-384, ES512→P-521) so `verify_block` no longer accepts an ES256
14
+ header with a P-384/P-521 key (#284).
15
+ - security(jws): reject a JWS whose header carries `crit` (RFC 7515 §4.1.11);
16
+ this library understands no critical extensions (#292).
17
+ - security(cli): write key PEMs with `fchmod` on the open descriptor (no
18
+ path-based chmod TOCTOU) and map write OSError to a clean CLI exit (#289).
19
+ - fix(cli): `openbadges-verifier --json` emits a single JSON object when
20
+ trusted-key/config resolution fails, not a bare `[!]` line (#285).
21
+ - fix(cli): reject OB3-only verifier flags (`--check-status`,
22
+ `--no-verify-status-list`, `--resolve-did`) on `-V 1`/`-V 2` instead of
23
+ silently ignoring them (#286).
24
+ - fix(cli): ensure the signer `-o` directory exists and is writable before
25
+ allocating OB3 status indices, so a bad output path does not orphan
26
+ registry slots (#282).
27
+ - fix(ob1): map assertion body decode failures to `AssertionFormatIncorrect`
28
+ (#281); map `OSError`/`TimeoutError` during status checks to a `VerifyInfo`
29
+ verdict (#283); allow offline verification when a trusted key is supplied
30
+ even if the badge-declared key URL is dark (#290).
31
+ - fix(jws): map `TypeError` from `prepare_key` in `sign()` to
32
+ `SignatureError` (live wrong-family key on the OB1 path) (#288).
33
+ - fix(ob3): map an unparseable SD-JWT signing key to `EudiError` (#287).
34
+ - fix(oid4vci): `SqliteOID4VCIStore.close()` closes every per-thread
35
+ connection (was only the calling thread), clearing the bulk of
36
+ `ResourceWarning: unclosed database` under `--cov` (#301).
37
+ - fix(keys): drop the unused module logger and never-raised KeyGen exception
38
+ classes; PEM loads already map to `*ReadError` (#291).
39
+ - docs: correct `--check-status` status-list signature wording, Upgrading-to-4.0
40
+ exit codes, PyJWT floor ≥2.13, openbadges-init argparse docs, six CLI entry
41
+ points, publish `--json` `no_validity_bound`, and CachingDownloader
42
+ `max_entries` memory wording (#293–#300).
43
+
44
+
45
+ * v4.3.0 - 2026-08-01
46
+
47
+ Issue a badge straight to a wallet, over OpenID4VCI.
48
+
49
+ - feat(oid4vci): issue badges to a wallet over OpenID for Verifiable
50
+ Credential Issuance 1.0, pre-authorized code flow. New `openbadgeslib.
51
+ oid4vci` package: Credential Offers, the issuer and RFC 8414 discovery
52
+ documents, and handlers for the token, nonce and credential endpoints. It
53
+ ships NO web server — the handlers return response bodies and the
54
+ integrator mounts the routes — which keeps the boundary openbadges-publish
55
+ already has (it writes files to serve, it does not serve them). The wallet
56
+ key-proof cryptography is delegated to openvc-core's `openvc.openid4vci`
57
+ behind a new `[oid4vci]` extra; the state that openvc refuses to hold —
58
+ codes, tokens, nonces, grants and their lifetimes — lives here, and with it
59
+ the "no wrong-issue" property. Formats: `jwt_vc_json` (revocable) and
60
+ `vc+sd-jwt` (the EUDI track, irrevocable per #226).
61
+ NOT HAIP-conformant and nothing built on it may claim to be: no DPoP, no
62
+ verified key attestations, no client authentication, no authorization code
63
+ flow. See wiki/Issuing-to-Wallets-with-OID4VCI.md and the new Security
64
+ Model section for the full list of what is and is not defended.
65
+ The state store is an interface with two backends: in-memory for a single
66
+ process, and SQLite for the threads and processes of one host. Single-use is
67
+ settled in the database rather than in Python — burning a nonce and claiming
68
+ an issuance are INSERTs under BEGIN IMMEDIATE — so exactly one caller wins
69
+ no matter how many workers race. Secrets are stored as digests, the file is
70
+ 0600 in a 0700 directory, and an unknown schema version is refused.
71
+ - feat(issue): `issue_credential_from_conf()` issues an OB 3.0 credential as
72
+ a TOKEN rather than a baked badge image, returning a `CredentialResult`.
73
+ The existing `issue_from_conf` always bakes a PNG/SVG, which is wasted work
74
+ (and the wrong deliverable) when the credential goes to a wallet, an LMS
75
+ API or an email — reaching the raw token previously meant baking an SVG and
76
+ extracting it again. Takes an optional `holder_jwk` to bind the credential
77
+ to a wallet key: for `jwt_vc_json` the subject id becomes that key's
78
+ `did:jwk` and the recipient moves to a salted `credentialSubject.
79
+ identifier` hash, so the badge stays attributable to a person without
80
+ publishing their address; for `vc+sd-jwt` the key goes into `cnf`.
81
+ - feat(ob3): `did_jwk_from_jwk()` derives a did:jwk from a public JWK, and
82
+ `IdentityObject.create()` builds a salted-hash identifier (the OB 3.0
83
+ mirror of the OB 2.0 constructor). Both refuse JWKs carrying private key
84
+ material.
85
+ - feat(ob3): the status registry gains reservations (schema version 2).
86
+ An OID4VCI offer reserves its revocation slot when the offer is built, not
87
+ when the wallet claims — deciding it at claim time would put the registry's
88
+ exclusive lock and full-file rewrite inside a concurrent HTTP path, on a
89
+ lock that is a no-op where fcntl is missing. Reserved entries show as
90
+ `reserved` in `--list`; `openbadges-publish -V 3 --reclaim-unclaimed`
91
+ frees the ones whose offer lapsed, refusing to touch any slot the registry
92
+ and the OID4VCI store do not both agree was never issued against. Schema
93
+ version 1 files load unchanged, and a registry with no reservations
94
+ serialises byte-identically to before.
95
+ - fix(oid4vci): the SQLite store's opportunistic garbage collection takes an
96
+ injectable clock. It ran on wall-clock time while every query takes an
97
+ explicit `now=`, so a write collected the very rows a caller had just
98
+ written against a frozen instant — which made the store's own test suite
99
+ pass on the day it was written and fail a week later on the calendar
100
+ rather than on a code change.
101
+ - security(verifier): `openbadges-verifier -V 1` no longer reports
102
+ `trusted: true` for a badge that FAILED verification (#278). The OB 1.0 path
103
+ set the trust flag before the valid/invalid branch, so a tampered badge
104
+ checked against an operator-supplied key (-k/--local) emitted
105
+ `{valid: false, trusted: true, status: SIGNATURE_ERROR}` — contradicting the
106
+ invariant the OB 2.0 and OB 3.0 paths already honoured (#258) and that a
107
+ consumer reading the JSON object, rather than the exit status, relies on.
108
+ Exit codes are unchanged: such a badge already exited 1.
109
+ - fix(cli): `openbadges-signer` maps a misconfigured badge section to a clean
110
+ `[!]` error instead of a raw traceback (#279). It built the badge model
111
+ without the mapper that turns a missing config key or an unreadable
112
+ key/image into an IssuanceError, so the most likely first-run mistake — a
113
+ section missing `private_key`, or keys not generated yet — ended in a
114
+ KeyError traceback in human mode (`--json` was already covered).
115
+ - fix(errors): the documented exception tree no longer misfiles
116
+ `AmbiguousCredential` (#280). `openbadgeslib.errors` carried a dead
117
+ `AmbiguousCredential(StatusError)` that was never raised, while the one
118
+ actually raised and exported is `ob3.publish.AmbiguousCredential`, a
119
+ `PublishError` — so `except StatusError` around a publish/revoke call, as
120
+ the tree in the docstring and the wiki instructed, did not catch it. The
121
+ dead class is gone and the `PublishError` family (with `CredentialNotFound`)
122
+ is documented in both places.
123
+ - build(deps): the openvc-core floors move to >=1.24 across `[eudi]`,
124
+ `[ldp-sd]` and `[oid4vci]`, which is the release the suite is actually
125
+ tested against — the rule the extras already documented, applied uniformly
126
+ (`[ldp-sd]` had been left behind at >=1.21). Substantively 1.23 still
127
+ suffices for the OpenID4VCI key proof and 1.22 for the Token Status List;
128
+ 1.24 is additive over both (`ResolveProofKey` is unchanged and not
129
+ deprecated), so an install already on 1.24 sees no behaviour change.
130
+
131
+
7
132
  * v4.2.0 - 2026-07-29
8
133
 
9
134
  The OpenID4VCI credential configuration an issuer advertises can now declare
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 4.2.0
3
+ Version: 4.3.1
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
@@ -36,9 +36,11 @@ Requires-Dist: defusedxml>=0.7
36
36
  Provides-Extra: ldp
37
37
  Requires-Dist: pyld>=2.0.4; extra == "ldp"
38
38
  Provides-Extra: eudi
39
- Requires-Dist: openvc-core<2,>=1.23; extra == "eudi"
39
+ Requires-Dist: openvc-core<2,>=1.24; extra == "eudi"
40
40
  Provides-Extra: ldp-sd
41
- Requires-Dist: openvc-core[data-integrity]<2,>=1.21; extra == "ldp-sd"
41
+ Requires-Dist: openvc-core[data-integrity]<2,>=1.24; extra == "ldp-sd"
42
+ Provides-Extra: oid4vci
43
+ Requires-Dist: openvc-core<2,>=1.24; extra == "oid4vci"
42
44
  Provides-Extra: dev
43
45
  Requires-Dist: pytest>=8.0; extra == "dev"
44
46
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
@@ -83,7 +85,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
83
85
  - Issue and verify badges as EUDI **SD-JWT VC** (the EU wallet / ARF format)
84
86
  with selective disclosure, via the optional `[eudi]` extra
85
87
  - did:web issuer identity: `did.json` generation and DID resolution
86
- - Five command-line tools included
88
+ - Six command-line entry points: the unified `openbadges` front-end plus five `openbadges-*` aliases
87
89
 
88
90
  ## Why openbadgeslib
89
91
 
@@ -119,7 +121,7 @@ additive EUDI SD-JWT VC wallet track.
119
121
  | Revocation / suspension | ✅ W3C Bitstring Status List | hosted check | `1EdTechRevocationList` | ad-hoc flag | — |
120
122
  | `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
121
123
  | Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
122
- | Form factor | library + 5 CLI tools | library + CLI | Django server | library | binding |
124
+ | Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
123
125
  | Typing / CI | `mypy --strict`, CI 3.10–3.14 | — | — | Pydantic | — |
124
126
  | License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
125
127
 
@@ -141,7 +143,7 @@ all three OB versions.
141
143
 
142
144
  - Python >= 3.10 (tested on 3.10–3.14)
143
145
  - [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
144
- - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.8
146
+ - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
145
147
  - [cryptography](https://pypi.org/project/cryptography/) >= 42
146
148
  - [defusedxml](https://pypi.org/project/defusedxml/) >= 0.7
147
149
 
@@ -30,7 +30,7 @@ legacy format, selected with `-V {1,2,3}` (default `3`).
30
30
  - Issue and verify badges as EUDI **SD-JWT VC** (the EU wallet / ARF format)
31
31
  with selective disclosure, via the optional `[eudi]` extra
32
32
  - did:web issuer identity: `did.json` generation and DID resolution
33
- - Five command-line tools included
33
+ - Six command-line entry points: the unified `openbadges` front-end plus five `openbadges-*` aliases
34
34
 
35
35
  ## Why openbadgeslib
36
36
 
@@ -66,7 +66,7 @@ additive EUDI SD-JWT VC wallet track.
66
66
  | Revocation / suspension | ✅ W3C Bitstring Status List | hosted check | `1EdTechRevocationList` | ad-hoc flag | — |
67
67
  | `did:web` (generate + resolve) | ✅ | — | — | — | resolve only |
68
68
  | Image baking (SVG + PNG) | ✅ | unbake only | ✅ | — | — |
69
- | Form factor | library + 5 CLI tools | library + CLI | Django server | library | binding |
69
+ | Form factor | library + CLI (`openbadges` + 5 aliases) | library + CLI | Django server | library | binding |
70
70
  | Typing / CI | `mypy --strict`, CI 3.10–3.14 | — | — | Pydantic | — |
71
71
  | License | LGPLv3 / BSD-2 | Apache-2.0 | AGPL-3.0 | MIT/LGPL | Apache-2.0 |
72
72
 
@@ -88,7 +88,7 @@ all three OB versions.
88
88
 
89
89
  - Python >= 3.10 (tested on 3.10–3.14)
90
90
  - [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
91
- - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.8
91
+ - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.13
92
92
  - [cryptography](https://pypi.org/project/cryptography/) >= 42
93
93
  - [defusedxml](https://pypi.org/project/defusedxml/) >= 0.7
94
94
 
@@ -66,7 +66,8 @@ from ._ob1_api import OB1_API as _OB1_API # noqa: F401
66
66
  # (hence cryptography) — inside the run-to-run noise — so the documentation of
67
67
  # the recommended modern API is worth more than it.
68
68
  from .issue import ( # noqa: F401
69
- IssuanceError, SignResult, issue_from_conf,
69
+ CredentialResult, IssuanceError, SignResult, issue_credential_from_conf,
70
+ issue_from_conf,
70
71
  )
71
72
  from .verify import verify_badge # noqa: F401
72
73
 
@@ -113,6 +114,9 @@ __all__ = [
113
114
  'OpenBadgeCredential', 'Achievement', 'Issuer',
114
115
  # Keys (KeyEd25519 is the recommended OB 3.0 / LDP key type).
115
116
  'KeyFactory', 'KeyRSA', 'KeyECC', 'KeyEd25519',
116
- # Programmatic issue / verify facades.
117
- 'issue_from_conf', 'verify_badge', 'IssuanceError', 'SignResult',
117
+ # Programmatic issue / verify facades. issue_credential_from_conf is the
118
+ # token-returning sibling of issue_from_conf, for delivering a credential
119
+ # over a digital channel (a wallet, an API) rather than as a badge image.
120
+ 'issue_from_conf', 'issue_credential_from_conf', 'verify_badge',
121
+ 'IssuanceError', 'SignResult', 'CredentialResult',
118
122
  ]
@@ -48,16 +48,23 @@ def _prepare_key_arg(key: Any) -> Any:
48
48
  return key_to_pem(key)
49
49
 
50
50
 
51
- # Each entry is (algorithm class, hash id). EdDSA's OKPAlgorithm takes no hash
52
- # argument — its hash id is None and _algo_for constructs it with no args.
51
+ # Each entry is (algorithm class, hash id, expected_curve). EdDSA's
52
+ # OKPAlgorithm takes no hash or curve — both are None and _algo_for constructs
53
+ # it with no args. RSA has no curve. EC algorithms bind the NIST curve that
54
+ # RFC 7518 §3.4 pairs with each ES* alg (ES256→P-256, ES384→P-384, ES512→P-521)
55
+ # so a P-521 key cannot "verify" an ES256 header and a P-256 key cannot sign
56
+ # ES512 — the same defence jwt.decode() gets from PyJWT's default registry
57
+ # (#284). Without expected_curve, ECAlgorithm accepts any EC key for any ES*
58
+ # hash, which is a lost defence-in-depth layer and an interop asymmetry with
59
+ # conformant verifiers.
53
60
  _ALGORITHMS = {
54
- 'RS256': (RSAAlgorithm, RSAAlgorithm.SHA256),
55
- 'RS384': (RSAAlgorithm, RSAAlgorithm.SHA384),
56
- 'RS512': (RSAAlgorithm, RSAAlgorithm.SHA512),
57
- 'ES256': (ECAlgorithm, ECAlgorithm.SHA256),
58
- 'ES384': (ECAlgorithm, ECAlgorithm.SHA384),
59
- 'ES512': (ECAlgorithm, ECAlgorithm.SHA512),
60
- 'EdDSA': (OKPAlgorithm, None),
61
+ 'RS256': (RSAAlgorithm, RSAAlgorithm.SHA256, None),
62
+ 'RS384': (RSAAlgorithm, RSAAlgorithm.SHA384, None),
63
+ 'RS512': (RSAAlgorithm, RSAAlgorithm.SHA512, None),
64
+ 'ES256': (ECAlgorithm, ECAlgorithm.SHA256, ec.SECP256R1),
65
+ 'ES384': (ECAlgorithm, ECAlgorithm.SHA384, ec.SECP384R1),
66
+ 'ES512': (ECAlgorithm, ECAlgorithm.SHA512, ec.SECP521R1),
67
+ 'EdDSA': (OKPAlgorithm, None, None),
61
68
  }
62
69
 
63
70
 
@@ -65,9 +72,11 @@ def _algo_for(alg_name: str) -> Any:
65
72
  entry = _ALGORITHMS.get(alg_name)
66
73
  if entry is None:
67
74
  raise RouteMissingError(f"Algorithm {alg_name!r} is not supported")
68
- cls, hash_id = entry
75
+ cls, hash_id, expected_curve = entry
69
76
  if hash_id is None:
70
77
  return cls()
78
+ if expected_curve is not None:
79
+ return cls(hash_id, expected_curve)
71
80
  return cls(hash_id)
72
81
 
73
82
 
@@ -104,7 +113,13 @@ def sign(header_dict: Dict[str, Any], payload_dict: Dict[str, Any], key: Any) ->
104
113
  try:
105
114
  prepared = algo.prepare_key(_prepare_key_arg(key))
106
115
  return cast(bytes, algo.sign(signing_input, prepared))
107
- except (InvalidKeyError, ValueError) as exc:
116
+ except (InvalidKeyError, ValueError, TypeError, AttributeError) as exc:
117
+ # prepare_key raises a bare TypeError ("Expecting a PEM-formatted key.")
118
+ # when handed a live cryptography object of the wrong family — the OB1
119
+ # path passes Badge.priv_key as a live object via _prepare_key_arg
120
+ # (#215). Mirror verify_block's except tuple so a mismatched key family
121
+ # surfaces as SignatureError, not a raw traceback out of the signer
122
+ # (#288).
108
123
  raise SignatureError(str(exc)) from exc
109
124
 
110
125
 
@@ -150,6 +165,16 @@ def verify_block(msg: Union[str, bytes], key: Optional[Any] = None) -> bool:
150
165
  "Algorithm %r in JWS header is not allowed for this key type"
151
166
  % alg_name)
152
167
 
168
+ # RFC 7515 §4.1.11: a verifier MUST reject a JWS whose 'crit' header names
169
+ # extensions it does not understand. This library understands none, so any
170
+ # present 'crit' (even an empty list — non-conformant but still a signal the
171
+ # issuer intended critical processing) fails closed rather than silently
172
+ # dropping the constraint while a conformant verifier would reject (#292).
173
+ if 'crit' in header:
174
+ raise SignatureError(
175
+ "JWS header requires unsupported 'crit' extensions: %r"
176
+ % (header.get('crit'),))
177
+
153
178
  signing_input = head_b64 + b'.' + payload_b64
154
179
  try:
155
180
  raw_sig = utils.from_base64(sig_b64)
@@ -52,6 +52,45 @@ revocationList = revoked.json
52
52
  ; imsglobal vct.
53
53
  ;sd_jwt_vct = ${publish_url}vct/openbadge
54
54
 
55
+ ; OpenID for Verifiable Credential Issuance (OID4VCI), issuer side. Optional and
56
+ ; entirely opt-in: without this section nothing changes, and a badge is only
57
+ ; offered over OID4VCI if its own 'oid4vci_formats' key says so.
58
+ ;
59
+ ; This library does NOT run the endpoints. It builds the offers, verifies the
60
+ ; wallet's key proof (via openvc-core, the [oid4vci] extra) and issues the
61
+ ; credential; you mount the routes in your own web framework at the URLs below.
62
+ ;
63
+ ; NOT a HAIP-conformant issuer: no DPoP, no key attestation checking, no client
64
+ ; authentication and no authorization code flow. Pre-authorized code only.
65
+ ;[oid4vci]
66
+ ; The Credential Issuer Identifier: the https base URL where you serve
67
+ ; /.well-known/openid-credential-issuer. Every wallet key proof binds its 'aud'
68
+ ; to this exact string, so it must match what you actually serve. Default:
69
+ ; [issuer] publish_url.
70
+ ;credential_issuer = https://openbadges.issuer.badge/issuer/
71
+ ; The three endpoints you mount. Default: derived from credential_issuer.
72
+ ;credential_endpoint = https://openbadges.issuer.badge/issuer/credential
73
+ ;nonce_endpoint = https://openbadges.issuer.badge/issuer/nonce
74
+ ;token_endpoint = https://openbadges.issuer.badge/issuer/token
75
+ ; Where the flow's short-lived state lives (pre-authorized codes, access tokens,
76
+ ; spent nonces). SQLite, single host: NOT on NFS/SMB, where its locking breaks.
77
+ ; The directory is created 0700 and holds recipient PII — do not back it up.
78
+ ; Default: ${paths:base}/oid4vci.sqlite3
79
+ ;store_path = ${paths:base}/oid4vci.sqlite3
80
+ ; Lifetimes, in seconds. The offer TTL bounds how long a QR code stays
81
+ ; redeemable; keep it short, it is a bearer secret.
82
+ ;offer_ttl_s = 600
83
+ ;nonce_ttl_s = 120
84
+ ;token_ttl_s = 300
85
+ ; How stale a wallet's key proof may be before it is refused.
86
+ ;proof_max_age_s = 300
87
+ ; Credentials one Credential Request may claim. Leave at 1 unless you have a
88
+ ; reason: a batch mints one credential per key proof off a single grant.
89
+ ;batch_size = 1
90
+ ; The out-of-band PIN, when an offer asks for one. 'numeric' or 'text'.
91
+ ;tx_code_length = 6
92
+ ;tx_code_input_mode = numeric
93
+
55
94
  ;Badge configuration sections.
56
95
  [badge_1]
57
96
  name = Badge 1
@@ -98,6 +137,12 @@ key_type = RSA
98
137
  ; lapses; pick a cadence you can sustain (e.g. 7 => weekly). Unset = no bound
99
138
  ; (offline-friendly, but publishes a loud warning).
100
139
  ;status_validity_days = 7
140
+ ; OID4VCI opt-in for this badge (comma-separated subset of: jwt_vc_json,
141
+ ; vc+sd-jwt). Unset means the badge is not published in the issuer metadata and
142
+ ; cannot be offered to a wallet. jwt_vc_json is revocable via the status lists
143
+ ; above; vc+sd-jwt is the EUDI track and is IRREVOCABLE, so it cannot be
144
+ ; combined with status_lists. vc+sd-jwt also needs key_type = ED25519 or ECC.
145
+ ;oid4vci_formats = jwt_vc_json
101
146
  ;mail = ${paths:base}/badge_1_mail.txt
102
147
 
103
148
  [badge_2]
@@ -23,7 +23,7 @@
23
23
 
24
24
  from configparser import ConfigParser, ExtendedInterpolation, Error as ConfigParserError
25
25
  from dataclasses import dataclass
26
- from typing import Dict, List, Optional, TYPE_CHECKING
26
+ from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
27
27
  import os
28
28
  import sys
29
29
  import logging
@@ -197,6 +197,189 @@ def ob3_status_config(conf: ConfigParser,
197
197
  validity_days=validity_days)
198
198
 
199
199
 
200
+ # ── OID4VCI (OpenID for Verifiable Credential Issuance) ──────────────────────
201
+ # Issuing to a wallet inverts the library's usual direction: instead of the
202
+ # issuer pushing a badge to a known recipient, the wallet claims it against an
203
+ # offer and proves possession of its own key. That needs a handful of
204
+ # deployment values the rest of the config has no reason to carry — the public
205
+ # identifier the wallet's key proofs bind to, and the lifetimes of the
206
+ # short-lived secrets the flow mints.
207
+ #
208
+ # The whole section is optional: without it, nothing else in the library
209
+ # changes behaviour.
210
+
211
+ #: How long a Credential Offer's pre-authorized code stays redeemable. Short by
212
+ #: default: it is a bearer secret that travels in a QR code or a link.
213
+ DEFAULT_OFFER_TTL_S = 600
214
+
215
+ #: Lifetime of a c_nonce handed out by the nonce endpoint.
216
+ DEFAULT_NONCE_TTL_S = 120
217
+
218
+ #: Lifetime of an access token minted at the token endpoint. It only has to
219
+ #: survive the wallet's immediate follow-up call to the credential endpoint.
220
+ DEFAULT_TOKEN_TTL_S = 300
221
+
222
+ #: How stale a wallet key proof's `iat` may be (openvc's max_age_s).
223
+ DEFAULT_PROOF_MAX_AGE_S = 300
224
+
225
+ #: Digits in a numeric tx_code, and the input modes OID4VCI 1.0 defines.
226
+ DEFAULT_TX_CODE_LENGTH = 6
227
+ TX_CODE_INPUT_MODES = ('numeric', 'text')
228
+
229
+
230
+ @dataclass
231
+ class OID4VCIConfig:
232
+ """Resolved [oid4vci] section: the issuer identifier, the endpoint URLs and
233
+ the lifetimes of the flow's short-lived secrets.
234
+
235
+ ``credential_issuer`` is load-bearing beyond being an identifier: it is the
236
+ value every wallet key proof must name in its ``aud``, so it has to match
237
+ byte for byte what is actually served at
238
+ ``<credential_issuer>/.well-known/openid-credential-issuer``."""
239
+
240
+ credential_issuer: str
241
+ credential_endpoint: str
242
+ nonce_endpoint: str
243
+ token_endpoint: str
244
+ store_path: str
245
+ offer_ttl_s: int = DEFAULT_OFFER_TTL_S
246
+ nonce_ttl_s: int = DEFAULT_NONCE_TTL_S
247
+ token_ttl_s: int = DEFAULT_TOKEN_TTL_S
248
+ proof_max_age_s: int = DEFAULT_PROOF_MAX_AGE_S
249
+ batch_size: int = 1
250
+ tx_code_length: int = DEFAULT_TX_CODE_LENGTH
251
+ tx_code_input_mode: str = 'numeric'
252
+
253
+
254
+ def _positive_int(conf: ConfigParser, section: str, key: str,
255
+ default: int) -> int:
256
+ """Read a positive integer config key, or its default when unset."""
257
+ raw = (conf[section].get(key) or '').strip() if section in conf else ''
258
+ if not raw:
259
+ return default
260
+ try:
261
+ value = int(raw)
262
+ except ValueError:
263
+ raise ConfigError("[%s] %s must be an integer, got %r"
264
+ % (section, key, raw)) from None
265
+ if value <= 0:
266
+ raise ConfigError("[%s] %s must be positive" % (section, key))
267
+ return value
268
+
269
+
270
+ def oid4vci_config(conf: ConfigParser) -> OID4VCIConfig:
271
+ """Resolve the optional [oid4vci] section.
272
+
273
+ Every key has a default, so a typical deployment writes none of them: the
274
+ issuer identifier falls back to ``[issuer] publish_url`` and the three
275
+ endpoints are derived from it. Raises :class:`ConfigError` when there is no
276
+ base to derive an issuer identifier from, when it is not https, or when a
277
+ lifetime is not a positive integer.
278
+
279
+ ``https`` is required rather than encouraged. A key proof's ``aud`` binds
280
+ the wallet's signature to this exact string; over plaintext http an attacker
281
+ who can rewrite the metadata response also chooses what the wallet signs
282
+ for, which makes the binding decorative.
283
+ """
284
+ from urllib.parse import urljoin
285
+
286
+ section = conf['oid4vci'] if 'oid4vci' in conf else None
287
+ base = (section.get('credential_issuer') if section else None) or \
288
+ conf['issuer'].get('publish_url') or ''
289
+ base = base.strip()
290
+ if not base:
291
+ raise ConfigError(
292
+ "[oid4vci] needs a credential_issuer (or an [issuer] publish_url "
293
+ "to derive it from): it is the identifier wallet key proofs bind "
294
+ "their 'aud' to")
295
+ if not base.startswith('https://'):
296
+ raise ConfigError(
297
+ "[oid4vci] credential_issuer must be an https URL, got %r — a "
298
+ "wallet key proof's audience binding is meaningless over plaintext"
299
+ % base)
300
+ # A trailing slash makes urljoin append rather than replace the last path
301
+ # segment; without it, an issuer at https://host/issuer would derive
302
+ # https://host/credential.
303
+ endpoint_base = base if base.endswith('/') else base + '/'
304
+
305
+ def endpoint(key: str, default_path: str) -> str:
306
+ explicit = (section.get(key) or '').strip() if section else ''
307
+ return explicit or urljoin(endpoint_base, default_path)
308
+
309
+ store_path = (section.get('store_path') if section else None) or \
310
+ os.path.join(conf['paths']['base'], 'oid4vci.sqlite3')
311
+
312
+ input_mode = ((section.get('tx_code_input_mode') if section else None)
313
+ or 'numeric').strip()
314
+ if input_mode not in TX_CODE_INPUT_MODES:
315
+ raise ConfigError("[oid4vci] tx_code_input_mode must be one of %s, "
316
+ "got %r" % (', '.join(TX_CODE_INPUT_MODES),
317
+ input_mode))
318
+
319
+ return OID4VCIConfig(
320
+ credential_issuer=base,
321
+ credential_endpoint=endpoint('credential_endpoint', 'credential'),
322
+ nonce_endpoint=endpoint('nonce_endpoint', 'nonce'),
323
+ token_endpoint=endpoint('token_endpoint', 'token'),
324
+ store_path=store_path,
325
+ offer_ttl_s=_positive_int(conf, 'oid4vci', 'offer_ttl_s',
326
+ DEFAULT_OFFER_TTL_S),
327
+ nonce_ttl_s=_positive_int(conf, 'oid4vci', 'nonce_ttl_s',
328
+ DEFAULT_NONCE_TTL_S),
329
+ token_ttl_s=_positive_int(conf, 'oid4vci', 'token_ttl_s',
330
+ DEFAULT_TOKEN_TTL_S),
331
+ proof_max_age_s=_positive_int(conf, 'oid4vci', 'proof_max_age_s',
332
+ DEFAULT_PROOF_MAX_AGE_S),
333
+ batch_size=_positive_int(conf, 'oid4vci', 'batch_size', 1),
334
+ tx_code_length=_positive_int(conf, 'oid4vci', 'tx_code_length',
335
+ DEFAULT_TX_CODE_LENGTH),
336
+ tx_code_input_mode=input_mode,
337
+ )
338
+
339
+
340
+ def oid4vci_formats(conf: ConfigParser, badge_section: str) -> Tuple[str, ...]:
341
+ """The OID4VCI credential formats a badge section opts into, in order.
342
+
343
+ Returns an empty tuple when the badge does not opt in (no
344
+ ``oid4vci_formats`` key), which callers treat as "this badge is not offered
345
+ over OID4VCI" — the same contract as :func:`ob3_status_config` returning
346
+ None. Opt-in rather than opt-out because listing a badge in the issuer
347
+ metadata publishes its existence and makes it claimable.
348
+
349
+ Raises :class:`ConfigError` for an unknown format, or for ``vc+sd-jwt`` on a
350
+ section with ``key_type = RSA``: SD-JWT VC has no RSA algorithm profile, so
351
+ that pairing could never issue and the operator should learn about it when
352
+ the config loads, not when a wallet is waiting.
353
+ """
354
+ from .keys import KeyType
355
+ from .oid4vci.formats import EC_ONLY_FORMATS, OID4VCI_FORMATS
356
+
357
+ raw = conf[badge_section].get('oid4vci_formats', '')
358
+ formats: List[str] = []
359
+ for piece in raw.split(','):
360
+ fmt = piece.strip()
361
+ if not fmt:
362
+ continue
363
+ if fmt not in OID4VCI_FORMATS:
364
+ raise ConfigError(
365
+ "[%s] oid4vci_formats: unknown format %r (choose from %s)"
366
+ % (badge_section, fmt, ', '.join(OID4VCI_FORMATS)))
367
+ if fmt not in formats:
368
+ formats.append(fmt)
369
+ if not formats:
370
+ return ()
371
+
372
+ key_type = resolve_key_type(conf[badge_section].get('key_type'))
373
+ if key_type is KeyType.RSA:
374
+ rsa_incapable = [f for f in formats if f in EC_ONLY_FORMATS]
375
+ if rsa_incapable:
376
+ raise ConfigError(
377
+ "[%s] oid4vci_formats includes %s, which cannot be signed with "
378
+ "an RSA key — use key_type = ED25519 or ECC, or drop that "
379
+ "format" % (badge_section, ', '.join(rsa_incapable)))
380
+ return tuple(formats)
381
+
382
+
200
383
  # ── centralized config defaults ──────────────────────────────────────────────
201
384
  # One inventory for the accepted-key defaults, so each lives in a single place
202
385
  # rather than being re-spelled at every read site. (The status-list defaults —
@@ -285,7 +468,9 @@ def badge_section_config(conf: ConfigParser,
285
468
  #: Sections whose values are the *public* identifiers of a deployment, checked
286
469
  #: by :func:`reject_url_userinfo`. [smtp] is deliberately excluded: its
287
470
  #: username/password are credentials that stay on the issuer's machine.
288
- _PUBLIC_URL_SECTIONS = ('issuer',)
471
+ #: [oid4vci] belongs here because its credential_issuer and endpoint URLs are
472
+ #: published in the issuer metadata and handed to every wallet that reads it.
473
+ _PUBLIC_URL_SECTIONS = ('issuer', 'oid4vci')
289
474
  _PUBLIC_URL_SECTION_PREFIX = 'badge_'
290
475
 
291
476
 
@@ -31,8 +31,7 @@ class LibOpenBadgesException(Exception):
31
31
 
32
32
  LibOpenBadgesException
33
33
  ├── KeyGenExceptions (alias: KeyGenException)
34
- │ ├── GenPrivateKeyError / GenPublicKeyError
35
- │ ├── PrivateKeySaveError / PublicKeySaveError
34
+ │ ├── GenPrivateKeyError
36
35
  │ ├── PrivateKeyReadError / PublicKeyReadError
37
36
  │ └── UnknownKeyType
38
37
  ├── SignerExceptions (alias: SignerException)
@@ -47,9 +46,10 @@ class LibOpenBadgesException(Exception):
47
46
  ├── IssuanceError
48
47
  ├── DecompressionLimitExceeded
49
48
  ├── StatusError (issuer-side revocation)
50
- │ └── StatusListFull / UnknownCredential / AmbiguousCredential
51
- │ / AlreadyRevoked / AlreadySuspended / NotSuspended
52
- │ / RegistryCorrupt
49
+ │ └── StatusListFull / UnknownCredential / AlreadyRevoked
50
+ │ / AlreadySuspended / NotSuspended / RegistryCorrupt
51
+ ├── PublishError (openbadgeslib.ob3.publish)
52
+ │ └── CredentialNotFound / AmbiguousCredential
53
53
  ├── OB2VerificationError (openbadgeslib.ob2.verifier)
54
54
  └── OB3VerificationError (openbadgeslib.ob3.verifier)
55
55
 
@@ -89,18 +89,6 @@ class GenPrivateKeyError(KeyGenExceptions):
89
89
  pass
90
90
 
91
91
 
92
- class GenPublicKeyError(KeyGenExceptions):
93
- pass
94
-
95
-
96
- class PrivateKeySaveError(KeyGenExceptions):
97
- pass
98
-
99
-
100
- class PublicKeySaveError(KeyGenExceptions):
101
- pass
102
-
103
-
104
92
  class PrivateKeyReadError(KeyGenExceptions):
105
93
  pass
106
94
 
@@ -201,10 +189,6 @@ class UnknownCredential(StatusError):
201
189
  pass
202
190
 
203
191
 
204
- class AmbiguousCredential(StatusError):
205
- pass
206
-
207
-
208
192
  class AlreadyRevoked(StatusError):
209
193
  pass
210
194