openbadgeslib 4.1.0__tar.gz → 4.2.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 (150) hide show
  1. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/Changelog.txt +23 -0
  2. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/PKG-INFO +1 -1
  3. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/eudi.py +68 -4
  4. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/util.py +1 -1
  5. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib.egg-info/PKG-INFO +1 -1
  6. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_eudi_oid4vci.py +71 -0
  7. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/CONTRIBUTING.md +0 -0
  8. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/GOVERNANCE.md +0 -0
  9. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/LICENSE.txt +0 -0
  10. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/MANIFEST.in +0 -0
  11. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/README.md +0 -0
  12. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/SECURITY.md +0 -0
  13. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/docs/README.md +0 -0
  14. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/__init__.py +0 -0
  15. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/_jsonmodel.py +0 -0
  16. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/_jws/__init__.py +0 -0
  17. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/_jws/exceptions.py +0 -0
  18. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/_jws/utils.py +0 -0
  19. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/_ob1_api.py +0 -0
  20. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/badge_model.py +0 -0
  21. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/baking.py +0 -0
  22. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/cli_common.py +0 -0
  23. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/config.ini.example +0 -0
  24. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/confparser.py +0 -0
  25. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/errors.py +0 -0
  26. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/issue.py +0 -0
  27. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/keys.py +0 -0
  28. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/logs.py +0 -0
  29. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/mail.py +0 -0
  30. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob1/__init__.py +0 -0
  31. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob1/badge.py +0 -0
  32. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob1/signer.py +0 -0
  33. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob1/verifier.py +0 -0
  34. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob2/__init__.py +0 -0
  35. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob2/models.py +0 -0
  36. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob2/signer.py +0 -0
  37. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob2/verifier.py +0 -0
  38. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/__init__.py +0 -0
  39. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/__init__.py +0 -0
  40. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
  41. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
  42. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
  43. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
  44. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/ob-v3p0-extensions.json +0 -0
  45. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
  46. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
  47. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/credential.py +0 -0
  48. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/did.py +0 -0
  49. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/ldp.py +0 -0
  50. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/publish.py +0 -0
  51. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/signer.py +0 -0
  52. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/status.py +0 -0
  53. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/status_list.py +0 -0
  54. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/status_registry.py +0 -0
  55. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/ob3/verifier.py +0 -0
  56. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/openbadges_cli.py +0 -0
  57. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/openbadges_init.py +0 -0
  58. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/openbadges_keygenerator.py +0 -0
  59. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/openbadges_publish.py +0 -0
  60. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/openbadges_signer.py +0 -0
  61. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/openbadges_status.py +0 -0
  62. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/openbadges_verifier.py +0 -0
  63. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/py.typed +0 -0
  64. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib/verify.py +0 -0
  65. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib.egg-info/SOURCES.txt +0 -0
  66. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  67. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
  68. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib.egg-info/requires.txt +0 -0
  69. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/openbadgeslib.egg-info/top_level.txt +0 -0
  70. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/pyproject.toml +0 -0
  71. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/setup.cfg +0 -0
  72. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/config1.ini +0 -0
  73. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/conformance/Dockerfile.ob3 +0 -0
  74. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/conformance/README.md +0 -0
  75. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/conformance/conftest.py +0 -0
  76. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/conformance/docker-compose.yml +0 -0
  77. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/conformance/test_official_validators.py +0 -0
  78. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/conftest.py +0 -0
  79. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/ob_schemas/README.md +0 -0
  80. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
  81. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
  82. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
  83. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
  84. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
  85. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
  86. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
  87. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
  88. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
  89. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/images/sample1.png +0 -0
  90. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/images/sample1.svg +0 -0
  91. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/images/userimage01.svg +0 -0
  92. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/ldp_helpers.py +0 -0
  93. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/logo Python Espan/314/203a.svg" +0 -0
  94. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/logo Python Espa/303/261a.svg" +0 -0
  95. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_baking.py +0 -0
  96. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_cli_json.py +0 -0
  97. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_cli_smoke.py +0 -0
  98. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_confparser.py +0 -0
  99. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_docs.py +0 -0
  100. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_eddsa.py +0 -0
  101. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_errors.py +0 -0
  102. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_examples.py +0 -0
  103. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_issue.py +0 -0
  104. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_jsonmodel.py +0 -0
  105. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_jws.py +0 -0
  106. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_key_operation.py +0 -0
  107. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_keys.py +0 -0
  108. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_mail.py +0 -0
  109. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob1_badge_io.py +0 -0
  110. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob1_deprecation.py +0 -0
  111. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob1_signer.py +0 -0
  112. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob1_verifier.py +0 -0
  113. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob2_cli.py +0 -0
  114. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob2_error_branches.py +0 -0
  115. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob2_models.py +0 -0
  116. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob2_signer.py +0 -0
  117. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob2_verifier.py +0 -0
  118. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_conformance_schema.py +0 -0
  119. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_contexts.py +0 -0
  120. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_credential.py +0 -0
  121. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_did.py +0 -0
  122. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_did_doc.py +0 -0
  123. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_endorsement.py +0 -0
  124. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
  125. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_eudi_type_metadata.py +0 -0
  126. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_eudi_x5c.py +0 -0
  127. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_ldp.py +0 -0
  128. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_ldp_cli.py +0 -0
  129. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_ldp_ecdsa_sd.py +0 -0
  130. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_ldp_errors.py +0 -0
  131. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_ldp_sign.py +0 -0
  132. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_publish_cli.py +0 -0
  133. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_signer.py +0 -0
  134. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
  135. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_status.py +0 -0
  136. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_status_list.py +0 -0
  137. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_status_registry.py +0 -0
  138. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_ob3_verifier.py +0 -0
  139. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_openbadges_cli.py +0 -0
  140. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_public_api.py +0 -0
  141. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_sign_ecc.pem +0 -0
  142. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_sign_rsa.pem +0 -0
  143. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_signer_batch.py +0 -0
  144. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_util.py +0 -0
  145. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_verifier_cli_coverage.py +0 -0
  146. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_verify.py +0 -0
  147. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_verify_ecc.pem +0 -0
  148. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/test_verify_rsa.pem +0 -0
  149. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/withoutxmlheader.svg +0 -0
  150. {openbadgeslib-4.1.0 → openbadgeslib-4.2.0}/tests/withxmlheader.svg +0 -0
