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.
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/Changelog.txt +91 -0
- {openbadgeslib-3.5.1/openbadgeslib.egg-info → openbadgeslib-3.7.0}/PKG-INFO +1 -5
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/README.md +0 -2
- openbadgeslib-3.7.0/SECURITY.md +54 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/__init__.py +32 -11
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/_jws/__init__.py +2 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/badge.py +9 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/baking.py +4 -4
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/keys.py +120 -105
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/mail.py +1 -1
- openbadgeslib-3.7.0/openbadgeslib/ob1/__init__.py +61 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob1/badge.py +13 -34
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob1/verifier.py +2 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/models.py +14 -14
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/verifier.py +3 -3
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/__init__.py +10 -6
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/credential.py +44 -16
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/did.py +64 -11
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/eudi.py +5 -5
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/ldp.py +13 -13
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/signer.py +4 -4
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/status.py +2 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/status_list.py +3 -3
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/status_registry.py +6 -6
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/verifier.py +59 -11
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_keygenerator.py +24 -7
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_publish.py +235 -14
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_signer.py +70 -35
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_verifier.py +8 -1
- openbadgeslib-3.7.0/openbadgeslib/signer.py +13 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/util.py +48 -2
- openbadgeslib-3.7.0/openbadgeslib/verifier.py +13 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0/openbadgeslib.egg-info}/PKG-INFO +1 -5
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/SOURCES.txt +2 -1
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/requires.txt +0 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/pyproject.toml +8 -8
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conftest.py +2 -2
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_cli_smoke.py +33 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_key_operation.py +64 -69
- openbadgeslib-3.7.0/tests/test_ob1_deprecation.py +158 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_credential.py +8 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_did.py +105 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_ldp_sign.py +3 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_publish_cli.py +219 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_verifier.py +36 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_util.py +47 -1
- openbadgeslib-3.5.1/openbadgeslib/ob1/__init__.py +0 -36
- openbadgeslib-3.5.1/openbadgeslib/signer.py +0 -4
- openbadgeslib-3.5.1/openbadgeslib/verifier.py +0 -4
- openbadgeslib-3.5.1/tests/runtests.sh +0 -3
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/LICENSE.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/MANIFEST.in +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/docs/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/config.ini.example +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/confparser.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/__init__.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/setup.cfg +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/config1.ini +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_cli_json.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_confparser.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_docs.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_jws.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob1_badge_io.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob1_verifier.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob2_verifier.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_status.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_ob3_status_registry.py +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-3.5.1 → openbadgeslib-3.7.0}/tests/withoutxmlheader.svg +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
self.
|
|
79
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
119
|
+
return _private_pem(self.priv_key)
|
|
95
120
|
|
|
96
121
|
def get_pub_key_pem(self) -> bytes:
|
|
97
|
-
return self.pub_key
|
|
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 =
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
self.
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
149
|
+
return _private_pem(self.priv_key)
|
|
130
150
|
|
|
131
151
|
def get_pub_key_pem(self) -> bytes:
|
|
132
|
-
return self.pub_key
|
|
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
|
|
162
|
-
|
|
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
|
|
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
|
|
240
|
+
"""Convert a key object to PEM bytes.
|
|
204
241
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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,
|
|
210
|
-
return key
|
|
211
|
-
if isinstance(key, (
|
|
212
|
-
return key
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
283
|
-
"""
|
|
320
|
+
def detect_key_type(pem_data: Union[str, bytes]) -> 'KeyType':
|
|
321
|
+
"""Positive key-type detection via ``cryptography``'s PEM loaders.
|
|
284
322
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')
|