openbadgeslib 3.6.0__tar.gz → 3.8.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.6.0 → openbadgeslib-3.8.0}/Changelog.txt +86 -0
- {openbadgeslib-3.6.0/openbadgeslib.egg-info → openbadgeslib-3.8.0}/PKG-INFO +1 -5
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/README.md +0 -2
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/__init__.py +32 -11
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/badge.py +9 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/confparser.py +19 -1
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/keys.py +115 -100
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/mail.py +1 -1
- openbadgeslib-3.8.0/openbadgeslib/ob1/__init__.py +61 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob1/badge.py +8 -29
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/did.py +58 -5
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/status.py +139 -13
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/status_list.py +12 -2
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/verifier.py +68 -11
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/openbadges_keygenerator.py +24 -7
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/openbadges_publish.py +241 -16
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/openbadges_signer.py +70 -35
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/openbadges_verifier.py +8 -1
- openbadgeslib-3.8.0/openbadgeslib/signer.py +13 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/util.py +48 -2
- openbadgeslib-3.8.0/openbadgeslib/verifier.py +13 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0/openbadgeslib.egg-info}/PKG-INFO +1 -5
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib.egg-info/SOURCES.txt +1 -1
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib.egg-info/requires.txt +0 -2
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/pyproject.toml +0 -2
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/conftest.py +2 -2
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_cli_smoke.py +33 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_key_operation.py +64 -69
- openbadgeslib-3.8.0/tests/test_ob1_deprecation.py +158 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_did.py +105 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_publish_cli.py +274 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_status.py +123 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_util.py +47 -1
- openbadgeslib-3.6.0/openbadgeslib/ob1/__init__.py +0 -36
- openbadgeslib-3.6.0/openbadgeslib/signer.py +0 -4
- openbadgeslib-3.6.0/openbadgeslib/verifier.py +0 -4
- openbadgeslib-3.6.0/tests/runtests.sh +0 -3
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/LICENSE.txt +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/MANIFEST.in +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/SECURITY.md +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/docs/README.md +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/_jws/__init__.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/baking.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/config.ini.example +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob1/signer.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob1/verifier.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob2/__init__.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob2/models.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob2/signer.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob2/verifier.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/__init__.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/contexts/__init__.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/contexts/credentials-v2.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.1.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.2.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/contexts/ob-v3p0-context-3.0.3.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/contexts/security-data-integrity-v2.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/contexts/security-multikey-v1.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/credential.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/eudi.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/ldp.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/signer.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/ob3/status_registry.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/setup.cfg +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/config1.ini +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/conformance/Dockerfile.ob3 +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/conformance/README.md +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/conformance/conftest.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/conformance/docker-compose.yml +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/conformance/test_official_validators.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/ob_schemas/README.md +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/ob_schemas/ob_v3p0_achievementcredential_schema.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/ob_schemas/ob_v3p0_profile_schema.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/ob_schemas/refresh.sh +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/vc_di_eddsa/README.md +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/vc_di_eddsa/credentials-examples-v2.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/vc_di_eddsa/doc-hash.txt +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/vc_di_eddsa/key-pair.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/vc_di_eddsa/proof-config-hash.txt +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/fixtures/vc_di_eddsa/signed-credential.json +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/ldp_helpers.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_cli_json.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_confparser.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_docs.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_jws.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob1_badge_io.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob1_verifier.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob2_cli.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob2_models.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob2_signer.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob2_verifier.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_conformance_schema.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_contexts.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_credential.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_did_doc.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_eudi_sd_jwt.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_ldp.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_ldp_cli.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_ldp_sign.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_signer.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_signer_cli_ldp.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_status_list.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_status_registry.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_ob3_verifier.py +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-3.6.0 → openbadgeslib-3.8.0}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,92 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v3.8.0 - 2026-07-08
|
|
8
|
+
|
|
9
|
+
- security(ob3): status lists gain replay protection and opt-in proof
|
|
10
|
+
verification, closing two documented trust gaps. (1) A per-badge
|
|
11
|
+
`status_validity_days` makes published lists carry a `validUntil`, and the
|
|
12
|
+
reader now enforces the list's validFrom/validUntil window — a lapsed
|
|
13
|
+
(replayed, or no-longer-republished) `revocation.jwt` is rejected instead
|
|
14
|
+
of silently resurrecting a revoked badge. (2)
|
|
15
|
+
`check_credential_status(..., verify_list=True)` (and
|
|
16
|
+
`OB3Verifier.check_status(..., verify_list=True)`) additionally verifies
|
|
17
|
+
the status list credential's own JWT-VC proof and binds its issuer to the
|
|
18
|
+
badge's, so a compromised status host cannot serve a validly-shaped list
|
|
19
|
+
that silently un-revokes. The list is signed with the badge key, so that
|
|
20
|
+
same key (reused automatically) verifies it; a did:web/did:key issuer is
|
|
21
|
+
resolved otherwise. Both are opt-in — the default stays a single
|
|
22
|
+
offline-friendly fetch, no behaviour break. (#164)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
* v3.7.0 - 2026-07-08
|
|
26
|
+
|
|
27
|
+
- security(deps): `keys.py` (and the OB1 key-loading path) are ported to
|
|
28
|
+
`cryptography`, and both `pycryptodome` and `python-ecdsa` are dropped as
|
|
29
|
+
dependencies. They only generated and parsed key material here — signing
|
|
30
|
+
always went through PyJWT+`cryptography` — all of which `cryptography`
|
|
31
|
+
(already a hard dependency via `PyJWT[crypto]`) covers. This removes
|
|
32
|
+
python-ecdsa's permanent pip-audit flag (CVE-2024-23342 "Minerva", wontfix
|
|
33
|
+
upstream), moves ECC key generation onto constant-time code, and shrinks
|
|
34
|
+
the install to a single crypto backend. PEM output is byte-compatible
|
|
35
|
+
(PKCS#1 `RSA PRIVATE KEY`, SEC1 `EC PRIVATE KEY`, SubjectPublicKeyInfo
|
|
36
|
+
public keys), so existing key files round-trip. `key_to_pem` still accepts
|
|
37
|
+
a live pycryptodome/python-ecdsa key object via a soft import, for a caller
|
|
38
|
+
predating the port. (#167)
|
|
39
|
+
|
|
40
|
+
- feat(cli): `--json` machine-readable output and a documented exit-code
|
|
41
|
+
contract for `openbadges-signer`, `openbadges-publish` and
|
|
42
|
+
`openbadges-keygenerator` (the verifier already had one), so issuance can
|
|
43
|
+
be automated without scraping human text. Each prints exactly one JSON
|
|
44
|
+
object to stdout — the signer `{ob_version, badge_file, jti, status_index,
|
|
45
|
+
proof_format}`, the keygenerator `{key_type, private_key, public_key}`,
|
|
46
|
+
publish `{did, files_written, status_operation, skipped}`, and
|
|
47
|
+
`--list`/`--status` the queried records — under a shared exit contract:
|
|
48
|
+
`0` success, `2` partial (some badges skipped), `1` any error (reported as
|
|
49
|
+
`{"error": "..."}`). Human output and its historical (inconsistent) exit
|
|
50
|
+
codes are untouched — unifying those is reserved for 4.0.0. `publish
|
|
51
|
+
--json` is OpenBadges 3.0 only. (#166)
|
|
52
|
+
|
|
53
|
+
- feat(ob3): JWT-VC verification now honours the token's `kid` header for a
|
|
54
|
+
multi-key did:web issuer. `OB3Verifier.for_issuer_did` fetches the DID
|
|
55
|
+
document once and selects the exact `verificationMethod` the kid names (an
|
|
56
|
+
absolute `did:web:…#badge_N` URL, or a bare `#badge_N` resolved against the
|
|
57
|
+
issuer DID), so issuers that rotate keys or publish one key per badge now
|
|
58
|
+
verify without pinning `-k`/`-l` — closing the gap where only the first
|
|
59
|
+
listed key worked. Fails closed when the kid names a method of a *different*
|
|
60
|
+
DID (the header is attacker-controlled) or one absent from the document; a
|
|
61
|
+
token with no kid still falls back to the first method. (#163)
|
|
62
|
+
|
|
63
|
+
- feat(ob1): OpenBadges 1.0 is now deprecated (removal scheduled for the
|
|
64
|
+
grouped 4.0.0 release, #170). Reaching the legacy import surface — the
|
|
65
|
+
`openbadgeslib.ob1` package, the top-level `openbadgeslib.badge` /
|
|
66
|
+
`openbadgeslib.signer` / `openbadgeslib.verifier` shims, and the
|
|
67
|
+
unprefixed `openbadgeslib.Signer` / `Verifier` / `Badge` / … re-exports —
|
|
68
|
+
now emits a `DeprecationWarning`, and the `-V 1` CLI paths (signer,
|
|
69
|
+
verifier, publish) print a `[!]` notice (suppressed under the verifier's
|
|
70
|
+
`--json`, so machine output stays clean). OB1 stays fully functional; a
|
|
71
|
+
bare `import openbadgeslib` and the OB2/OB3 APIs stay silent. OpenBadges
|
|
72
|
+
2.0 is **not** affected — it remains fully supported. The lifecycle policy
|
|
73
|
+
and the rejected alternatives (a `[legacy]` extra, a separate PyPI
|
|
74
|
+
package) are recorded on the "Python API OB1" wiki page. (#159)
|
|
75
|
+
|
|
76
|
+
- feat(publish): `openbadges-publish -V 3` gains two read-only registry
|
|
77
|
+
query commands, closing the credential lifecycle from the CLI
|
|
78
|
+
(issue → revoke → audit). `--list [-b badge]` tabulates every issued
|
|
79
|
+
credential — jti, recipient, issue date and state (active / REVOKED /
|
|
80
|
+
SUSPENDED); `--status <jti|email>` prints the full record of the matching
|
|
81
|
+
credential(s), including the revocation or suspension date and reason.
|
|
82
|
+
Both read only the private status registries, so neither needs an output
|
|
83
|
+
directory (`-o`). (#157)
|
|
84
|
+
|
|
85
|
+
- refactor(cli): `openbadges-keygenerator` no longer accepts the decorative
|
|
86
|
+
`-V/--ob-version` flag. Key material never depended on the OpenBadges
|
|
87
|
+
version (the algorithm comes from the badge's `key_type`), so the flag only
|
|
88
|
+
changed a log line. Internal tidy-ups alongside it: a dead `if args.badge`
|
|
89
|
+
guard in the signer (`-b` is required), the redundant `tests/runtests.sh`
|
|
90
|
+
wrapper, and an inaccurate conftest comment. (#156)
|
|
91
|
+
|
|
92
|
+
|
|
7
93
|
* v3.6.0 - 2026-07-08
|
|
8
94
|
|
|
9
95
|
- feat(ob3): the verifier now exposes the raw, already-validated VC document
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.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
|
|
@@ -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,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',
|
|
@@ -105,6 +105,7 @@ class OB3StatusConfig:
|
|
|
105
105
|
size_bits: int
|
|
106
106
|
registry_path: str # private index registry JSON
|
|
107
107
|
list_urls: Dict[str, str] # purpose -> public status list URL
|
|
108
|
+
validity_days: Optional[int] = None # validUntil horizon; None => no bound
|
|
108
109
|
|
|
109
110
|
|
|
110
111
|
def ob3_status_config(conf: ConfigParser,
|
|
@@ -140,6 +141,22 @@ def ob3_status_config(conf: ConfigParser,
|
|
|
140
141
|
raise ValueError("[%s] status_size_bits must be an integer"
|
|
141
142
|
% badge_section) from None
|
|
142
143
|
|
|
144
|
+
# Optional validUntil horizon: when set, published status lists carry a
|
|
145
|
+
# validUntil = now + N days, and a verifier rejects a stale copy (replay
|
|
146
|
+
# protection). Unset => no bound (offline-friendly default). The issuer
|
|
147
|
+
# must republish within the window (see the wiki).
|
|
148
|
+
validity_raw = (conf[badge_section].get('status_validity_days') or '').strip()
|
|
149
|
+
validity_days: Optional[int] = None
|
|
150
|
+
if validity_raw:
|
|
151
|
+
try:
|
|
152
|
+
validity_days = int(validity_raw)
|
|
153
|
+
except ValueError:
|
|
154
|
+
raise ValueError("[%s] status_validity_days must be an integer"
|
|
155
|
+
% badge_section) from None
|
|
156
|
+
if validity_days <= 0:
|
|
157
|
+
raise ValueError("[%s] status_validity_days must be positive"
|
|
158
|
+
% badge_section)
|
|
159
|
+
|
|
143
160
|
base_status = conf['paths'].get('base_status') or \
|
|
144
161
|
os.path.join(conf['paths']['base'], 'status')
|
|
145
162
|
registry_path = os.path.join(base_status, badge_section + '.json')
|
|
@@ -151,7 +168,8 @@ def ob3_status_config(conf: ConfigParser,
|
|
|
151
168
|
list_urls = {p: urljoin(status_base, p + '.jwt') for p in purposes}
|
|
152
169
|
|
|
153
170
|
return OB3StatusConfig(purposes=purposes, size_bits=size_bits,
|
|
154
|
-
registry_path=registry_path, list_urls=list_urls
|
|
171
|
+
registry_path=registry_path, list_urls=list_urls,
|
|
172
|
+
validity_days=validity_days)
|
|
155
173
|
|
|
156
174
|
|
|
157
175
|
class ConfParser():
|
|
@@ -21,11 +21,17 @@
|
|
|
21
21
|
License along with this library.
|
|
22
22
|
"""
|
|
23
23
|
|
|
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
|
+
|
|
24
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
|
|
119
|
+
return _private_pem(self.priv_key)
|
|
95
120
|
|
|
96
121
|
def get_pub_key_pem(self) -> bytes:
|
|
97
|
-
return
|
|
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
|
|
149
|
+
return _private_pem(self.priv_key)
|
|
130
150
|
|
|
131
151
|
def get_pub_key_pem(self) -> bytes:
|
|
132
|
-
return
|
|
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]:
|
|
@@ -183,9 +199,7 @@ class KeyEd25519(KeyBase):
|
|
|
183
199
|
_crypto_serialization.NoEncryption()))
|
|
184
200
|
|
|
185
201
|
def get_pub_key_pem(self) -> bytes:
|
|
186
|
-
return
|
|
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,28 +213,54 @@ 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
|
|
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
|
|
|
@@ -232,7 +272,6 @@ def public_jwk_from_pem(pubkey_pem: Union[str, bytes]) -> dict[str, Any]:
|
|
|
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))
|
|
@@ -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')
|
|
@@ -29,7 +29,7 @@ from email.mime.multipart import MIMEMultipart
|
|
|
29
29
|
from email.mime.text import MIMEText
|
|
30
30
|
from email.utils import formatdate
|
|
31
31
|
from email.header import Header
|
|
32
|
-
from .ob1 import BadgeImgType
|
|
32
|
+
from .ob1.badge import BadgeImgType # leaf import: avoids the ob1 deprecation warning
|
|
33
33
|
from .errors import BadgeImgFormatUnsupported
|
|
34
34
|
|
|
35
35
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenBadges Library
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
|
|
5
|
+
Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
|
|
6
|
+
|
|
7
|
+
All rights reserved.
|
|
8
|
+
|
|
9
|
+
This library is free software; you can redistribute it and/or
|
|
10
|
+
modify it under the terms of the GNU Lesser General Public
|
|
11
|
+
License as published by the Free Software Foundation; either
|
|
12
|
+
version 3.0 of the License, or (at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This library is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
17
|
+
Lesser General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU Lesser General Public
|
|
20
|
+
License along with this library.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# OpenBadges 1.0 is a dead format (Mozilla Backpack shut down in 2019) and is
|
|
24
|
+
# deprecated: it will be removed in openbadgeslib 4.0.0 (see the "OpenBadges
|
|
25
|
+
# 1.0 lifecycle" wiki page). Reaching the OB1 API *through this package* —
|
|
26
|
+
# `from openbadgeslib.ob1 import Signer`, `openbadgeslib.ob1.Badge` — emits a
|
|
27
|
+
# DeprecationWarning. Migrate to openbadgeslib.ob2 (strict OB 2.0) or
|
|
28
|
+
# openbadgeslib.ob3 (OB 3.0).
|
|
29
|
+
#
|
|
30
|
+
# The symbols are resolved lazily (PEP 562) from their leaf modules, so the
|
|
31
|
+
# warning fires on the deprecated *package* surface only. Library internals
|
|
32
|
+
# that still need the shared, config-driven badge model import it straight
|
|
33
|
+
# from openbadgeslib.ob1.badge (BadgeImgType, Badge) and stay silent.
|
|
34
|
+
import importlib
|
|
35
|
+
import warnings
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
#: Public OB1 name -> the leaf submodule that defines it.
|
|
39
|
+
_OB1_API = {
|
|
40
|
+
'BadgeStatus': 'badge', 'BadgeImgType': 'badge', 'BadgeType': 'badge',
|
|
41
|
+
'Assertion': 'badge', 'Badge': 'badge', 'BadgeSigned': 'badge',
|
|
42
|
+
'extract_svg_assertion': 'badge', 'extract_png_assertion': 'badge',
|
|
43
|
+
'Signer': 'signer', 'Verifier': 'verifier', 'VerifyInfo': 'verifier',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
__all__ = list(_OB1_API)
|
|
47
|
+
|
|
48
|
+
_OB1_DEPRECATION = (
|
|
49
|
+
'openbadgeslib.ob1 (OpenBadges 1.0) is deprecated and will be removed in '
|
|
50
|
+
'openbadgeslib 4.0.0; migrate to openbadgeslib.ob2 (strict OB 2.0) or '
|
|
51
|
+
'openbadgeslib.ob3 (OB 3.0). See the "OpenBadges 1.0 lifecycle" wiki page.'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __getattr__(name: str) -> Any:
|
|
56
|
+
module = _OB1_API.get(name)
|
|
57
|
+
if module is None:
|
|
58
|
+
raise AttributeError(
|
|
59
|
+
'module %r has no attribute %r' % (__name__, name))
|
|
60
|
+
warnings.warn(_OB1_DEPRECATION, DeprecationWarning, stacklevel=2)
|
|
61
|
+
return getattr(importlib.import_module('.' + module, __name__), name)
|