openbadgeslib 3.5.1__tar.gz → 3.7.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 (126) hide show
  1. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/Changelog.txt +91 -0
  2. {openbadgeslib-3.5.1/openbadgeslib.egg-info → openbadgeslib-3.7.0}/PKG-INFO +1 -5
  3. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/README.md +0 -2
  4. openbadgeslib-3.7.0/SECURITY.md +54 -0
  5. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/__init__.py +32 -11
  6. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/_jws/__init__.py +2 -2
  7. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/badge.py +9 -0
  8. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/baking.py +4 -4
  9. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/keys.py +120 -105
  10. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/mail.py +1 -1
  11. openbadgeslib-3.7.0/openbadgeslib/ob1/__init__.py +61 -0
  12. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob1/badge.py +13 -34
  13. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob1/verifier.py +2 -2
  14. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/models.py +14 -14
  15. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/verifier.py +3 -3
  16. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/__init__.py +10 -6
  17. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/credential.py +44 -16
  18. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/did.py +64 -11
  19. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/eudi.py +5 -5
  20. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/ldp.py +13 -13
  21. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/signer.py +4 -4
  22. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/status.py +2 -2
  23. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/status_list.py +3 -3
  24. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/status_registry.py +6 -6
  25. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/verifier.py +59 -11
  26. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_keygenerator.py +24 -7
  27. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_publish.py +235 -14
  28. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_signer.py +70 -35
  29. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_verifier.py +8 -1
  30. openbadgeslib-3.7.0/openbadgeslib/signer.py +13 -0
  31. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/util.py +48 -2
  32. openbadgeslib-3.7.0/openbadgeslib/verifier.py +13 -0
  33. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0/openbadgeslib.egg-info}/PKG-INFO +1 -5
  34. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/SOURCES.txt +2 -1
  35. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/requires.txt +0 -2
  36. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/pyproject.toml +8 -8
  37. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conftest.py +2 -2
  38. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_cli_smoke.py +33 -0
  39. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_key_operation.py +64 -69
  40. openbadgeslib-3.7.0/tests/test_ob1_deprecation.py +158 -0
  41. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_credential.py +8 -0
  42. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_did.py +105 -0
  43. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_ldp_sign.py +3 -0
  44. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_publish_cli.py +219 -0
  45. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_verifier.py +36 -0
  46. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_util.py +47 -1
  47. openbadgeslib-3.5.1/openbadgeslib/ob1/__init__.py +0 -36
  48. openbadgeslib-3.5.1/openbadgeslib/signer.py +0 -4
  49. openbadgeslib-3.5.1/openbadgeslib/verifier.py +0 -4
  50. openbadgeslib-3.5.1/tests/runtests.sh +0 -3
  51. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/LICENSE.txt +0 -0
  52. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/MANIFEST.in +0 -0
  53. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/docs/README.md +0 -0
  54. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/_jws/exceptions.py +0 -0
  55. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/_jws/utils.py +0 -0
  56. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/config.ini.example +0 -0
  57. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/confparser.py +0 -0
  58. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/errors.py +0 -0
  59. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/logs.py +0 -0
  60. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob1/signer.py +0 -0
  61. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/__init__.py +0 -0
  62. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/signer.py +0 -0
  63. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/__init__.py +0 -0
  64. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
  65. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
  66. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
  67. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
  68. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
  69. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
  70. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_init.py +0 -0
  71. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/py.typed +0 -0
  72. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  73. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
  74. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/top_level.txt +0 -0
  75. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/setup.cfg +0 -0
  76. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/config1.ini +0 -0
  77. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/Dockerfile.ob3 +0 -0
  78. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/README.md +0 -0
  79. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/conftest.py +0 -0
  80. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/docker-compose.yml +0 -0
  81. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/test_official_validators.py +0 -0
  82. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/README.md +0 -0
  83. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
  84. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
  85. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
  86. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
  87. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
  88. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
  89. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
  90. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
  91. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
  92. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/images/sample1.png +0 -0
  93. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/images/sample1.svg +0 -0
  94. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/images/userimage01.svg +0 -0
  95. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/ldp_helpers.py +0 -0
  96. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/logo Python Espan/314/203a.svg" +0 -0
  97. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/logo Python Espa/303/261a.svg" +0 -0
  98. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_cli_json.py +0 -0
  99. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_confparser.py +0 -0
  100. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_docs.py +0 -0
  101. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_eddsa.py +0 -0
  102. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_jws.py +0 -0
  103. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob1_badge_io.py +0 -0
  104. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob1_signer.py +0 -0
  105. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob1_verifier.py +0 -0
  106. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_cli.py +0 -0
  107. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_models.py +0 -0
  108. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_signer.py +0 -0
  109. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_verifier.py +0 -0
  110. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_conformance_schema.py +0 -0
  111. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_contexts.py +0 -0
  112. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_did_doc.py +0 -0
  113. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
  114. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_ldp.py +0 -0
  115. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_ldp_cli.py +0 -0
  116. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_signer.py +0 -0
  117. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
  118. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_status.py +0 -0
  119. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_status_list.py +0 -0
  120. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_status_registry.py +0 -0
  121. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_sign_ecc.pem +0 -0
  122. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_sign_rsa.pem +0 -0
  123. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_verify_ecc.pem +0 -0
  124. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_verify_rsa.pem +0 -0
  125. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/withoutxmlheader.svg +0 -0
  126. {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/withxmlheader.svg +0 -0
@@ -4,6 +4,97 @@ OpenBadgesLib - Changelog
4
4
  Newest first. Dates are ISO 8601 (YYYY-MM-DD).
5
5
 
6
6
 
7
+ * v3.7.0 - 2026-07-08
8
+
9
+ - security(deps): `keys.py` (and the OB1 key-loading path) are ported to
10
+ `cryptography`, and both `pycryptodome` and `python-ecdsa` are dropped as
11
+ dependencies. They only generated and parsed key material here — signing
12
+ always went through PyJWT+`cryptography` — all of which `cryptography`
13
+ (already a hard dependency via `PyJWT[crypto]`) covers. This removes
14
+ python-ecdsa's permanent pip-audit flag (CVE-2024-23342 "Minerva", wontfix
15
+ upstream), moves ECC key generation onto constant-time code, and shrinks
16
+ the install to a single crypto backend. PEM output is byte-compatible
17
+ (PKCS#1 `RSA PRIVATE KEY`, SEC1 `EC PRIVATE KEY`, SubjectPublicKeyInfo
18
+ public keys), so existing key files round-trip. `key_to_pem` still accepts
19
+ a live pycryptodome/python-ecdsa key object via a soft import, for a caller
20
+ predating the port. (#167)
21
+
22
+ - feat(cli): `--json` machine-readable output and a documented exit-code
23
+ contract for `openbadges-signer`, `openbadges-publish` and
24
+ `openbadges-keygenerator` (the verifier already had one), so issuance can
25
+ be automated without scraping human text. Each prints exactly one JSON
26
+ object to stdout — the signer `{ob_version, badge_file, jti, status_index,
27
+ proof_format}`, the keygenerator `{key_type, private_key, public_key}`,
28
+ publish `{did, files_written, status_operation, skipped}`, and
29
+ `--list`/`--status` the queried records — under a shared exit contract:
30
+ `0` success, `2` partial (some badges skipped), `1` any error (reported as
31
+ `{"error": "..."}`). Human output and its historical (inconsistent) exit
32
+ codes are untouched — unifying those is reserved for 4.0.0. `publish
33
+ --json` is OpenBadges 3.0 only. (#166)
34
+
35
+ - feat(ob3): JWT-VC verification now honours the token's `kid` header for a
36
+ multi-key did:web issuer. `OB3Verifier.for_issuer_did` fetches the DID
37
+ document once and selects the exact `verificationMethod` the kid names (an
38
+ absolute `did:web:…#badge_N` URL, or a bare `#badge_N` resolved against the
39
+ issuer DID), so issuers that rotate keys or publish one key per badge now
40
+ verify without pinning `-k`/`-l` — closing the gap where only the first
41
+ listed key worked. Fails closed when the kid names a method of a *different*
42
+ DID (the header is attacker-controlled) or one absent from the document; a
43
+ token with no kid still falls back to the first method. (#163)
44
+
45
+ - feat(ob1): OpenBadges 1.0 is now deprecated (removal scheduled for the
46
+ grouped 4.0.0 release, #170). Reaching the legacy import surface — the
47
+ `openbadgeslib.ob1` package, the top-level `openbadgeslib.badge` /
48
+ `openbadgeslib.signer` / `openbadgeslib.verifier` shims, and the
49
+ unprefixed `openbadgeslib.Signer` / `Verifier` / `Badge` / … re-exports —
50
+ now emits a `DeprecationWarning`, and the `-V 1` CLI paths (signer,
51
+ verifier, publish) print a `[!]` notice (suppressed under the verifier's
52
+ `--json`, so machine output stays clean). OB1 stays fully functional; a
53
+ bare `import openbadgeslib` and the OB2/OB3 APIs stay silent. OpenBadges
54
+ 2.0 is **not** affected — it remains fully supported. The lifecycle policy
55
+ and the rejected alternatives (a `[legacy]` extra, a separate PyPI
56
+ package) are recorded on the "Python API OB1" wiki page. (#159)
57
+
58
+ - feat(publish): `openbadges-publish -V 3` gains two read-only registry
59
+ query commands, closing the credential lifecycle from the CLI
60
+ (issue → revoke → audit). `--list [-b badge]` tabulates every issued
61
+ credential — jti, recipient, issue date and state (active / REVOKED /
62
+ SUSPENDED); `--status <jti|email>` prints the full record of the matching
63
+ credential(s), including the revocation or suspension date and reason.
64
+ Both read only the private status registries, so neither needs an output
65
+ directory (`-o`). (#157)
66
+
67
+ - refactor(cli): `openbadges-keygenerator` no longer accepts the decorative
68
+ `-V/--ob-version` flag. Key material never depended on the OpenBadges
69
+ version (the algorithm comes from the badge's `key_type`), so the flag only
70
+ changed a log line. Internal tidy-ups alongside it: a dead `if args.badge`
71
+ guard in the signer (`-b` is required), the redundant `tests/runtests.sh`
72
+ wrapper, and an inaccurate conftest comment. (#156)
73
+
74
+
75
+ * v3.6.0 - 2026-07-08
76
+
77
+ - feat(ob3): the verifier now exposes the raw, already-validated VC document
78
+ on the returned credential (`credential.raw`) — on both the JWT-VC and the
79
+ Data Integrity path — so an integrator can read spec fields the dataclass
80
+ does not map (alignment, results, multiple evidence, endorsements, …)
81
+ without re-parsing the token. It is None for an in-memory credential built
82
+ to issue, and excluded from equality/repr so it never affects comparisons.
83
+
84
+ - feat(ob3): issued OB 3.0 credentials now carry a `credentialSchema` entry
85
+ (`1EdTechJsonSchemaValidator2019`) pointing at 1EdTech's published
86
+ AchievementCredential JSON Schema, so validators and wallets can
87
+ self-check them. The offline conformance gate proves our issued
88
+ credentials satisfy that exact schema.
89
+
90
+ - refactor(typing): the library now type-checks clean under `mypy --strict`
91
+ (73 bare `dict` annotations parameterised as `dict[str, Any]`, 20 Any
92
+ returns narrowed with `typing.cast`), and `strict = true` is enabled in
93
+ the mypy config so CI enforces it. This makes the README's long-standing
94
+ `mypy --strict` claim true and improves the shipped `py.typed` annotations
95
+ for downstream type-checkers. Annotations only — no runtime change.
96
+
97
+
7
98
  * v3.5.1 - 2026-07-07
8
99
 
9
100
  - fix(ob3): a naive datetime passed as the issuance/expiration date is now
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbadgeslib
3
- Version: 3.5.1
3
+ Version: 3.7.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
@@ -29,8 +29,6 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGP
29
29
  Requires-Python: >=3.10
30
30
  Description-Content-Type: text/markdown
31
31
  License-File: LICENSE.txt
32
- Requires-Dist: pycryptodome>=3.20
33
- Requires-Dist: ecdsa>=0.19
34
32
  Requires-Dist: pypng>=0.20220715.0
35
33
  Requires-Dist: PyJWT[crypto]>=2.8
36
34
  Requires-Dist: cryptography>=42
@@ -136,8 +134,6 @@ all three OB versions.
136
134
  ## Requirements
137
135
 
138
136
  - Python >= 3.10 (tested on 3.10–3.14)
139
- - [pycryptodome](https://pypi.org/project/pycryptodome/) >= 3.20
140
- - [ecdsa](https://pypi.org/project/ecdsa/) >= 0.19
141
137
  - [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
142
138
  - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.8
143
139
  - [cryptography](https://pypi.org/project/cryptography/) >= 42
@@ -85,8 +85,6 @@ all three OB versions.
85
85
  ## Requirements
86
86
 
87
87
  - Python >= 3.10 (tested on 3.10–3.14)
88
- - [pycryptodome](https://pypi.org/project/pycryptodome/) >= 3.20
89
- - [ecdsa](https://pypi.org/project/ecdsa/) >= 0.19
90
88
  - [pypng](https://pypi.org/project/pypng/) >= 0.20220715.0
91
89
  - [PyJWT[crypto]](https://pypi.org/project/PyJWT/) >= 2.8
92
90
  - [cryptography](https://pypi.org/project/cryptography/) >= 42
@@ -0,0 +1,54 @@
1
+ # Security Policy
2
+
3
+ `openbadgeslib` issues and verifies cryptographically-signed credentials, so we
4
+ take reports seriously and aim to respond quickly.
5
+
6
+ ## Supported versions
7
+
8
+ Security fixes land in a new release on the latest minor line and are published
9
+ to [PyPI](https://pypi.org/project/openbadgeslib/). Always run the latest
10
+ release; older versions are not patched in place.
11
+
12
+ | Version | Supported |
13
+ | -------------- | ----------------------------- |
14
+ | Latest release | ✅ security fixes |
15
+ | Older releases | ❌ upgrade to the latest |
16
+
17
+ ## Reporting a vulnerability
18
+
19
+ **Please do not open a public issue for a suspected vulnerability.** Report it
20
+ privately instead:
21
+
22
+ - **Preferred:** GitHub private vulnerability reporting — the **“Report a
23
+ vulnerability”** button under this repository’s
24
+ [Security tab](https://github.com/luisgf/openbadgeslib/security/advisories).
25
+ - **Email:** `luisgf@luisgf.es` (you may encrypt to the same address’s public
26
+ key if you have it).
27
+
28
+ Please include enough to reproduce: the affected version, a minimal example or
29
+ proof of concept, and the impact you observed. If it touches signature
30
+ verification, revocation, DID/key resolution, or the handling of untrusted
31
+ input (downloaded documents, baked images, status lists), say so — those are the
32
+ highest-priority areas.
33
+
34
+ ## What to expect
35
+
36
+ - Acknowledgement of your report, normally within a few days.
37
+ - An assessment of severity and scope, and a fix or mitigation plan.
38
+ - Coordinated disclosure: we will agree a timeline with you and credit you in
39
+ the release notes and advisory unless you prefer to stay anonymous.
40
+ - A patched release on PyPI and a published GitHub Security Advisory once the
41
+ fix is available.
42
+
43
+ ## Scope and threat model
44
+
45
+ What the library defends against, and what it delegates to the operator (key
46
+ custody, DNS/TLS for `did:web`, hosting of published documents), is documented
47
+ in the [Security Model](https://github.com/luisgf/openbadgeslib/wiki/Security-Model)
48
+ wiki page. Reports that fall within that model — a bypass of algorithm pinning,
49
+ signature or recipient-binding verification, the SSRF/decompression guards, the
50
+ context allowlist, or the status-list checks — are especially welcome.
51
+
52
+ Out of scope: issues that require the operator’s own private keys or config to
53
+ already be compromised, and hardening we already document as a deliberate,
54
+ human-owned decision (for example, passphrase-encrypted private keys at rest).
@@ -21,17 +21,7 @@
21
21
  License along with this library.
22
22
  """
23
23
 
24
- # ── OpenBadges 1.0 (legacy) ─────────────────────────────────────────────────────
25
- # The classes formerly exposed as "OpenBadges 2.0" are the pre-2.0 wire format
26
- # (no @context/type, uid, verify{}, Unix timestamps). They are re-exported here
27
- # unchanged for backward compatibility; the strict OB 2.0 implementation lives
28
- # in openbadgeslib.ob2 (OB2Signer/OB2Verifier).
29
- from .ob1 import ( # noqa: F401
30
- Signer, Verifier, VerifyInfo,
31
- Badge, BadgeSigned, Assertion,
32
- BadgeStatus, BadgeImgType, BadgeType,
33
- extract_svg_assertion, extract_png_assertion,
34
- )
24
+ from typing import Any
35
25
 
36
26
  # ── OpenBadges 2.0 (strict) ──────────────────────────────────────────────────
37
27
  from .ob2 import ( # noqa: F401
@@ -47,3 +37,34 @@ from .ob3 import ( # noqa: F401
47
37
  # ── Shared utilities ────────────────────────────────────────────────────────────
48
38
  from .keys import KeyFactory, KeyRSA, KeyECC # noqa: F401
49
39
  from .util import __version__ # noqa: F401
40
+
41
+
42
+ # ── OpenBadges 1.0 (legacy) ──────────────────────────────────────────────────
43
+ # The unprefixed OB1 names (Signer, Verifier, Badge, …) stay importable from
44
+ # the top-level package for backward compatibility, but they are the legacy
45
+ # OpenBadges 1.0 surface: accessing one now emits a DeprecationWarning
46
+ # (removal in 4.0.0). Modern code uses openbadgeslib.ob2 / openbadgeslib.ob3.
47
+ # They are resolved lazily from the ob1 leaf modules (PEP 562), so a bare
48
+ # `import openbadgeslib` neither warns nor drags in the ob1 package.
49
+ _OB1_API = {
50
+ 'Signer': 'signer', 'Verifier': 'verifier', 'VerifyInfo': 'verifier',
51
+ 'Badge': 'badge', 'BadgeSigned': 'badge', 'Assertion': 'badge',
52
+ 'BadgeStatus': 'badge', 'BadgeImgType': 'badge', 'BadgeType': 'badge',
53
+ 'extract_svg_assertion': 'badge', 'extract_png_assertion': 'badge',
54
+ }
55
+
56
+
57
+ def __getattr__(name: str) -> Any:
58
+ module = _OB1_API.get(name)
59
+ if module is None:
60
+ raise AttributeError(
61
+ 'module %r has no attribute %r' % (__name__, name))
62
+ import importlib
63
+ import warnings
64
+ warnings.warn(
65
+ 'openbadgeslib.%s is the legacy OpenBadges 1.0 API and is deprecated; '
66
+ 'it will be removed in openbadgeslib 4.0.0. Use openbadgeslib.ob2 '
67
+ '(strict OB 2.0) or openbadgeslib.ob3 (OB 3.0). See the "OpenBadges '
68
+ '1.0 lifecycle" wiki page.' % name,
69
+ DeprecationWarning, stacklevel=2)
70
+ return getattr(importlib.import_module('.ob1.' + module, __name__), name)
@@ -1,6 +1,6 @@
1
1
  """JWS sign/verify backed by PyJWT algorithm implementations (RS256/384/512, ES256/384/512)."""
2
2
 
3
- from typing import Any, Dict, Optional, Set, Union
3
+ from typing import Any, Dict, Optional, Set, Union, cast
4
4
 
5
5
  from . import utils
6
6
  from .exceptions import SignatureError, MissingKey, MissingSigner, MissingVerifier, RouteMissingError
@@ -66,7 +66,7 @@ def sign(header_dict: Dict[str, Any], payload_dict: Dict[str, Any], key: Any) ->
66
66
  algo = _algo_for(alg_name)
67
67
  try:
68
68
  prepared = algo.prepare_key(key_to_pem(key))
69
- return algo.sign(signing_input, prepared)
69
+ return cast(bytes, algo.sign(signing_input, prepared))
70
70
  except (InvalidKeyError, ValueError) as exc:
71
71
  raise SignatureError(str(exc)) from exc
72
72
 
@@ -1,10 +1,19 @@
1
1
  """OpenBadges 1.0 (legacy) badge objects — compatibility shim, re-exports from openbadgeslib.ob1."""
2
+ import warnings
3
+
2
4
  from .ob1.badge import (
3
5
  BadgeStatus, BadgeImgType, BadgeType,
4
6
  Assertion, Badge, BadgeSigned,
5
7
  extract_svg_assertion, extract_png_assertion,
6
8
  )
7
9
 
10
+ warnings.warn(
11
+ 'openbadgeslib.badge is a legacy OpenBadges 1.0 compatibility shim and is '
12
+ 'deprecated; it will be removed in openbadgeslib 4.0.0. Import from '
13
+ 'openbadgeslib.ob2 (strict OB 2.0) or openbadgeslib.ob3 (OB 3.0) instead. '
14
+ 'See the "OpenBadges 1.0 lifecycle" wiki page.',
15
+ DeprecationWarning, stacklevel=2)
16
+
8
17
  __all__ = [
9
18
  'BadgeStatus', 'BadgeImgType', 'BadgeType',
10
19
  'Assertion', 'Badge', 'BadgeSigned',
@@ -29,7 +29,7 @@
29
29
  from struct import pack
30
30
  from zlib import crc32
31
31
 
32
- from typing import List, Optional, Tuple, Union
32
+ from typing import List, Optional, Tuple, Union, cast
33
33
 
34
34
  from defusedxml.minidom import parseString
35
35
  from png import Reader, signature as _png_signature
@@ -98,7 +98,7 @@ def bake_svg(image_bytes: bytes, token: str, comment: Optional[str] = None, *,
98
98
  svg_tag.appendChild(node)
99
99
  if comment:
100
100
  svg_tag.appendChild(svg_doc.createComment(comment))
101
- return svg_doc.toxml().encode('utf-8')
101
+ return cast(bytes, svg_doc.toxml().encode('utf-8'))
102
102
  finally:
103
103
  svg_doc.unlink()
104
104
 
@@ -133,7 +133,7 @@ def extract_svg(image_bytes: bytes, *, element: str = SVG_ELEMENT,
133
133
  return None
134
134
  attrs = nodes[0].attributes
135
135
  if 'verify' in attrs:
136
- return attrs['verify'].nodeValue
136
+ return cast(Optional[str], attrs['verify'].nodeValue)
137
137
  if not text_fallback:
138
138
  return None
139
139
  text = ''.join(
@@ -158,7 +158,7 @@ def _serialize_png(chunks: List[Tuple[Union[str, bytes], bytes]]) -> bytes:
158
158
  checksum = crc32(tag)
159
159
  checksum = crc32(data, checksum) & 0xFFFFFFFF
160
160
  out += pack("!I", checksum)
161
- return out
161
+ return cast(bytes, out)
162
162
 
163
163
 
164
164
  def bake_png(image_bytes: bytes, token: str, text_comment: Optional[str] = None, *,
@@ -21,11 +21,17 @@
21
21
  License along with this library.
22
22
  """
23
23
 
24
- from typing import Any, Optional, Tuple, Union
24
+ # Key material generation and parsing. All backed by ``cryptography`` (already
25
+ # a hard dependency through PyJWT[crypto]); pycryptodome and python-ecdsa were
26
+ # dropped in the 3.7 port (#167) — they only ever generated/parsed keys here,
27
+ # never signed, and python-ecdsa carried a permanent pip-audit CVE flag
28
+ # (CVE-2024-23342, Minerva). ``key_to_pem`` still accepts a live pycryptodome /
29
+ # python-ecdsa key object via a soft import, for a caller predating the port.
30
+
31
+ from typing import Any, Optional, Tuple, Union, cast
25
32
  from .errors import PublicKeyReadError, UnknownKeyType
26
- from ecdsa import SigningKey, VerifyingKey, NIST256p
27
- from Crypto.PublicKey import RSA
28
33
  from cryptography.hazmat.primitives import serialization as _crypto_serialization
34
+ from cryptography.hazmat.primitives.asymmetric import ec, rsa
29
35
  from cryptography.hazmat.primitives.asymmetric.ed25519 import (
30
36
  Ed25519PrivateKey, Ed25519PublicKey)
31
37
  from enum import Enum
@@ -52,6 +58,27 @@ def KeyFactory(key_type: 'KeyType' = KeyType.RSA) -> 'Union[KeyRSA, KeyECC, KeyE
52
58
  raise UnknownKeyType()
53
59
 
54
60
 
61
+ def _pem_bytes(key_pem: Union[str, bytes]) -> bytes:
62
+ return key_pem.encode('utf-8') if isinstance(key_pem, str) else key_pem
63
+
64
+
65
+ def _private_pem(key: Any) -> bytes:
66
+ """Serialise an RSA/EC private key to a TraditionalOpenSSL PEM — PKCS#1
67
+ ``RSA PRIVATE KEY`` / SEC1 ``EC PRIVATE KEY``, exactly what pycryptodome and
68
+ python-ecdsa emitted, so existing key files round-trip unchanged."""
69
+ return cast(bytes, key.private_bytes(
70
+ _crypto_serialization.Encoding.PEM,
71
+ _crypto_serialization.PrivateFormat.TraditionalOpenSSL,
72
+ _crypto_serialization.NoEncryption()))
73
+
74
+
75
+ def _public_pem(key: Any) -> bytes:
76
+ """Serialise a public key to a SubjectPublicKeyInfo ``PUBLIC KEY`` PEM."""
77
+ return cast(bytes, key.public_bytes(
78
+ _crypto_serialization.Encoding.PEM,
79
+ _crypto_serialization.PublicFormat.SubjectPublicKeyInfo))
80
+
81
+
55
82
  class KeyBase():
56
83
  def __init__(self) -> None:
57
84
  self.priv_key: Any = None # crypto Object
@@ -73,67 +100,56 @@ class KeyRSA(KeyBase):
73
100
 
74
101
  def generate_keypair(self) -> Tuple[bytes, bytes]:
75
102
  """ Generate a RSA Key, returning in PEM Format """
76
-
77
- # RSA Key Generation
78
- self.priv_key = RSA.generate(self._key_size)
79
- priv_key_pem = self.priv_key.export_key('PEM')
80
- self.pub_key = self.priv_key.publickey()
81
- pub_key_pem = self.pub_key.export_key('PEM')
82
-
83
- return priv_key_pem, pub_key_pem
103
+ self.priv_key = rsa.generate_private_key(
104
+ public_exponent=65537, key_size=self._key_size)
105
+ self.pub_key = self.priv_key.public_key()
106
+ return self.get_priv_key_pem(), self.get_pub_key_pem()
84
107
 
85
108
  def read_private_key(self, key_pem: Any = None) -> None:
86
109
  """ Read the private key from param in PEM format """
87
- self.priv_key = RSA.import_key(key_pem)
110
+ self.priv_key = _crypto_serialization.load_pem_private_key(
111
+ _pem_bytes(key_pem), password=None)
88
112
 
89
113
  def read_public_key(self, key_pem: Any = None) -> None:
90
114
  """ Read the public key from param in PEM format """
91
- self.pub_key = RSA.import_key(key_pem)
115
+ self.pub_key = _crypto_serialization.load_pem_public_key(
116
+ _pem_bytes(key_pem))
92
117
 
93
118
  def get_priv_key_pem(self) -> bytes:
94
- return self.priv_key.export_key('PEM')
119
+ return _private_pem(self.priv_key)
95
120
 
96
121
  def get_pub_key_pem(self) -> bytes:
97
- return self.pub_key.export_key('PEM')
122
+ return _public_pem(self.pub_key)
98
123
 
99
124
 
100
125
  class KeyECC(KeyBase):
101
- """ Elliptic Curve Cryptography Factory class """
126
+ """ Elliptic Curve Cryptography Factory class (NIST P-256 / ES256) """
102
127
 
103
- def __init__(self, key_curve: Any = NIST256p) -> None:
104
- self._key_curve = key_curve
128
+ def __init__(self, key_curve: Any = None) -> None:
129
+ self._key_curve = key_curve or ec.SECP256R1()
105
130
  super().__init__()
106
131
 
107
132
  def generate_keypair(self) -> Tuple[bytes, bytes]:
108
- """ Generate a ECDSA keypair """
109
-
110
- # Private key generation
111
- self.priv_key = SigningKey.generate(curve=self._key_curve)
112
- priv_key_pem = self.priv_key.to_pem()
113
-
114
- # Derive the public (verifying) key from the private key.
115
- self.pub_key = self.priv_key.get_verifying_key()
116
- pub_key_pem = self.pub_key.to_pem()
117
-
118
- return priv_key_pem, pub_key_pem
133
+ """ Generate an ECDSA keypair """
134
+ self.priv_key = ec.generate_private_key(self._key_curve)
135
+ self.pub_key = self.priv_key.public_key()
136
+ return self.get_priv_key_pem(), self.get_pub_key_pem()
119
137
 
120
- def read_private_key(self, key_pem: Optional[Union[str, bytes]] = None) -> None:
138
+ def read_private_key(self, key_pem: Any = None) -> None:
121
139
  """ Read the private key from param in PEM format """
122
- self.priv_key = SigningKey.from_pem(key_pem)
140
+ self.priv_key = _crypto_serialization.load_pem_private_key(
141
+ _pem_bytes(key_pem), password=None)
123
142
 
124
- def read_public_key(self, key_pem: Optional[Union[str, bytes]] = None) -> None:
143
+ def read_public_key(self, key_pem: Any = None) -> None:
125
144
  """ Read the public key from param in PEM format """
126
- self.pub_key = VerifyingKey.from_pem(key_pem)
145
+ self.pub_key = _crypto_serialization.load_pem_public_key(
146
+ _pem_bytes(key_pem))
127
147
 
128
148
  def get_priv_key_pem(self) -> bytes:
129
- return self.priv_key.to_pem()
149
+ return _private_pem(self.priv_key)
130
150
 
131
151
  def get_pub_key_pem(self) -> bytes:
132
- return self.pub_key.to_pem()
133
-
134
-
135
- def _pem_bytes(key_pem: Union[str, bytes]) -> bytes:
136
- return key_pem.encode('utf-8') if isinstance(key_pem, str) else key_pem
152
+ return _public_pem(self.pub_key)
137
153
 
138
154
 
139
155
  def _load_ed25519_private_key(key_pem: Optional[Union[str, bytes]]) -> Ed25519PrivateKey:
@@ -158,8 +174,8 @@ class KeyEd25519(KeyBase):
158
174
  """Edwards-curve Ed25519 (EdDSA) key class, backed by ``cryptography``.
159
175
 
160
176
  Kept separate from KeyECC: Ed25519 is an EdDSA curve, not an ECDSA NIST
161
- curve. It maps to the JWS ``EdDSA`` algorithm, and the ``ecdsa`` library
162
- used for the NIST curves would misread its PEM (see detect_key_type).
177
+ curve. It maps to the JWS ``EdDSA`` algorithm, and its PEM is PKCS#8
178
+ (Ed25519 has no TraditionalOpenSSL encoding).
163
179
  """
164
180
 
165
181
  def generate_keypair(self) -> Tuple[bytes, bytes]:
@@ -177,15 +193,13 @@ class KeyEd25519(KeyBase):
177
193
  self.pub_key = _load_ed25519_public_key(key_pem)
178
194
 
179
195
  def get_priv_key_pem(self) -> bytes:
180
- return self.priv_key.private_bytes(
196
+ return cast(bytes, self.priv_key.private_bytes(
181
197
  _crypto_serialization.Encoding.PEM,
182
198
  _crypto_serialization.PrivateFormat.PKCS8,
183
- _crypto_serialization.NoEncryption())
199
+ _crypto_serialization.NoEncryption()))
184
200
 
185
201
  def get_pub_key_pem(self) -> bytes:
186
- return self.pub_key.public_bytes(
187
- _crypto_serialization.Encoding.PEM,
188
- _crypto_serialization.PublicFormat.SubjectPublicKeyInfo)
202
+ return _public_pem(self.pub_key)
189
203
 
190
204
 
191
205
  def alg_for_key_type(key_type: 'KeyType') -> str:
@@ -199,32 +213,58 @@ def alg_for_key_type(key_type: 'KeyType') -> str:
199
213
  raise UnknownKeyType('No signing algorithm for key type: %r' % (key_type,))
200
214
 
201
215
 
216
+ def _legacy_key_to_pem(key: Any) -> Optional[Union[str, bytes]]:
217
+ """Best-effort PEM for a live pycryptodome RSA or python-ecdsa key object.
218
+
219
+ Kept for backward compatibility with a caller still holding one from before
220
+ the cryptography port (#167). Both libraries were dropped as dependencies,
221
+ so this soft-imports them and returns ``None`` when they are absent (or the
222
+ object is neither) — the caller then raises UnknownKeyType.
223
+ """
224
+ try:
225
+ from Crypto.PublicKey import RSA as _RSA
226
+ if isinstance(key, _RSA.RsaKey):
227
+ return cast(bytes, key.export_key('PEM'))
228
+ except ImportError:
229
+ pass
230
+ try:
231
+ from ecdsa import SigningKey as _SigningKey, VerifyingKey as _VerifyingKey
232
+ if isinstance(key, (_SigningKey, _VerifyingKey)):
233
+ return cast(Union[str, bytes], key.to_pem())
234
+ except ImportError:
235
+ pass
236
+ return None
237
+
238
+
202
239
  def key_to_pem(key: Any) -> Union[str, bytes]:
203
- """Convert a pycryptodome RSA or ecdsa key object to PEM bytes.
240
+ """Convert a key object to PEM bytes.
204
241
 
205
- Bytes/str are passed through unchanged. Centralised here so the OB2 JWS
206
- layer and both OB3 signer/verifier share one implementation instead of
207
- three hand-maintained copies.
242
+ Handles ``cryptography`` key objects (what keys.py and ob1.badge now
243
+ produce) and passes bytes/str through unchanged; a live pycryptodome /
244
+ python-ecdsa object is still accepted via :func:`_legacy_key_to_pem`.
245
+ Centralised here so the OB1/OB2 JWS layer and both OB3 signer/verifier
246
+ share one implementation instead of hand-maintained copies.
208
247
  """
209
- if isinstance(key, RSA.RsaKey):
210
- return key.export_key('PEM')
211
- if isinstance(key, (SigningKey, VerifyingKey)):
212
- return key.to_pem()
248
+ if isinstance(key, (bytes, str)):
249
+ return key
250
+ if isinstance(key, (rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey)):
251
+ return _private_pem(key)
252
+ if isinstance(key, (rsa.RSAPublicKey, ec.EllipticCurvePublicKey)):
253
+ return _public_pem(key)
213
254
  if isinstance(key, Ed25519PrivateKey):
214
255
  return key.private_bytes(
215
256
  _crypto_serialization.Encoding.PEM,
216
257
  _crypto_serialization.PrivateFormat.PKCS8,
217
258
  _crypto_serialization.NoEncryption())
218
259
  if isinstance(key, Ed25519PublicKey):
219
- return key.public_bytes(
220
- _crypto_serialization.Encoding.PEM,
221
- _crypto_serialization.PublicFormat.SubjectPublicKeyInfo)
222
- if isinstance(key, (bytes, str)):
223
- return key
260
+ return _public_pem(key)
261
+ legacy = _legacy_key_to_pem(key)
262
+ if legacy is not None:
263
+ return legacy
224
264
  raise UnknownKeyType('Unsupported key object type: %r' % type(key))
225
265
 
226
266
 
227
- def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict:
267
+ def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict[str, Any]:
228
268
  """Serialise a public key PEM as a public JWK dict.
229
269
 
230
270
  Counterpart of the OB3 signer's private-key-based JWK derivation, for
@@ -232,7 +272,6 @@ def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict:
232
272
  verification keys in a did:web DID document.
233
273
  """
234
274
  import json
235
- from cryptography.hazmat.primitives.asymmetric import ec, rsa
236
275
  from jwt.algorithms import ECAlgorithm, OKPAlgorithm, RSAAlgorithm
237
276
  try:
238
277
  pub = _crypto_serialization.load_pem_public_key(_pem_bytes(pubkey_pem))
@@ -250,7 +289,7 @@ def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict:
250
289
  except Exception as exc:
251
290
  raise PublicKeyReadError(
252
291
  'could not read public key PEM: %s' % exc) from exc
253
- return json.loads(jwk_json)
292
+ return cast(dict[str, Any], json.loads(jwk_json))
254
293
 
255
294
 
256
295
  def ec_curve_from_pem(pem_data: Union[str, bytes]) -> Optional[str]:
@@ -262,7 +301,6 @@ def ec_curve_from_pem(pem_data: Union[str, bytes]) -> Optional[str]:
262
301
  uses this to pick the ES256 vs ES384 signing backend — the ECDSA curve
263
302
  fixes the JOSE algorithm.
264
303
  """
265
- from cryptography.hazmat.primitives.asymmetric import ec
266
304
  data = _pem_bytes(pem_data)
267
305
  for load in (
268
306
  lambda: _crypto_serialization.load_pem_public_key(data),
@@ -279,53 +317,30 @@ def ec_curve_from_pem(pem_data: Union[str, bytes]) -> Optional[str]:
279
317
  return None
280
318
 
281
319
 
282
- def _is_ed25519_pem(pem_data: Union[str, bytes]) -> bool:
283
- """True if pem_data is an Ed25519 public or private key.
320
+ def detect_key_type(pem_data: Union[str, bytes]) -> 'KeyType':
321
+ """Positive key-type detection via ``cryptography``'s PEM loaders.
284
322
 
285
- Probed before the RSA/ECC checks in detect_key_type: the ``ecdsa`` library's
286
- VerifyingKey/SigningKey.from_pem *accept* an Ed25519 PEM and would otherwise
287
- misclassify it as KeyType.ECC, pinning the wrong (ES*) algorithm family.
288
- ``cryptography``'s loaders are unambiguous about the key type.
323
+ Accepts a public or private PEM and distinguishes Ed25519/RSA/EC by the
324
+ loaded key's concrete type cryptography's loaders are unambiguous, unlike
325
+ the old python-ecdsa loader which accepted an Ed25519 PEM as an EC key and
326
+ forced the wrong (ES*) algorithm family.
289
327
  """
290
328
  data = _pem_bytes(pem_data)
291
- try:
292
- if isinstance(_crypto_serialization.load_pem_public_key(data), Ed25519PublicKey):
293
- return True
294
- except Exception:
295
- pass
296
- try:
297
- if isinstance(_crypto_serialization.load_pem_private_key(data, password=None),
298
- Ed25519PrivateKey):
299
- return True
300
- except Exception:
301
- pass
302
- return False
303
-
304
-
305
- def detect_key_type(pem_data: Union[str, bytes]) -> 'KeyType':
306
- """ Positive Key type detection """
329
+ key: Any = None
330
+ for load in (
331
+ lambda: _crypto_serialization.load_pem_public_key(data),
332
+ lambda: _crypto_serialization.load_pem_private_key(data, password=None),
333
+ ):
334
+ try:
335
+ key = load()
336
+ break
337
+ except Exception:
338
+ continue
307
339
 
308
- # Ed25519 first: see _is_ed25519_pem — the ecdsa library would otherwise
309
- # claim an Ed25519 PEM as ECC below.
310
- if _is_ed25519_pem(pem_data):
340
+ if isinstance(key, (Ed25519PrivateKey, Ed25519PublicKey)):
311
341
  return KeyType.ED25519
312
-
313
- try:
314
- RSA.import_key(pem_data)
342
+ if isinstance(key, (rsa.RSAPrivateKey, rsa.RSAPublicKey)):
315
343
  return KeyType.RSA
316
- except Exception:
317
- pass
318
-
319
- try:
320
- VerifyingKey.from_pem(pem_data)
321
- return KeyType.ECC
322
- except Exception:
323
- pass
324
-
325
- try:
326
- SigningKey.from_pem(pem_data)
344
+ if isinstance(key, (ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey)):
327
345
  return KeyType.ECC
328
- except Exception:
329
- pass
330
-
331
346
  raise UnknownKeyType('Unable to guess Key type')