openbadgeslib 3.13.0__tar.gz → 4.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. openbadgeslib-4.0.0/CONTRIBUTING.md +93 -0
  2. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/Changelog.txt +366 -0
  3. openbadgeslib-4.0.0/GOVERNANCE.md +72 -0
  4. {openbadgeslib-3.13.0/openbadgeslib.egg-info → openbadgeslib-4.0.0}/PKG-INFO +19 -5
  5. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/README.md +12 -0
  6. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/__init__.py +51 -19
  7. openbadgeslib-4.0.0/openbadgeslib/_jsonmodel.py +86 -0
  8. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/__init__.py +39 -2
  9. openbadgeslib-4.0.0/openbadgeslib/_ob1_api.py +41 -0
  10. openbadgeslib-4.0.0/openbadgeslib/badge_model.py +199 -0
  11. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/baking.py +17 -9
  12. openbadgeslib-4.0.0/openbadgeslib/cli_common.py +63 -0
  13. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/config.ini.example +15 -0
  14. openbadgeslib-4.0.0/openbadgeslib/confparser.py +414 -0
  15. openbadgeslib-4.0.0/openbadgeslib/errors.py +221 -0
  16. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/issue.py +89 -47
  17. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/keys.py +8 -33
  18. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/mail.py +8 -2
  19. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/__init__.py +3 -7
  20. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/badge.py +11 -155
  21. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/verifier.py +3 -3
  22. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/models.py +7 -41
  23. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/signer.py +4 -4
  24. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/verifier.py +41 -4
  25. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/__init__.py +15 -0
  26. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/__init__.py +24 -2
  27. openbadgeslib-4.0.0/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +9 -0
  28. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/credential.py +15 -43
  29. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/did.py +40 -11
  30. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/eudi.py +108 -7
  31. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/ldp.py +94 -17
  32. openbadgeslib-4.0.0/openbadgeslib/ob3/publish.py +459 -0
  33. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/signer.py +41 -16
  34. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status.py +33 -17
  35. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status_list.py +5 -0
  36. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/verifier.py +47 -18
  37. openbadgeslib-4.0.0/openbadgeslib/openbadges_cli.py +98 -0
  38. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_init.py +26 -8
  39. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_keygenerator.py +20 -28
  40. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_publish.py +151 -327
  41. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_signer.py +31 -25
  42. openbadgeslib-4.0.0/openbadgeslib/openbadges_status.py +75 -0
  43. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/openbadges_verifier.py +122 -183
  44. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/util.py +159 -5
  45. openbadgeslib-4.0.0/openbadgeslib/verify.py +295 -0
  46. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0/openbadgeslib.egg-info}/PKG-INFO +19 -5
  47. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/SOURCES.txt +16 -3
  48. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/entry_points.txt +1 -0
  49. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/requires.txt +8 -4
  50. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/pyproject.toml +35 -5
  51. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conftest.py +9 -9
  52. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_baking.py +56 -0
  53. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_cli_json.py +2 -2
  54. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_cli_smoke.py +169 -15
  55. openbadgeslib-4.0.0/tests/test_confparser.py +492 -0
  56. openbadgeslib-4.0.0/tests/test_errors.py +92 -0
  57. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_issue.py +60 -0
  58. openbadgeslib-4.0.0/tests/test_jsonmodel.py +69 -0
  59. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_jws.py +43 -0
  60. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_keys.py +3 -10
  61. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_mail.py +13 -0
  62. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_badge_io.py +1 -1
  63. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_deprecation.py +11 -11
  64. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_signer.py +11 -12
  65. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob1_verifier.py +2 -2
  66. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_cli.py +19 -1
  67. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_verifier.py +66 -0
  68. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_conformance_schema.py +4 -2
  69. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_contexts.py +11 -0
  70. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_did_doc.py +50 -0
  71. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_endorsement.py +23 -2
  72. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_eudi_sd_jwt.py +63 -0
  73. openbadgeslib-4.0.0/tests/test_ob3_eudi_x5c.py +231 -0
  74. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp.py +102 -5
  75. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_ecdsa_sd.py +31 -4
  76. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_publish_cli.py +217 -3
  77. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_signer.py +59 -0
  78. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_status.py +135 -5
  79. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_status_registry.py +30 -0
  80. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_verifier.py +48 -2
  81. openbadgeslib-4.0.0/tests/test_openbadges_cli.py +103 -0
  82. openbadgeslib-4.0.0/tests/test_public_api.py +78 -0
  83. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_signer_batch.py +17 -1
  84. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_util.py +141 -4
  85. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_verifier_cli_coverage.py +80 -9
  86. openbadgeslib-4.0.0/tests/test_verify.py +113 -0
  87. openbadgeslib-3.13.0/openbadgeslib/badge.py +0 -21
  88. openbadgeslib-3.13.0/openbadgeslib/confparser.py +0 -240
  89. openbadgeslib-3.13.0/openbadgeslib/errors.py +0 -137
  90. openbadgeslib-3.13.0/openbadgeslib/signer.py +0 -13
  91. openbadgeslib-3.13.0/openbadgeslib/verifier.py +0 -13
  92. openbadgeslib-3.13.0/tests/test_confparser.py +0 -172
  93. openbadgeslib-3.13.0/tests/test_ob3_eudi_x5c.py +0 -120
  94. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/LICENSE.txt +0 -0
  95. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/MANIFEST.in +0 -0
  96. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/SECURITY.md +0 -0
  97. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/docs/README.md +0 -0
  98. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/exceptions.py +0 -0
  99. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/_jws/utils.py +0 -0
  100. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/logs.py +0 -0
  101. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob1/signer.py +0 -0
  102. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob2/__init__.py +0 -0
  103. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
  104. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
  105. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
  106. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
  107. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
  108. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
  109. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/ob3/status_registry.py +0 -0
  110. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib/py.typed +0 -0
  111. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  112. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/openbadgeslib.egg-info/top_level.txt +0 -0
  113. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/setup.cfg +0 -0
  114. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/config1.ini +0 -0
  115. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/Dockerfile.ob3 +0 -0
  116. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/README.md +0 -0
  117. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/conftest.py +0 -0
  118. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/docker-compose.yml +0 -0
  119. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/conformance/test_official_validators.py +0 -0
  120. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/README.md +0 -0
  121. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
  122. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
  123. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
  124. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
  125. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
  126. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
  127. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
  128. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
  129. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
  130. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/images/sample1.png +0 -0
  131. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/images/sample1.svg +0 -0
  132. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/images/userimage01.svg +0 -0
  133. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/ldp_helpers.py +0 -0
  134. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/logo Python Espan/314/203a.svg" +0 -0
  135. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/logo Python Espa/303/261a.svg" +0 -0
  136. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_docs.py +0 -0
  137. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_eddsa.py +0 -0
  138. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_examples.py +0 -0
  139. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_key_operation.py +0 -0
  140. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_error_branches.py +0 -0
  141. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_models.py +0 -0
  142. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob2_signer.py +0 -0
  143. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_credential.py +0 -0
  144. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_did.py +0 -0
  145. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_eudi_type_metadata.py +0 -0
  146. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_cli.py +0 -0
  147. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_errors.py +0 -0
  148. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_ldp_sign.py +0 -0
  149. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
  150. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_ob3_status_list.py +0 -0
  151. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_sign_ecc.pem +0 -0
  152. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_sign_rsa.pem +0 -0
  153. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_verify_ecc.pem +0 -0
  154. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/test_verify_rsa.pem +0 -0
  155. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/withoutxmlheader.svg +0 -0
  156. {openbadgeslib-3.13.0 → openbadgeslib-4.0.0}/tests/withxmlheader.svg +0 -0