@@ -4,6 +4,29 @@ OpenBadgesLib - Changelog
4
4
  Newest first. Dates are ISO 8601 (YYYY-MM-DD).
5
5
 
6
6
 
7
+ * v4.2.0 - 2026-07-29
8
+
9
+ The OpenID4VCI credential configuration an issuer advertises can now declare
10
+ the key attestation it requires from a wallet — opt-in, because the spec makes
11
+ the field's mere presence a demand.
12
+
13
+ - feat(eudi): `badge_credential_configuration()` takes
14
+ `key_attestations_required=`, so an issuer that requires a wallet key
15
+ attestation can advertise it — `{}` for "required, no additional
16
+ constraints", or the OpenID4VCI 1.0 D.2 resistance values it accepts under
17
+ `key_storage` / `user_authentication`. The default stays **omitted**, and
18
+ that default is normative rather than conservative: §12.2.4 says the
19
+ parameter "MUST NOT be present in the metadata" unless the issuer requires an
20
+ attestation, and an empty object is not a neutral placeholder but the
21
+ positive claim that one is needed — emitting it unasked would have every
22
+ badge issuer demand a hardware-backed attestation and lock out the software
23
+ wallets that cannot produce one. Callers who pass nothing get the same bytes
24
+ as before. The shape is validated (`EudiError` on an unknown key, an empty
25
+ array where the spec says non-empty, a bare string where an array belongs, or
26
+ a non-string level) and defensively copied; the resistance values are not
27
+ checked, since D.2 lets each ecosystem define its own (#276).
28
+
29
+
7
30
  * v4.1.0 - 2026-07-27
8
31
 
9
32
  The EUDI SD-JWT VC track becomes revocable (IETF Token Status List) and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 4.1.0
3
+ Version: 4.2.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
@@ -275,12 +275,61 @@ _CLAIM_DISPLAY_NAMES: dict[tuple[str, ...], str] = {
275
275
  ("credentialSubject",): "Recipient",
276
276
  }
277
277
 
278
+ # The only two constraints OpenID4VCI 1.0 Appendix D.2 defines inside
279
+ # ``key_attestations_required``. The *values* are deliberately extensible there
280
+ # ("ecosystems may define their own values"), so only the shape is checked here,
281
+ # never the level strings themselves.
282
+ _KEY_ATTESTATION_CONSTRAINTS = ("key_storage", "user_authentication")
283
+
284
+
285
+ def _key_attestations_required(value: Mapping[str, Any]) -> dict[str, Any]:
286
+ """Validate and copy a ``key_attestations_required`` object.
287
+
288
+ Empty is legal and meaningful — OpenID4VCI 1.0 §12.2.4: with both
289
+ ``key_storage`` and ``user_authentication`` absent the object indicates "a key
290
+ attestation is needed without additional constraints". What is rejected is the
291
+ shape a wallet cannot act on: an unknown key, an empty array where the spec
292
+ says non-empty, or a bare string where an array belongs — that last one is
293
+ silent otherwise, since ``list("iso_18045_moderate")`` is a list of letters.
294
+ """
295
+ if not isinstance(value, Mapping):
296
+ raise EudiError(
297
+ "key_attestations_required must be a mapping (use {} for 'required, "
298
+ "no additional constraints'), got %s" % type(value).__name__)
299
+ unknown = sorted(k for k in value if k not in _KEY_ATTESTATION_CONSTRAINTS)
300
+ if unknown:
301
+ raise EudiError(
302
+ "unknown key_attestations_required key(s) %s; OpenID4VCI 1.0 D.2 "
303
+ "defines only %s" % (", ".join(unknown),
304
+ ", ".join(_KEY_ATTESTATION_CONSTRAINTS)))
305
+ required: dict[str, Any] = {}
306
+ for name in _KEY_ATTESTATION_CONSTRAINTS:
307
+ if name not in value:
308
+ continue
309
+ levels = value[name]
310
+ if isinstance(levels, (str, bytes)) or not isinstance(levels, (list, tuple)):
311
+ raise EudiError(
312
+ "key_attestations_required.%s must be a list of attack potential "
313
+ "resistance values, got %s" % (name, type(levels).__name__))
314
+ if not levels:
315
+ raise EudiError(
316
+ "key_attestations_required.%s must be a non-empty array; omit the "
317
+ "key entirely to leave it unconstrained" % name)
318
+ for level in levels:
319
+ if not isinstance(level, str):
320
+ raise EudiError(
321
+ "key_attestations_required.%s values must be strings, got %s"
322
+ % (name, type(level).__name__))
323
+ required[name] = list(levels)
324
+ return required
325
+
278
326
 
279
327
  def badge_credential_configuration(
280
328
  *,
281
329
  vct: str = OB3_SD_JWT_VCT,
282
330
  signing_alg_values: Iterable[str] = ("ES256",),
283
331
  proof_signing_alg_values: Optional[Iterable[str]] = None,
332
+ key_attestations_required: Optional[Mapping[str, Any]] = None,
284
333
  cryptographic_binding_methods: Iterable[str] = ("jwk",),
285
334
  display: Optional[Iterable[Mapping[str, Any]]] = None,
286
335
  scope: Optional[str] = None,
@@ -293,7 +342,8 @@ def badge_credential_configuration(
293
342
  ``credential_signing_alg_values_supported``,
294
343
  ``cryptographic_binding_methods_supported``, ``proof_types_supported``
295
344
  (``jwt``, whose ``proof_signing_alg_values_supported`` defaults to
296
- *signing_alg_values*), ``claims`` and ``display``, plus ``scope`` when given.
345
+ *signing_alg_values*), ``claims`` and ``display``, plus ``scope`` and the
346
+ proof type's ``key_attestations_required`` when given.
297
347
  Put it in your metadata document under the ``credential_configuration_id`` you
298
348
  want wallets to request::
299
349
 
@@ -313,6 +363,18 @@ def badge_credential_configuration(
313
363
  passes its own name, logo and locales. *signing_alg_values* defaults to
314
364
  ``ES256``, the algorithm HAIP pins for EUDI wallets.
315
365
 
366
+ *key_attestations_required* is omitted by default, and that default is
367
+ normative rather than conservative: OpenID4VCI 1.0 §12.2.4 says the parameter
368
+ "MUST NOT be present in the metadata" unless the issuer requires a key
369
+ attestation, and an **empty** object is not a neutral placeholder — it is the
370
+ positive claim "a key attestation is needed without additional constraints".
371
+ Emitting it unasked would have every badge issuer demand a hardware-backed
372
+ attestation from every holder and lock out software wallets. An issuer that
373
+ does require one passes ``{}``, or ``{"key_storage": [...],
374
+ "user_authentication": [...]}`` with the Appendix D.2 resistance values it
375
+ accepts; the shape is validated (:class:`EudiError`), the values are not —
376
+ D.2 lets each ecosystem define its own.
377
+
316
378
  Two things this deliberately does NOT do. It does not build the enclosing
317
379
  ``/.well-known/openid-credential-issuer`` document — ``credential_issuer``,
318
380
  the endpoints, the authorization servers and per-tenant display are
@@ -329,6 +391,10 @@ def badge_credential_configuration(
329
391
  """
330
392
  algs = list(signing_alg_values)
331
393
  proof_algs = list(proof_signing_alg_values) if proof_signing_alg_values is not None else algs
394
+ proof_jwt: dict[str, Any] = {"proof_signing_alg_values_supported": proof_algs}
395
+ if key_attestations_required is not None:
396
+ proof_jwt["key_attestations_required"] = _key_attestations_required(
397
+ key_attestations_required)
332
398
  claims: list[dict[str, Any]] = []
333
399
  for claim in badge_type_metadata(vct)["claims"]:
334
400
  path = list(claim["path"])
@@ -345,9 +411,7 @@ def badge_credential_configuration(
345
411
  "vct": vct,
346
412
  "credential_signing_alg_values_supported": algs,
347
413
  "cryptographic_binding_methods_supported": list(cryptographic_binding_methods),
348
- "proof_types_supported": {
349
- "jwt": {"proof_signing_alg_values_supported": proof_algs},
350
- },
414
+ "proof_types_supported": {"jwt": proof_jwt},
351
415
  "claims": claims,
352
416
  "display": ([dict(d) for d in display] if display is not None
353
417
  else [{"name": "Open Badge 3.0", "locale": locale}]),
@@ -21,7 +21,7 @@
21
21
  License along with this library.
22
22
  """
23
23
 
24
- __version__ = '4.1.0'
24
+ __version__ = '4.2.0'
25
25
 
26
26
  import contextlib
27
27
  import hashlib
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 4.1.0
3
+ Version: 4.2.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
@@ -12,6 +12,7 @@ from openbadgeslib.ob3.eudi import (
12
12
  DEFAULT_DISCLOSABLE,
13
13
  OB3_SD_JWT_FORMAT,
14
14
  OB3_SD_JWT_VCT,
15
+ EudiError,
15
16
  badge_credential_configuration,
16
17
  badge_to_sd_jwt_claims,
17
18
  badge_type_metadata,
@@ -29,6 +30,8 @@ class TestCredentialConfigurationShape:
29
30
  "proof_signing_alg_values_supported"] == ["ES256"]
30
31
  assert cfg["display"] == [{"name": "Open Badge 3.0", "locale": "en"}]
31
32
  assert "scope" not in cfg # only when asked for
33
+ assert "key_attestations_required" not in cfg[
34
+ "proof_types_supported"]["jwt"] # ditto — see below
32
35
 
33
36
  def test_caller_overrides(self):
34
37
  cfg = badge_credential_configuration(
@@ -57,6 +60,74 @@ class TestCredentialConfigurationShape:
57
60
  json.loads(json.dumps(badge_credential_configuration()))
58
61
 
59
62
 
63
+ class TestKeyAttestationsRequired:
64
+ """An empty ``key_attestations_required`` is not a placeholder: OpenID4VCI
65
+ 1.0 §12.2.4 makes it the claim "a key attestation is needed without
66
+ additional constraints", and says the parameter MUST NOT be present at all
67
+ unless the issuer requires one. So the default is absence, and asking for it
68
+ is explicit."""
69
+
70
+ def _jwt(self, **kwargs):
71
+ return badge_credential_configuration(**kwargs)["proof_types_supported"]["jwt"]
72
+
73
+ def test_absent_by_default(self):
74
+ # Do not "fix" a strict wallet by emitting {} here: that would declare
75
+ # every badge issuer demands a key attestation, locking out the software
76
+ # wallets that cannot produce one.
77
+ assert "key_attestations_required" not in self._jwt()
78
+
79
+ def test_empty_object_means_required_without_constraints(self):
80
+ assert self._jwt(key_attestations_required={})[
81
+ "key_attestations_required"] == {}
82
+
83
+ def test_constraints_are_emitted(self):
84
+ jwt = self._jwt(key_attestations_required={
85
+ "key_storage": ["iso_18045_moderate"],
86
+ "user_authentication": ["iso_18045_moderate", "iso_18045_high"]})
87
+ assert jwt["key_attestations_required"] == {
88
+ "key_storage": ["iso_18045_moderate"],
89
+ "user_authentication": ["iso_18045_moderate", "iso_18045_high"]}
90
+ assert jwt["proof_signing_alg_values_supported"] == ["ES256"]
91
+
92
+ def test_is_copied_not_aliased(self):
93
+ mine = {"key_storage": ["iso_18045_moderate"]}
94
+ emitted = self._jwt(key_attestations_required=mine)[
95
+ "key_attestations_required"]
96
+ emitted["user_authentication"] = ["iso_18045_high"]
97
+ emitted["key_storage"].append("iso_18045_basic")
98
+ assert mine == {"key_storage": ["iso_18045_moderate"]}
99
+
100
+ def test_is_json_serializable(self):
101
+ import json
102
+ json.loads(json.dumps(badge_credential_configuration(
103
+ key_attestations_required={"key_storage": ["iso_18045_moderate"]})))
104
+
105
+ def test_unknown_key_is_rejected(self):
106
+ with pytest.raises(EudiError):
107
+ self._jwt(key_attestations_required={
108
+ "key_storag": ["iso_18045_moderate"]})
109
+
110
+ def test_empty_array_is_rejected(self):
111
+ # The spec says "non-empty array"; omitting the key is how you say
112
+ # "unconstrained".
113
+ with pytest.raises(EudiError):
114
+ self._jwt(key_attestations_required={"key_storage": []})
115
+
116
+ def test_bare_string_is_rejected(self):
117
+ # Left alone this would be emitted as a list of single letters.
118
+ with pytest.raises(EudiError):
119
+ self._jwt(key_attestations_required={
120
+ "user_authentication": "iso_18045_moderate"})
121
+
122
+ def test_non_string_level_is_rejected(self):
123
+ with pytest.raises(EudiError):
124
+ self._jwt(key_attestations_required={"key_storage": [4]})
125
+
126
+ def test_non_mapping_is_rejected(self):
127
+ with pytest.raises(EudiError):
128
+ self._jwt(key_attestations_required=["iso_18045_moderate"])
129
+
130
+
60
131
  class TestNoDrift:
61
132
  """The reason this function exists: a hand-written copy gets the vct and the
62
133
  claim set wrong, and a wallet then either rejects the credential or displays
File without changes
File without changes
File without changes
File without changes
File without changes