@@ -0,0 +1,93 @@
1
+ # Contributing to openbadgeslib
2
+
3
+ Thanks for your interest in improving **openbadgeslib** — a library and CLI for
4
+ issuing and verifying OpenBadges 2.0 and 3.0 credentials. This page is the quick
5
+ entry point; the full developer guide lives in the wiki
6
+ ([Contributing](https://github.com/luisgf/openbadgeslib/wiki/Contributing),
7
+ [Releasing](https://github.com/luisgf/openbadgeslib/wiki/Releasing)).
8
+
9
+ ## Reporting bugs and requesting capabilities
10
+
11
+ - **Bugs:** open a [Bug report](https://github.com/luisgf/openbadgeslib/issues/new/choose).
12
+ Include the openbadgeslib version, your Python version, and a minimal
13
+ reproduction.
14
+ - **Capabilities / features:** open a
15
+ [Capability request](https://github.com/luisgf/openbadgeslib/issues/new/choose).
16
+ This is also the **demand signal** the roadmap runs on — several planned items
17
+ are deliberately *built when someone asks for them with a concrete use case*,
18
+ so a well-described request genuinely moves them forward.
19
+ - **Security vulnerabilities:** do **not** open a public issue. Follow
20
+ [SECURITY.md](SECURITY.md) (private GitHub advisory or email).
21
+
22
+ ## Development setup
23
+
24
+ Requires a supported Python (see below). Everything runs from an editable
25
+ virtual environment:
26
+
27
+ ```sh
28
+ git clone https://github.com/luisgf/openbadgeslib
29
+ cd openbadgeslib
30
+ python3 -m venv .venv && source .venv/bin/activate
31
+ pip install --upgrade pip
32
+ pip install -e ".[dev]" # add ,ldp and ,eudi to exercise those tracks
33
+ ```
34
+
35
+ ## The checks a pull request must pass
36
+
37
+ CI runs these on Python 3.10–3.14; run them locally before opening a PR (via
38
+ your `.venv`):
39
+
40
+ ```sh
41
+ flake8 openbadgeslib tests # style (max-line-length = 120, see setup.cfg)
42
+ mypy # types (config in pyproject.toml)
43
+ pytest # the suite; install [ldp]/[eudi] so their
44
+ # tests actually run instead of skipping
45
+ ```
46
+
47
+ The Data Integrity (LDP) tests need the `[ldp]` extra (pyld) and the EUDI
48
+ SD-JWT / X.509 tests need `[eudi]` — without them those tests silently
49
+ `importorskip`, so install the extras when touching those areas. Coverage is
50
+ enforced at a floor (currently 93%).
51
+
52
+ ## Commit messages
53
+
54
+ Commits follow a [Conventional Commits](https://www.conventionalcommits.org/)
55
+ style so the history is machine-readable and the changelog can be drafted from
56
+ it — CI's `gitlint` check enforces the title format. The shape is
57
+ `type(optional-scope)!: imperative summary` (≤ 80 chars), with the vocabulary:
58
+
59
+ | Type | For | Changelog |
60
+ | --- | --- | --- |
61
+ | `feat` / `fix` / `security` / `perf` | user-facing change | **yes** |
62
+ | `docs` / `test` / `refactor` / `chore` / `ci` / `build` / `style` | internal, no user-visible effect | no |
63
+ | `release` | the version-bump/changelog commit | no |
64
+
65
+ A `!` (or a `BREAKING CHANGE:` body trailer) marks a breaking change. A
66
+ user-facing `feat`/`fix`/`security`/`perf` should add its `Changelog.txt` entry
67
+ in the same commit (newest-first, under a `* vX.Y.Z - unreleased` header). See
68
+ the wiki [Contributing](https://github.com/luisgf/openbadgeslib/wiki/Contributing)
69
+ page for the full convention.
70
+
71
+ ## Supported Python versions
72
+
73
+ openbadgeslib supports **every CPython release that is not end-of-life**. The
74
+ policy is deliberately simple and predictable:
75
+
76
+ - A version is **dropped** — from `requires-python`, the classifiers, and the CI
77
+ matrix — at the next **major** release after it reaches end-of-life. For
78
+ example, Python **3.10 (EOL 2026-10)** is dropped in **4.0.0**; until then it
79
+ stays supported. Grouping drops into majors keeps breaking changes rare.
80
+ - A new CPython minor is **added** to the CI matrix once it has a stable
81
+ (non-pre) release, so downstreams can adopt it early.
82
+ - The current matrix is **3.10–3.14**, and `requires-python = ">=3.10"`.
83
+
84
+ Security fixes always target the latest release only (see
85
+ [SECURITY.md](SECURITY.md)).
86
+
87
+ ## Governance and licensing
88
+
89
+ Project governance, the release process, and the maintenance/continuity plan are
90
+ in [GOVERNANCE.md](GOVERNANCE.md). The library is licensed under the LGPLv3 and
91
+ the CLI entry points under BSD-2-Clause (see
92
+ [LICENSE.txt](LICENSE.txt)); by contributing you agree your contribution is
93
+ provided under those same terms.
@@ -4,6 +4,372 @@ OpenBadgesLib - Changelog
4
4
  Newest first. Dates are ISO 8601 (YYYY-MM-DD).
5
5
 
6
6
 
7
+ * v4.0.0 - 2026-07-22
8
+
9
+ Grouped breaking release (#170) — freeze the declared public contract and
10
+ unify the CLIs — shipped together with the Round-2 roadmap work: close the
11
+ revocation/trust gaps the deep-logic analysis surfaced, test the fail-closed
12
+ guards, and speed up batch issuance and verification. Breaking changes first,
13
+ then the rest of the cycle.
14
+
15
+ - fix(cli)!: unify every console tool on the 0/1/2 exit-code contract (#233).
16
+ The 19 `sys.exit(-1)` sites (which exited 255) now exit 1, and the verifier
17
+ applies the same status in human mode as under --json: 1 on any failure, 2
18
+ when the signature is valid but the issuer is not anchored (an OB2
19
+ badge-embedded key or a self-asserted did:key), and 0 only when the badge is
20
+ valid AND trusted. A partial batch sign or publish, and a stale/missing
21
+ artifact under `publish --check-live`, now exit 2 in human mode too (they
22
+ exited 1 before). BREAKING: a wrapper that keyed on 255, or that treated a
23
+ valid-but-untrusted badge's exit 0 as success, must be updated.
24
+ - feat(cli): add a unified `openbadges <command>` front-end and centralise the
25
+ shared flags (#234). A new `openbadgeslib/cli_common.py` provides argparse
26
+ parent parsers for -c/--config, -d/--debug, --json and -v/--version, which
27
+ all five build_parser()s now compose — so the flags are defined once with
28
+ consistent help. openbadges-publish gains -d/--debug (it had none) and
29
+ openbadges-init gains argparse (hence -v/--version, and -h now exits 0 via
30
+ argparse). The new `openbadges` entry point dispatches to init/keygen/sign/
31
+ verify/publish, each of which stays installed under its own openbadges-<name>
32
+ script; a command owns everything after its name, so `openbadges sign ...`
33
+ behaves exactly like `openbadges-signer ...`. Minor breaking: openbadges-init
34
+ with a bad/missing argument now exits 2 (argparse), not 1.
35
+ - feat(cli): add `openbadges status [ID]`, the sixth subcommand promised by
36
+ the unified front-end (#170). It is the read-only registry query that until
37
+ now was reachable only as `openbadges-publish -V 3 --list/--status`: with no
38
+ ID it tabulates every issued OB3 credential and its state, with one (a jti
39
+ or a recipient email) it prints that credential's full record including the
40
+ revocation reason. Auditing a credential is a read, so it no longer requires
41
+ typing a publish verb, nor `-V 3` — status lists are OB3-only by
42
+ construction. The query itself is not duplicated: the command and publish's
43
+ flags both call the shared `query_ob3()`, which now takes plain arguments
44
+ instead of a parsed argparse Namespace. `--list`/`--status` keep working
45
+ unchanged. Unlike the other five commands this one has no standalone
46
+ `openbadges-status` script — those aliases exist only because they predate
47
+ the front-end.
48
+ - refactor(ob1)!: consolidate the OB1 compatibility surface (#235). The OB1
49
+ legacy name table (11 names -> leaf module) is single-sourced in
50
+ openbadgeslib/_ob1_api.py instead of being duplicated between the top-level
51
+ package and openbadgeslib.ob1. The three top-level shim modules —
52
+ openbadgeslib.badge, openbadgeslib.signer and openbadgeslib.verifier — are
53
+ removed. OpenBadges 1.0 stays fully supported and reachable via
54
+ openbadgeslib.ob1.* (e.g. `from openbadgeslib.ob1 import Signer`) and the
55
+ unprefixed openbadgeslib.Signer / .Badge / … re-exports, both of which still
56
+ emit the steering DeprecationWarning. BREAKING: `from openbadgeslib.badge
57
+ import ...` (and .signer/.verifier) no longer resolve — repoint them to
58
+ `openbadgeslib.ob1.<module>`.
59
+ - refactor(keys)!: drop the last pycryptodome / python-ecdsa soft-import
60
+ (#170). Both libraries stopped being dependencies in the 3.7 port (#167),
61
+ but `key_to_pem` kept a soft-import shim that still accepted a *live*
62
+ pycryptodome RSA or python-ecdsa key object. That shim (`_legacy_key_to_pem`)
63
+ is removed, along with the `Crypto.*`/`ecdsa.*` mypy overrides: `key_to_pem`
64
+ now takes only `cryptography` key objects and PEM bytes/str, raising
65
+ `UnknownKeyType` otherwise. Key *files* are unaffected — they were already
66
+ plain PEM and round-trip unchanged. BREAKING: pass a `cryptography` key
67
+ object or PEM instead of a live pycryptodome/python-ecdsa object.
68
+
69
+ - fix(ob3): bundle and reference the OB v3p0 extensions context so the
70
+ `credentialSchema` type (`1EdTechJsonSchemaValidator2019`, emitted since
71
+ v3.6.0) resolves to a defined JSON-LD term. Without it the term was
72
+ undefined: the official 1EdTech OB3 validator rejected every issued
73
+ credential ("Undefined JSON-LD term") and canonicalization mapped the type
74
+ to a base-relative IRI, so the Data Integrity proof no longer verified. The
75
+ offline JSON-Schema gate did not catch this (it checks the string, not
76
+ JSON-LD term resolution). Issued credentials now carry a third `@context`
77
+ entry (the official schema pins only the first two and allows extras); the
78
+ context is pinned in the fail-closed allowlist and covered by the drift
79
+ watchdog (#239).
80
+ - security(config): close the bypass in the userinfo guard below: checking
81
+ `urlsplit(...).netloc` only catches a credential whose `@` lands inside the
82
+ authority, and a password holding a `/`, `?` or `#` pushes it out. Both
83
+ layers accepted `https://user:12345/x@host/` — which `did_web_from_url`
84
+ then turned into the issuer id `did:web:user%3A12345:x%40host`, carried by
85
+ every signed credential — and `https://user:secret/x@host/`, which surfaced
86
+ the password through urllib's own `Port could not be cast to integer value
87
+ as 'secret'` (a message the redaction added below could not reach, since
88
+ urllib raises it). Both layers now refuse an `@` or `%40` *anywhere* in a
89
+ URL-shaped value, and `did_web_from_url` wraps the port parse and quotes
90
+ nothing at all. Values with no authority — `[issuer] email`, local paths —
91
+ still accept an `@`. BREAKING (narrow): a `publish_url` whose path contains
92
+ an `@`, e.g. `https://host/@name/`, is now refused; once parsed it is
93
+ indistinguishable from one hiding a credential (#255).
94
+ - security(config): reject a `[issuer]` / `[badge_*]` URL that embeds
95
+ userinfo (`https://user:password@host/`) when the config is loaded. Those
96
+ URLs are public identifiers: `publish_url` is printed by every publish
97
+ command, `urljoin`ed into the ids written to the publicly served
98
+ organization.json / badge.json / key.json (urljoin preserves the
99
+ `user:pass@`), turned into the issuer's did:web, and used to build the
100
+ `credentialStatus` list URLs signed into every OB3 credential — so a
101
+ password there leaked into user-visible output, files served on the open
102
+ web and the badges delivered to recipients alike. Only one narrow path
103
+ guarded against it (did:web derivation, reached solely by
104
+ `[issuer] did = auto`), and that guard echoed the password back in its own
105
+ error for a non-https URL. The check now lives once in
106
+ `ConfParser.read_conf()` — after `${...}` interpolation, so a reference
107
+ cannot smuggle a credential past it — and every consumer (OB1/OB2/OB3
108
+ publishing, DID derivation, status lists) inherits it; the ConfigError
109
+ names the offending section and key but never the value. `did_web_from_url`
110
+ keeps its own guard as defence in depth, with the userinfo check moved
111
+ ahead of the scheme and host checks and their messages redacted. Behaviour
112
+ change: a config that protected a staging host with credentials in a
113
+ published URL no longer loads — drop the userinfo and use an allow-list or
114
+ a token the library never sees. `[smtp] username` / `password` are
115
+ untouched; they never leave the issuer's machine (#252).
116
+ - security(eudi): document and pin the X.509 / eIDAS trust boundary for
117
+ SD-JWT VC verification. All x5c chain validation is delegated to openvc-core
118
+ (path building, temporal validity, SAN<->iss binding); openbadgeslib only
119
+ guarantees the envelope (the x5c header is present, so the anchors are never
120
+ bypassed by a DID fallback — fail-closed). Crucially, certificate revocation
121
+ (CRL/OCSP) is NOT checked by either layer — a revoked-but-unexpired leaf
122
+ still verifies — so a deployment must apply revocation out of band. The
123
+ division of responsibility is spelled out on `verify_badge_sd_jwt` and the
124
+ Security Model wiki page; new boundary tests assert openvc-core rejects an
125
+ expired/not-yet-valid leaf and pin the revocation gap, and the eudi
126
+ floor/latest CI job now runs them so a drift in the delegate's behaviour
127
+ turns the build red (#236).
128
+ - security(ob3): with `--check-status`, verify the status list's OWN JWT-VC
129
+ signature by default and bind its issuer to the badge's, so a compromised
130
+ status host can no longer silently un-revoke a credential. The badge's
131
+ verification key is reused (publish signs the list with it), or the issuer
132
+ DID is resolved. `verify(check_status=True)` gains `verify_status_list=True`
133
+ by default; the verifier CLI gains `--no-verify-status-list` to opt out for
134
+ issuers that serve an unsigned list (#213).
135
+ - security(ob3): `OB3LdpVerifier` fails closed when it has no pinned key and
136
+ the credential's issuer is not a DID — the proof's `verificationMethod`
137
+ cannot be bound to a non-DID issuer, so resolving and trusting it would
138
+ prove internal consistency, not identity (#214).
139
+ - security(ob3): the endorsement validity window fails closed on a malformed
140
+ `validFrom`/`validUntil` (it previously swallowed the parse error and
141
+ accepted the endorsement, unlike every other window) (#218).
142
+ - fix(ob2, ob3): apply a 60-second clock-skew leeway to every validity window
143
+ (vc `validFrom`/`validUntil`, the status list window, OB2 `issuedOn`/
144
+ `expires`), so a slightly-fast issuer clock no longer false-rejects a
145
+ freshly issued credential (#217).
146
+ - perf(ob3, ob1): load the private-key object once per signer instead of
147
+ re-parsing the PEM on every signature (twice per badge on the JWT-VC path).
148
+ An RSA batch of 1000 badges drops from ~86 s to ~1.5 s; the OB1/OB2 JWS
149
+ path reuses the already-loaded key too (#215).
150
+ - perf(baking): build the baked PNG with a single `b''.join` instead of
151
+ quadratic `bytes` concatenation — ~200x faster on images written with many
152
+ small IDAT chunks (Photoshop/libpng), e.g. ~100 ms -> ~0.5 ms for 6.6 MB
153
+ (#216).
154
+ - test: add negative tests for the fail-closed rejection guards that had none
155
+ — OB2 hosted `startsWith`/`allowedOrigins` reject sides, the OB3 status
156
+ anti-gzip-bomb cap and negative index, the LDP verificationMethod binding,
157
+ and a future JWT `nbf` (#219).
158
+ - ci: the nightly conformance job now fails if a 1EdTech validator does not
159
+ come up or if no conformance test actually ran (all-skipped), instead of
160
+ reporting green, and files (or updates) an issue on a scheduled failure
161
+ (#220).
162
+ - ci: install the `[ldp-sd]` extra explicitly and turn the ecdsa-sd-2023
163
+ selective-disclosure test skip into a hard failure under CI, so an
164
+ openvc-core packaging change can no longer silently disable that suite
165
+ (#221).
166
+ - perf(ob3): add an opt-in `CachingDownloader` so verifying a batch of
167
+ credentials from one issuer fetches each did:web document and status list
168
+ once rather than per credential. `OB3LdpVerifier` also memoizes each
169
+ resolved `verificationMethod` per instance, and `OB3Verifier.verify` /
170
+ `check_status` accept a `download=` fetcher to thread it through (#228).
171
+ - perf(ob3): tag the bundled JSON-LD `@context`s `static` in the pyld
172
+ document loader, priming pyld's process-global resolved-context cache so
173
+ term definitions are not rebuilt on every canonicalization (~1.6x faster
174
+ Data Integrity sign/verify); per-call `extra_contexts` stay untagged so a
175
+ changing context is never served stale (#229).
176
+ - feat: extract a programmatic verification facade — `openbadgeslib.verify`
177
+ with `verify_badge(data, ob_version, *, pubkey_pem | resolve_did,
178
+ expected_recipient, check_status, …) -> VerifyResult` (fields `valid`,
179
+ `trusted`, and the decoded `credential`/`assertion`). It runs the whole
180
+ verification — token extraction, JWT-VC/LDP auto-detection, issuer-DID
181
+ resolution, trust classification, signature/status — doing no I/O; an
182
+ invalid badge is a result, not an exception. `openbadges-verifier` is now a
183
+ thin presenter over it (byte-identical output). Part of #222.
184
+ - feat: extract a programmatic publication facade —
185
+ `openbadgeslib.ob3.publish_ob3(conf, output, *, revoke | suspend |
186
+ unsuspend, reason, badge, check_live) -> PublishResult`. It runs the OB3
187
+ issuer-artefact regeneration (the did:web document, the signed Bitstring
188
+ Status List credentials, the opt-in SD-JWT VC Type Metadata) and the
189
+ status-registry revoke/suspend/unsuspend transaction — writing the
190
+ artefacts but doing no printing — and raises `PublishError` /
191
+ `CredentialNotFound` / `AmbiguousCredential` instead of exiting.
192
+ `openbadges-publish -V 3` is now a thin presenter over it (byte-identical
193
+ output). Completes #222.
194
+ - fix(issue): move the version-neutral badge config model (`Badge`,
195
+ `BadgeImgType`) out of the legacy `ob1` package into
196
+ `openbadgeslib.badge_model` (re-exported from `ob1.badge` for
197
+ compatibility). `create_from_conf` now reads the OpenBadges 1.0-only
198
+ `verify_key`/`criteria` keys optionally, so issuing an OB3 badge that omits
199
+ them no longer blows up with a raw `KeyError('verify_key')`; a missing
200
+ required key surfaces as `IssuanceError` through the `issue_*` entry points,
201
+ and a missing image raises a typed error instead of `print` + bare
202
+ `IOError` (#223).
203
+ - refactor(errors): consolidate the exception hierarchy so a single
204
+ `except LibOpenBadgesException` traps every library error. `IssuanceError`
205
+ and `DecompressionLimitExceeded` are re-anchored under the root (still
206
+ importable from `openbadgeslib.issue` / `openbadgeslib.baking`); the config
207
+ helpers' bare `ValueError`s become a new `ConfigError` and the signers'
208
+ unsupported-algorithm error becomes `UnsupportedAlgorithm` — both also
209
+ subclass `ValueError` so `except ValueError` callers keep working.
210
+ `_write_badge_and_log` reads the `[logs]` path inside its `try`, so a missing
211
+ `[logs]` section is reported instead of crashing with a traceback after the
212
+ badge is already on disk. Adds singular aliases (`KeyGenException`,
213
+ `SignerException`, `VerifierException`) for the plural base families (#224).
214
+ - refactor(ob2, ob3): deduplicate the shared JSON helpers (`_iso`,
215
+ `_parse_iso`, `_as_dict`, `_require`) into an internal
216
+ `openbadgeslib._jsonmodel`. The two copies had drifted: OB2's `_iso`
217
+ serialised a naive datetime as *local* time (`astimezone` on a naive value)
218
+ while OB3 anchored it to UTC, so the same timestamp emitted a different
219
+ instant in OB2 than in OB3; the single implementation keeps OB3's
220
+ naive=>UTC rule (#225).
221
+ - feat(confparser): resolve config through typed dataclasses with centralized
222
+ defaults. `load_config(path) -> ConfigParser` is the library entry point
223
+ (raises `ConfigError`); `read_config_or_exit` is now a thin CLI wrapper over
224
+ it (and the flagship example uses `load_config`). New `IssuerConfig` /
225
+ `BadgeSectionConfig` dataclasses (extending the `OB3StatusConfig` pattern)
226
+ resolve the `[issuer]` and badge sections and their defaults once (issuer id,
227
+ `criteria_narrative`->`criteria` fallback, hosted-assertions base), and a
228
+ single `DEFAULT_KEY_TYPE` + `resolve_key_type()` centralises the key_type
229
+ name->KeyType mapping the keygenerator used to inline (#225).
230
+ - security(ob3): warn loudly when `openbadges-publish -V 3` regenerates a
231
+ revocable badge's status lists without a `validUntil` bound (no
232
+ `status_validity_days` set). An unbounded list has no anti-replay freshness —
233
+ an authentic older copy with fewer revocations passes the window unchecked,
234
+ so a revoked credential could be silently resurrected. `PublishResult` gains
235
+ a `no_validity_bound` list (surfaced in the CLI's human output and `--json`),
236
+ and the config example documents the republication cadence (#227).
237
+ - security(ob3): SD-JWT VC (EUDI) issuance no longer silently drops a
238
+ credential's `credentialStatus`. Because SD-JWT VC status carriage/checking
239
+ is not wired yet (both verify paths hard-set `require_status=False`), an
240
+ SD-JWT VC badge is irrevocable; `issue_badge_sd_jwt` now rejects a credential
241
+ that carries a `credentialStatus` with a clear `EudiError` (before requiring
242
+ the [eudi] extra) rather than issue a badge the issuer believes is revocable.
243
+ `badge_to_sd_jwt_claims` also carries `credentialSubject.identifier` (the
244
+ hashed recipient identities), so a badge whose subject travelled via
245
+ `identifier` (no `id`) keeps a checkable recipient. The irrevocability is
246
+ documented prominently on the issue/verify docstrings (#226).
247
+ - security(util): pin the SSRF-validated IP on the HTTPS connection to defeat
248
+ DNS rebinding. `_assert_public_host` validated the resolved IPs, but
249
+ `opener.open` then re-resolved on its own, so a hostile DNS (TTL 0) could
250
+ answer public during the check and internal (169.254.169.254 / loopback /
251
+ RFC1918) at the real connect. A new `_PinnedHTTPSConnection` re-resolves,
252
+ re-validates and dials the validated IP in one step (keeping the hostname
253
+ for TLS SNI / certificate verification and the Host header) — for the initial
254
+ request and every redirect. Also adds a wall-clock download budget
255
+ (`MAX_DOWNLOAD_SECONDS`, 60 s), since the per-socket `timeout=30` bounds each
256
+ read but not a slow-drip response's total time (#231).
257
+ - ci: harden the release-critical workflows. SHA-pin the actions that run with
258
+ `id-token: write` — `pypa/gh-action-pypi-publish` (PyPI Trusted Publishing)
259
+ and the GitHub Pages `upload-pages-artifact` / `deploy-pages` — so a
260
+ compromised upstream tag cannot publish or deploy under our OIDC identity
261
+ (Dependabot already tracks github-actions and bumps the pins). Add a
262
+ least-privilege `permissions: contents: read` to commit-lint.yml. Add a
263
+ `windows-latest` CI leg plus a monkeypatched test of the `fcntl=None`
264
+ StatusRegistry lock fallback, so the "OS Independent" classifier is actually
265
+ exercised on the non-POSIX path. The leg immediately caught a real
266
+ portability bug it fixes: `openbadges-publish` now records `files_written`
267
+ with forward slashes (a Windows `\` would have broken the artefact URLs built
268
+ via `urljoin`); the POSIX file-mode assertions skip on Windows and the
269
+ ldp-sd hard-fail guard keys on an explicit `OPENBADGES_REQUIRE_LDP_SD` flag
270
+ (set by the main test job) so the core-only Windows leg skips it (#230).
271
+ - refactor: declare the stable public surface (freeze prep for v4). The root
272
+ package gains an explicit `__all__` (the "public contract"), re-exports
273
+ `KeyEd25519` (the recommended OB 3.0 / LDP key) alongside `KeyRSA`/`KeyECC`,
274
+ and exposes the `ob2`/`ob3`/`errors` subpackages and the
275
+ `issue_from_conf`/`verify_badge` facades by name. `openbadgeslib.ob3`
276
+ re-exports the `eudi` SD-JWT VC track (its heavy imports stay lazy).
277
+ `SignResult.credential`/`.assertion` and the `_Ob3Context` fields are typed
278
+ with `TYPE_CHECKING` forward refs instead of `Any` (zero runtime cost,
279
+ correct autocompletion / pdoc). Adds a "Stable API" wiki page documenting
280
+ the stable-vs-internal boundary (#232).
281
+ - build(deps): the [dev] extra depends on gitlint-core instead of the gitlint
282
+ meta-package, so it stops installing vulnerable pins. `gitlint` is an empty
283
+ shim for `gitlint-core[trusted-deps]`, and that extra hard-pins click==8.1.3
284
+ and sh==1.14.3 — PYSEC-2026-2132 (command injection in `click.edit()`, fixed
285
+ in 8.3.3) and CVE-2026-54552 (incomplete privilege drop in sh's `_uid`, fixed
286
+ in 2.2.4), which had been failing the weekly Audit job. Neither is reachable
287
+ here — gitlint never calls `click.edit()`, and it does not import `sh` at all
288
+ unless GITLINT_USE_SH_LIB=1, which this repo never sets — but upstream has
289
+ been dormant since 2023, so the pins would keep the audit red indefinitely.
290
+ gitlint-core declares the same dependencies loosely and provides the same
291
+ `gitlint` console script; the [dev] extra now floors `click>=8.3.3` and
292
+ `sh>=2.2.4` (keeping gitlint-core's non-Windows marker for sh) and caps both
293
+ below the next major. No pip-audit ignores were added: the advisories are
294
+ resolved by upgrading, not suppressed. Library users are unaffected — click
295
+ and sh were only ever development dependencies.
296
+ - build(deps)!: raise the PyJWT floor to >=2.13 and the openvc-core floors to
297
+ >=1.21. The OB3 JWT-VC verifier decodes an untrusted badge with a
298
+ signature-verifying `jwt.decode()`, so the PyJWT 2.12/2.13 advisory batch is
299
+ on a reachable path: RFC 7515 §4.1.11 `crit` validation (2.12.0,
300
+ CVE-2026-32597) and the b64=false unencoded-payload DoS (2.13.0,
301
+ CVE-2026-48525), plus JWK/alg-binding hardening; 2.13.0 is the minimal floor
302
+ carrying all of them. Installing [eudi]/[ldp-sd] already pulled a safe PyJWT
303
+ transitively via openvc-core (whose own floor is >=2.13), so this closes the
304
+ gap for a base, extras-free install. The native `_jws` engine is structurally
305
+ immune (it never calls `jwt.decode`). The [eudi]/[ldp-sd] openvc-core floors
306
+ move from >=1.18 / >=1.13 to >=1.21 per the pin's stated policy (floor at the
307
+ version the suite is tested against): 1.21 keeps in-floor the delegated
308
+ SD-JWT fixes openbadgeslib relies on — the key-binding replay fix that now
309
+ requires a nonce AND audience (1.19.3) and unknown-`crit` rejection on the
310
+ SD-JWT lane (1.20.4). No openbadgeslib code changed: openvc's new typed
311
+ ecdsa-sd errors (ProofValueMalformed/ProofMalformed) subclass ProofError, so
312
+ the existing `except ProofError` in ob3/ldp.py still catches them; the full
313
+ suite passes against openvc-core 1.21.0. BREAKING only for a caller that
314
+ pinned PyJWT <2.13 or openvc-core <1.21 alongside openbadgeslib.
315
+ - security(cli): `openbadges-init` creates config.ini owner-only (0600). It
316
+ is the file that goes on to hold the SMTP password and the paths to the
317
+ private keys, but the copy ran outside the umask block that hardens the
318
+ directories, so it inherited the caller's umask — 0644 on a default shell.
319
+ The 0700 parent contained it; this is the second lock, matching the one
320
+ the keygenerator puts on the private key.
321
+
322
+
323
+ * v3.14.0 - 2026-07-10
324
+
325
+ A security & correctness hardening pass (audit-bot). Highlights:
326
+
327
+ - security(eudi): reject an X.509-anchored SD-JWT verification whose issuer
328
+ JWT carries no `x5c` chain, so openvc-core cannot fall back to DID /
329
+ issuer-URL key resolution and bypass the EU trust anchors (#186).
330
+ - security(ob3): reject a Data Integrity credential carrying the unsigned VC
331
+ 1.1 aliases `expirationDate`/`issuanceDate` or a legacy `StatusList2021Entry`
332
+ status — the RDF-canonicalized proof cannot cover them, so a holder could
333
+ otherwise un-expire / un-revoke a signed credential (#205).
334
+ - security(ob2): reconcile a hosted badge's `expires` against the authoritative
335
+ fetched copy, so a stripped/extended local `expires` no longer defeats the
336
+ expiration check (#187).
337
+ - security(ob3): read `statusSize` from the BitstringStatusListEntry and fail
338
+ closed on a multi-bit entry, so a revoked/suspended multi-bit credential is
339
+ no longer misreported as valid (#188).
340
+ - security(mail): give `SMTP_SSL` a validating TLS context (check_hostname +
341
+ CERT_REQUIRED), so the badge-delivery client no longer exposes its SMTP AUTH
342
+ credentials to an on-path attacker (#203).
343
+ - fix(cli): `openbadges-verifier` exits non-zero in human mode when an OB1/OB2
344
+ badge is invalid (was exit 0), matching OB3 and `--json` (#189).
345
+ - fix(cli): `openbadges-signer` surfaces a partial-batch exit code in human
346
+ mode (#191); `-H/--hosted` is rejected outside `-V 2` (#206); a badge section
347
+ missing `private_key`/`public_key` yields a clean error (#210).
348
+ - fix(cli): `openbadges-init -h/--help` prints usage and exits 0 instead of
349
+ creating a directory named `--help` (#207).
350
+ - fix(issue): a baking failure surfaces as `IssuanceError` on the vc-jwt/OB2
351
+ paths (#190), and a missing `[issuer]`/config key does too (#208).
352
+ - fix(publish): isolate a per-badge key failure in status-list regeneration so
353
+ one bad badge no longer aborts the whole publish (#192).
354
+ - fix(ob3): a non-string Data Integrity `proof.created`/`expires` raises
355
+ `OB3VerificationError` instead of a raw `AttributeError` (#193).
356
+ - fix(ob2): accept an embedded `CryptographicKey` object in a Profile's
357
+ `publicKey` (#194).
358
+ - fix(confparser): reject a `status_size_bits` that is not a positive multiple
359
+ of 8 as a clean config error (#204).
360
+ - fix(keygenerator): switch on debug logging at the start of the run, so `-d`
361
+ also covers config reading and validation — not just the tail of key
362
+ generation, where `enable_debug_logging` used to sit.
363
+ - docs: announce the 4.0.0 deprecation window ahead of the release — a new
364
+ "Upgrading to 4.0" wiki page (dropped Python 3.10, the unified 0/1/2
365
+ exit-code contract, the removed pycryptodome/python-ecdsa compat) linked
366
+ from the sidebar, plus a README heads-up.
367
+ - feat(eudi): `issue_badge_sd_jwt` can embed an `x5c` certificate chain, so a
368
+ badge SD-JWT VC is issued anchored on X.509 and verified in one call with
369
+ `verify_badge_sd_jwt(..., x5c_trust_anchors=[…])` — the issue-side
370
+ counterpart of the verify-side x5c trust (#178), for eIDAS / EU Trusted
371
+ List anchoring. Raises the [eudi] openvc-core floor to >=1.18.
372
+
7
373
  * v3.13.0 - 2026-07-09
8
374
 
9
375
  - feat(eudi): **opt-in X.509 / EU Trusted List trust for received SD-JWT
@@ -0,0 +1,72 @@
1
+ # Governance
2
+
3
+ How **openbadgeslib** is maintained, released, and kept available. It is a small
4
+ project with a clear, lightweight process — written down here because third
5
+ parties already depend on it to *verify* credentials, so continuity matters.
6
+
7
+ ## Maintainership
8
+
9
+ openbadgeslib is actively maintained by **Luis González Fernández**
10
+ (`luisgf@luisgf.es`), the single active maintainer, who decides on scope,
11
+ roadmap, and releases. The project has a historical co-author, Jesús Cea Avión,
12
+ credited in the copyright headers and
13
+ [Authors, License and FAQ](https://github.com/luisgf/openbadgeslib/wiki/Authors-License-and-FAQ);
14
+ active maintenance today is solo.
15
+
16
+ Decision-making is lightweight: the roadmap is tracked as GitHub issues (label
17
+ `roadmap`, tracking issue
18
+ [#175](https://github.com/luisgf/openbadgeslib/issues/175)), and several items
19
+ are built on demand — a [capability request](CONTRIBUTING.md) with a concrete
20
+ use case is how they get prioritised.
21
+
22
+ ## Releasing and publishing
23
+
24
+ Releases are **CI-driven and credential-less to publish**:
25
+
26
+ - The version is single-sourced in `openbadgeslib/util.py`; a pushed `vX.Y.Z`
27
+ tag triggers the CI workflow, which runs the full test matrix (Python
28
+ 3.10–3.14) and then publishes to
29
+ [PyPI](https://pypi.org/project/openbadgeslib/).
30
+ - Publishing uses **PyPI Trusted Publishing (OIDC)** — there is **no long-lived
31
+ PyPI API token** stored anywhere. The publish job authenticates to PyPI from
32
+ GitHub Actions via a short-lived OIDC token, so there is no release secret to
33
+ leak, rotate, or lose.
34
+ - Each release is announced as a **GitHub Release** with curated notes drawn
35
+ from `Changelog.txt`; the Release is the canonical announcement channel.
36
+ - The **human fallback** (a maintainer cutting a release by hand) is fully
37
+ documented in the wiki
38
+ [Releasing](https://github.com/luisgf/openbadgeslib/wiki/Releasing) runbook —
39
+ it needs only repository write access plus the CI pipeline, no personal
40
+ credentials.
41
+
42
+ ## Bus factor and continuity
43
+
44
+ A single active maintainer is a real risk for a library others verify with. The
45
+ mitigations are deliberate:
46
+
47
+ 1. **No secret is a single point of failure.** Trusted Publishing means losing
48
+ access to a token cannot happen — there isn't one. Publishing is bound to the
49
+ GitHub repository's identity, not a person's stored key.
50
+ 2. **The whole release process is documented and automated** (the wiki Releasing
51
+ runbook and the `release` skill), so anyone with repository access can ship a
52
+ fix without tribal knowledge.
53
+ 3. **Handover requirements are explicit.** Taking over maintenance needs two
54
+ grants: **GitHub** repository admin, and **PyPI** owner/maintainer on the
55
+ [`openbadgeslib` project](https://pypi.org/project/openbadgeslib/) plus the
56
+ configured Trusted Publisher. A new maintainer with both can release
57
+ immediately.
58
+
59
+ **Recommended hardening (maintainer action):** add a second **PyPI project
60
+ owner** and a backup **GitHub admin** so a bus event does not strand the
61
+ project. Until then, note that everything published stays available on PyPI
62
+ indefinitely, and the LGPLv3 permits anyone to fork and continue the work.
63
+
64
+ If the project were to become unmaintained, the last release remains installable
65
+ from PyPI, and this document plus the wiki are enough for a fork to pick it up.
66
+
67
+ ## Reporting and contact
68
+
69
+ - Bugs and capability requests: GitHub issues (see [CONTRIBUTING.md](CONTRIBUTING.md)).
70
+ - Security vulnerabilities: **privately**, per [SECURITY.md](SECURITY.md) — never
71
+ a public issue.
72
+ - General contact: `luisgf@luisgf.es`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 3.13.0
3
+ Version: 4.0.0
4
4
  Summary: Production-ready Python library & CLI for Open Badges 3.0 (W3C Verifiable Credentials): issue, verify, and revoke JWT-VC credentials with Bitstring Status Lists and did:web — plus strict OB 2.0 and legacy OB 1.0.
5
5
  Author-email: Luis González Fernández <luisgf@luisgf.es>, Jesús Cea Avión <jcea@jcea.es>
6
6
  License: LGPLv3
@@ -30,22 +30,24 @@ Requires-Python: >=3.10
30
30
  Description-Content-Type: text/markdown
31
31
  License-File: LICENSE.txt
32
32
  Requires-Dist: pypng>=0.20220715.0
33
- Requires-Dist: PyJWT[crypto]>=2.8
33
+ Requires-Dist: PyJWT[crypto]>=2.13
34
34
  Requires-Dist: cryptography>=42
35
35
  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.13; extra == "eudi"
39
+ Requires-Dist: openvc-core<2,>=1.21; extra == "eudi"
40
40
  Provides-Extra: ldp-sd
41
- Requires-Dist: openvc-core[data-integrity]<2,>=1.13; extra == "ldp-sd"
41
+ Requires-Dist: openvc-core[data-integrity]<2,>=1.21; extra == "ldp-sd"
42
42
  Provides-Extra: dev
43
43
  Requires-Dist: pytest>=8.0; extra == "dev"
44
44
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
45
45
  Requires-Dist: flake8>=7.0; extra == "dev"
46
46
  Requires-Dist: pdoc>=14; extra == "dev"
47
47
  Requires-Dist: mypy>=1.8; extra == "dev"
48
- Requires-Dist: gitlint>=0.19; extra == "dev"
48
+ Requires-Dist: gitlint-core>=0.19; extra == "dev"
49
+ Requires-Dist: click<9,>=8.3.3; extra == "dev"
50
+ Requires-Dist: sh<3,>=2.2.4; sys_platform != "win32" and extra == "dev"
49
51
  Requires-Dist: jsonschema[format-nongpl]>=4.20; extra == "dev"
50
52
  Dynamic: license-file
51
53
 
@@ -64,6 +66,8 @@ with W3C Bitstring Status Lists and `did:web`. It also ships strict
64
66
  **OpenBadges 2.0** (JWS / hosted assertions) and a frozen **OpenBadges 1.0**
65
67
  legacy format, selected with `-V {1,2,3}` (default `3`).
66
68
 
69
+ > **Heads-up — 4.0.0 is coming.** Timed to Python 3.10's end of life (2026-10), the next major groups a few breaking changes: Python 3.10 is dropped (`requires-python >= 3.11`), the CLIs unify on one `0/1/2` exit-code contract, and the legacy pycryptodome/python-ecdsa compat shim is removed. OpenBadges 1.0 and the `openbadges-<command>` scripts stay. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
70
+
67
71
  ## Features
68
72
 
69
73
  - Sign badge images (SVG and PNG) as strict OB 2.0 JWS / hosted assertions (with a frozen OB 1.0 legacy format)
@@ -348,6 +352,16 @@ flake8 openbadgeslib tests # lint
348
352
  mypy # type check (config in pyproject.toml)
349
353
  ```
350
354
 
355
+ ## Contributing
356
+
357
+ Bug reports,
358
+ [capability requests](https://github.com/luisgf/openbadgeslib/issues/new/choose)
359
+ (the roadmap's demand signal), and pull requests are welcome — see
360
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) for the workflow and the supported-Python
361
+ policy, and [`GOVERNANCE.md`](GOVERNANCE.md) for maintenance and release
362
+ governance. Security issues go through [`SECURITY.md`](SECURITY.md), never a
363
+ public issue.
364
+
351
365
  ## Changelog
352
366
 
353
367
  See [`Changelog.txt`](Changelog.txt) for the full history, and the
@@ -13,6 +13,8 @@ with W3C Bitstring Status Lists and `did:web`. It also ships strict
13
13
  **OpenBadges 2.0** (JWS / hosted assertions) and a frozen **OpenBadges 1.0**
14
14
  legacy format, selected with `-V {1,2,3}` (default `3`).
15
15
 
16
+ > **Heads-up — 4.0.0 is coming.** Timed to Python 3.10's end of life (2026-10), the next major groups a few breaking changes: Python 3.10 is dropped (`requires-python >= 3.11`), the CLIs unify on one `0/1/2` exit-code contract, and the legacy pycryptodome/python-ecdsa compat shim is removed. OpenBadges 1.0 and the `openbadges-<command>` scripts stay. See **[Upgrading to 4.0](https://github.com/luisgf/openbadgeslib/wiki/Upgrading-to-4.0)** before pinning a major.
17
+
16
18
  ## Features
17
19
 
18
20
  - Sign badge images (SVG and PNG) as strict OB 2.0 JWS / hosted assertions (with a frozen OB 1.0 legacy format)
@@ -297,6 +299,16 @@ flake8 openbadgeslib tests # lint
297
299
  mypy # type check (config in pyproject.toml)
298
300
  ```
299
301
 
302
+ ## Contributing
303
+
304
+ Bug reports,
305
+ [capability requests](https://github.com/luisgf/openbadgeslib/issues/new/choose)
306
+ (the roadmap's demand signal), and pull requests are welcome — see
307
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) for the workflow and the supported-Python
308
+ policy, and [`GOVERNANCE.md`](GOVERNANCE.md) for maintenance and release
309
+ governance. Security issues go through [`SECURITY.md`](SECURITY.md), never a
310
+ public issue.
311
+
300
312
  ## Changelog
301
313
 
302
314
  See [`Changelog.txt`](Changelog.txt) for the full history, and the