openbadgeslib 1.3.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/Changelog.txt +69 -0
- {openbadgeslib-1.3.0/openbadgeslib.egg-info → openbadgeslib-3.0.0}/PKG-INFO +27 -32
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/README.md +26 -31
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/__init__.py +11 -2
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/badge.py +2 -2
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/baking.py +37 -24
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/config.ini.example +8 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/mail.py +1 -1
- openbadgeslib-3.0.0/openbadgeslib/ob2/__init__.py +41 -0
- openbadgeslib-3.0.0/openbadgeslib/ob2/models.py +376 -0
- openbadgeslib-3.0.0/openbadgeslib/ob2/signer.py +108 -0
- openbadgeslib-3.0.0/openbadgeslib/ob2/verifier.py +363 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/credential.py +84 -26
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/signer.py +43 -11
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/status.py +16 -2
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/verifier.py +40 -25
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_keygenerator.py +3 -3
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_publish.py +95 -2
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_signer.py +95 -6
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_verifier.py +93 -8
- openbadgeslib-3.0.0/openbadgeslib/signer.py +4 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/util.py +1 -1
- openbadgeslib-3.0.0/openbadgeslib/verifier.py +4 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0/openbadgeslib.egg-info}/PKG-INFO +27 -32
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/SOURCES.txt +12 -4
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_cli_json.py +11 -11
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_cli_smoke.py +29 -29
- openbadgeslib-1.3.0/tests/test_badge_io.py → openbadgeslib-3.0.0/tests/test_ob1_badge_io.py +10 -10
- openbadgeslib-1.3.0/tests/test_verify_operation.py → openbadgeslib-3.0.0/tests/test_ob1_verifier.py +12 -12
- openbadgeslib-3.0.0/tests/test_ob2_cli.py +215 -0
- openbadgeslib-3.0.0/tests/test_ob2_models.py +188 -0
- openbadgeslib-3.0.0/tests/test_ob2_signer.py +91 -0
- openbadgeslib-3.0.0/tests/test_ob2_verifier.py +233 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_credential.py +13 -7
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_signer.py +27 -8
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_status.py +29 -11
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_verifier.py +98 -24
- openbadgeslib-1.3.0/openbadgeslib/signer.py +0 -4
- openbadgeslib-1.3.0/openbadgeslib/verifier.py +0 -4
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/LICENSE.txt +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/MANIFEST.in +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/docs/README.md +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/_jws/__init__.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/_jws/exceptions.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/_jws/utils.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/confparser.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/errors.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/keys.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/logs.py +0 -0
- {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/__init__.py +0 -0
- {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/badge.py +0 -0
- {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/signer.py +0 -0
- {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/verifier.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/__init__.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/did.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_init.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/py.typed +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/requires.txt +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/top_level.txt +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/pyproject.toml +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/setup.cfg +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/config1.ini +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/conftest.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/images/sample1.png +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/images/sample1.svg +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/images/userimage01.svg +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/logo Python Espan/314/203a.svg" +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/logo Python Espa/303/261a.svg" +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/runtests.sh +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_confparser.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_docs.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_eddsa.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_jws.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_key_operation.py +0 -0
- /openbadgeslib-1.3.0/tests/test_signer_operation.py → /openbadgeslib-3.0.0/tests/test_ob1_signer.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_did.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_sign_ecc.pem +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_sign_rsa.pem +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_util.py +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_verify_ecc.pem +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_verify_rsa.pem +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/withoutxmlheader.svg +0 -0
- {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/withxmlheader.svg +0 -0
|
@@ -4,6 +4,75 @@ OpenBadgesLib - Changelog
|
|
|
4
4
|
Newest first. Dates are ISO 8601 (YYYY-MM-DD).
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
* v3.0.0 - 2026-07-01
|
|
8
|
+
|
|
9
|
+
- BREAKING: the pre-2.0 wire format previously shipped as `-V 2` is relabelled
|
|
10
|
+
OpenBadges 1.0 (`-V 1`) and frozen in the new `openbadgeslib.ob1` package (no
|
|
11
|
+
@context/type, a `uid`, a `verify` object, string `hashed`, Unix-timestamp
|
|
12
|
+
dates). `from openbadgeslib.ob2 import Signer/Verifier/Badge` no longer
|
|
13
|
+
resolves — import from `openbadgeslib.ob1` (the top-level
|
|
14
|
+
`openbadgeslib.signer` / `verifier` / `badge` shims still work).
|
|
15
|
+
|
|
16
|
+
- BREAKING: the default `-V` is now `3` (was `2`) for openbadges-signer,
|
|
17
|
+
-verifier, -publish and -keygenerator. Pass `-V 2` / `-V 1` explicitly for
|
|
18
|
+
the older generations.
|
|
19
|
+
|
|
20
|
+
- feat(ob2): new strict, spec-conformant Open Badges 2.0 implementation
|
|
21
|
+
(`openbadgeslib.ob2`). Assertions are valid JSON-LD Badge Objects with
|
|
22
|
+
`@context`, `type`, an IRI `id` (`urn:uuid:` for signed, the hosting URL for
|
|
23
|
+
hosted), a boolean `hashed`, ISO 8601 dates, and a `verification` object.
|
|
24
|
+
New `OB2Signer`, `OB2Verifier`, and dataclasses `Assertion`, `BadgeClass`,
|
|
25
|
+
`Profile`, `CryptographicKey`, `RevocationList`.
|
|
26
|
+
|
|
27
|
+
- feat(ob2): real HostedBadge verification — the assertion is fetched from its
|
|
28
|
+
own `id` over HTTPS and scope-checked against the issuer origin (default
|
|
29
|
+
same-origin, or the issuer's `startsWith` / `allowedOrigins`); the baked JWS
|
|
30
|
+
is non-gating defence-in-depth. Select it with `openbadges-signer -V 2 -H`.
|
|
31
|
+
|
|
32
|
+
- feat(ob2): SignedBadge verification resolves `verification.creator` to a
|
|
33
|
+
published `CryptographicKey` and checks its `owner` / `publicKey` back-link
|
|
34
|
+
to the issuer Profile.
|
|
35
|
+
|
|
36
|
+
- feat(publish): `openbadges-publish -V 2` emits conformant hosted metadata —
|
|
37
|
+
an issuer Profile with a `publicKey` array, a `BadgeClass` and a
|
|
38
|
+
`CryptographicKey` (`key.json`) per badge, and a `RevocationList`.
|
|
39
|
+
|
|
40
|
+
- feat(config): new optional badge keys `crypto_key` and
|
|
41
|
+
`hosted_assertions_base` for the OB 2.0 signed / hosted flows.
|
|
42
|
+
|
|
43
|
+
- OpenBadges 3.0 is unchanged.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
* v2.0.0 - 2026-07-01
|
|
47
|
+
|
|
48
|
+
- BREAKING (OB3): OpenBadges 3.0 credentials are now secured with the native
|
|
49
|
+
OB 3.0 VC-JWT (spec §8.2): the JWT payload IS the credential (its members at
|
|
50
|
+
the top level, no 'vc' claim wrapper), validFrom maps to 'nbf' (there is no
|
|
51
|
+
'iat'), and the JOSE header carries the issuer's public key as a 'jwk'.
|
|
52
|
+
Tokens issued by 1.x (the VCDM-1.1-style 'vc'-wrapper) are NOT compatible.
|
|
53
|
+
|
|
54
|
+
- BREAKING (OB3): baked images use the OB 3.0 document-format identifiers —
|
|
55
|
+
the PNG iTXt keyword 'openbadgecredential' and the SVG element
|
|
56
|
+
<openbadges:credential> (namespace https://purl.imsglobal.org/ob/v3p0). OB3
|
|
57
|
+
images baked by 1.x (OB2 identifiers) are not read by this verifier.
|
|
58
|
+
|
|
59
|
+
- OB3 verifier now accepts credentials that are valid per the spec schema but
|
|
60
|
+
were previously rejected: the AchievementCredential type alias, an issuer
|
|
61
|
+
given as a string IRI (not only a Profile object), and a credentialSubject
|
|
62
|
+
without an 'id' (identity conveyed via 'identifier').
|
|
63
|
+
|
|
64
|
+
- OB3 verifier now enforces required structure it previously ignored: the
|
|
65
|
+
@context (VC 2.0 + OB v3p0 pair) is validated, and the registered claims
|
|
66
|
+
'iss' and 'nbf' are required (with 'sub' required when the subject has an id).
|
|
67
|
+
|
|
68
|
+
- OB3 credentialStatus now honors statusPurpose: 'suspension' is reported
|
|
69
|
+
distinctly from 'revocation', a non-revocation/suspension purpose (e.g.
|
|
70
|
+
'message') no longer fails verification, and the entry's purpose is
|
|
71
|
+
cross-checked against the fetched status list's declared purpose.
|
|
72
|
+
|
|
73
|
+
- OpenBadges 2.0 is unchanged; only the OB3 path is affected.
|
|
74
|
+
|
|
75
|
+
|
|
7
76
|
* v1.3.0 - 2026-07-01
|
|
8
77
|
|
|
9
78
|
- SECURITY: download_file() now blocks server-side request forgery. The URLs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbadgeslib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: A library to sign and verify OpenBadges
|
|
5
5
|
Author-email: Luis González Fernández <luisgf@luisgf.es>, Jesús Cea Avión <jcea@jcea.es>
|
|
6
6
|
License: LGPLv3
|
|
@@ -48,12 +48,13 @@ Dynamic: license-file
|
|
|
48
48
|
|
|
49
49
|
A Python library and CLI for signing and verifying
|
|
50
50
|
[Open Badges](https://www.imsglobal.org/activity/digital-badges) embedded in SVG
|
|
51
|
-
and PNG image files. It supports
|
|
52
|
-
|
|
51
|
+
and PNG image files. It supports strict **OpenBadges 2.0** (JWS / hosted
|
|
52
|
+
assertions) and **OpenBadges 3.0** (W3C Verifiable Credentials / JWT-VC), plus a
|
|
53
|
+
frozen **OpenBadges 1.0** legacy format — selected with `-V {1,2,3}` (default `3`).
|
|
53
54
|
|
|
54
55
|
## Features
|
|
55
56
|
|
|
56
|
-
- Sign badge images (SVG and PNG)
|
|
57
|
+
- Sign badge images (SVG and PNG) as strict OB 2.0 JWS / hosted assertions (with a frozen OB 1.0 legacy format)
|
|
57
58
|
- Issue and verify OpenBadges 3.0 JWT-VC credentials
|
|
58
59
|
- Bake OB 3.0 JWT tokens into SVG and PNG badge images
|
|
59
60
|
- RSA 2048-bit (RS256), ECC NIST P-256 (ES256), and Ed25519 (EdDSA) key support
|
|
@@ -93,58 +94,52 @@ openbadges-init ./config/
|
|
|
93
94
|
# 2. Generate a key pair for a badge
|
|
94
95
|
openbadges-keygenerator -c ./config/config.ini -g 1
|
|
95
96
|
|
|
96
|
-
# 3a. Sign a badge — OpenBadges
|
|
97
|
+
# 3a. Sign a badge — OpenBadges 3.0 (default)
|
|
97
98
|
openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E
|
|
98
99
|
|
|
99
|
-
# 3b. Sign a badge — OpenBadges
|
|
100
|
-
openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E -V
|
|
100
|
+
# 3b. Sign a badge — strict OpenBadges 2.0
|
|
101
|
+
openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E -V 2
|
|
101
102
|
|
|
102
|
-
# 4a. Verify — OpenBadges
|
|
103
|
+
# 4a. Verify — OpenBadges 3.0
|
|
103
104
|
openbadges-verifier -i /tmp/badge_1_recipient@example.com.svg \
|
|
104
|
-
-r recipient@example.com -
|
|
105
|
+
-r recipient@example.com -V 3 -k ./config/keys/verify_rsa_key_1.pem
|
|
105
106
|
|
|
106
|
-
# 4b. Verify — OpenBadges
|
|
107
|
+
# 4b. Verify — strict OpenBadges 2.0 (pin a trusted key with -l/--local or -k/--pubkey)
|
|
107
108
|
openbadges-verifier -i /tmp/badge_1_recipient@example.com.svg \
|
|
108
|
-
-r recipient@example.com -V
|
|
109
|
+
-r recipient@example.com -V 2 -l 1
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
See the [Quick Start](https://github.com/luisgf/openbadgeslib/wiki/Quick-Start)
|
|
112
113
|
and [CLI Reference](https://github.com/luisgf/openbadgeslib/wiki/CLI-Reference)
|
|
113
114
|
wiki pages for the full walkthrough and every flag.
|
|
114
115
|
|
|
115
|
-
## Using the library — OpenBadges 2.0
|
|
116
|
+
## Using the library — OpenBadges 2.0 (strict)
|
|
116
117
|
|
|
117
118
|
```python
|
|
118
|
-
from
|
|
119
|
-
from openbadgeslib.
|
|
119
|
+
from datetime import datetime, timezone
|
|
120
|
+
from openbadgeslib.ob2 import OB2Signer, Assertion, IdentityObject, Verification
|
|
120
121
|
|
|
121
122
|
with open('sign.pem', 'rb') as f:
|
|
122
123
|
priv_pem = f.read()
|
|
123
|
-
with open('verify.pem', 'rb') as f:
|
|
124
|
-
pub_pem = f.read()
|
|
125
124
|
with open('badge.svg', 'rb') as f:
|
|
126
125
|
image = f.read()
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
image_url='https://example.com/badge.svg',
|
|
135
|
-
criteria_url='https://example.com/criteria.html',
|
|
136
|
-
json_url='https://example.com/badge.json',
|
|
137
|
-
verify_key_url='https://example.com/verify.pem',
|
|
138
|
-
key_type=KeyType.RSA,
|
|
139
|
-
privkey_pem=priv_pem,
|
|
140
|
-
pubkey_pem=pub_pem,
|
|
127
|
+
assertion = Assertion(
|
|
128
|
+
recipient=IdentityObject.create('recipient@example.com', salt='s4lt3d'),
|
|
129
|
+
badge='https://example.com/badge_1/badge.json',
|
|
130
|
+
verification=Verification(type='SignedBadge',
|
|
131
|
+
creator='https://example.com/badge_1/key.json'),
|
|
132
|
+
issued_on=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
|
141
133
|
)
|
|
142
134
|
|
|
143
|
-
signer =
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
signer = OB2Signer(privkey_pem=priv_pem, algorithm='RS256')
|
|
136
|
+
baked_svg = signer.sign_into_svg(assertion, image)
|
|
137
|
+
with open('/tmp/signed_badge.svg', 'wb') as f:
|
|
138
|
+
f.write(baked_svg)
|
|
146
139
|
```
|
|
147
140
|
|
|
141
|
+
For the frozen OpenBadges 1.0 legacy API (`Badge` / `Signer` / `Verifier`), import from `openbadgeslib.ob1` instead.
|
|
142
|
+
|
|
148
143
|
## Using the library — OpenBadges 3.0 (JWT-VC)
|
|
149
144
|
|
|
150
145
|
```python
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
A Python library and CLI for signing and verifying
|
|
9
9
|
[Open Badges](https://www.imsglobal.org/activity/digital-badges) embedded in SVG
|
|
10
|
-
and PNG image files. It supports
|
|
11
|
-
|
|
10
|
+
and PNG image files. It supports strict **OpenBadges 2.0** (JWS / hosted
|
|
11
|
+
assertions) and **OpenBadges 3.0** (W3C Verifiable Credentials / JWT-VC), plus a
|
|
12
|
+
frozen **OpenBadges 1.0** legacy format — selected with `-V {1,2,3}` (default `3`).
|
|
12
13
|
|
|
13
14
|
## Features
|
|
14
15
|
|
|
15
|
-
- Sign badge images (SVG and PNG)
|
|
16
|
+
- Sign badge images (SVG and PNG) as strict OB 2.0 JWS / hosted assertions (with a frozen OB 1.0 legacy format)
|
|
16
17
|
- Issue and verify OpenBadges 3.0 JWT-VC credentials
|
|
17
18
|
- Bake OB 3.0 JWT tokens into SVG and PNG badge images
|
|
18
19
|
- RSA 2048-bit (RS256), ECC NIST P-256 (ES256), and Ed25519 (EdDSA) key support
|
|
@@ -52,58 +53,52 @@ openbadges-init ./config/
|
|
|
52
53
|
# 2. Generate a key pair for a badge
|
|
53
54
|
openbadges-keygenerator -c ./config/config.ini -g 1
|
|
54
55
|
|
|
55
|
-
# 3a. Sign a badge — OpenBadges
|
|
56
|
+
# 3a. Sign a badge — OpenBadges 3.0 (default)
|
|
56
57
|
openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E
|
|
57
58
|
|
|
58
|
-
# 3b. Sign a badge — OpenBadges
|
|
59
|
-
openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E -V
|
|
59
|
+
# 3b. Sign a badge — strict OpenBadges 2.0
|
|
60
|
+
openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E -V 2
|
|
60
61
|
|
|
61
|
-
# 4a. Verify — OpenBadges
|
|
62
|
+
# 4a. Verify — OpenBadges 3.0
|
|
62
63
|
openbadges-verifier -i /tmp/badge_1_recipient@example.com.svg \
|
|
63
|
-
-r recipient@example.com -
|
|
64
|
+
-r recipient@example.com -V 3 -k ./config/keys/verify_rsa_key_1.pem
|
|
64
65
|
|
|
65
|
-
# 4b. Verify — OpenBadges
|
|
66
|
+
# 4b. Verify — strict OpenBadges 2.0 (pin a trusted key with -l/--local or -k/--pubkey)
|
|
66
67
|
openbadges-verifier -i /tmp/badge_1_recipient@example.com.svg \
|
|
67
|
-
-r recipient@example.com -V
|
|
68
|
+
-r recipient@example.com -V 2 -l 1
|
|
68
69
|
```
|
|
69
70
|
|
|
70
71
|
See the [Quick Start](https://github.com/luisgf/openbadgeslib/wiki/Quick-Start)
|
|
71
72
|
and [CLI Reference](https://github.com/luisgf/openbadgeslib/wiki/CLI-Reference)
|
|
72
73
|
wiki pages for the full walkthrough and every flag.
|
|
73
74
|
|
|
74
|
-
## Using the library — OpenBadges 2.0
|
|
75
|
+
## Using the library — OpenBadges 2.0 (strict)
|
|
75
76
|
|
|
76
77
|
```python
|
|
77
|
-
from
|
|
78
|
-
from openbadgeslib.
|
|
78
|
+
from datetime import datetime, timezone
|
|
79
|
+
from openbadgeslib.ob2 import OB2Signer, Assertion, IdentityObject, Verification
|
|
79
80
|
|
|
80
81
|
with open('sign.pem', 'rb') as f:
|
|
81
82
|
priv_pem = f.read()
|
|
82
|
-
with open('verify.pem', 'rb') as f:
|
|
83
|
-
pub_pem = f.read()
|
|
84
83
|
with open('badge.svg', 'rb') as f:
|
|
85
84
|
image = f.read()
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
image_url='https://example.com/badge.svg',
|
|
94
|
-
criteria_url='https://example.com/criteria.html',
|
|
95
|
-
json_url='https://example.com/badge.json',
|
|
96
|
-
verify_key_url='https://example.com/verify.pem',
|
|
97
|
-
key_type=KeyType.RSA,
|
|
98
|
-
privkey_pem=priv_pem,
|
|
99
|
-
pubkey_pem=pub_pem,
|
|
86
|
+
assertion = Assertion(
|
|
87
|
+
recipient=IdentityObject.create('recipient@example.com', salt='s4lt3d'),
|
|
88
|
+
badge='https://example.com/badge_1/badge.json',
|
|
89
|
+
verification=Verification(type='SignedBadge',
|
|
90
|
+
creator='https://example.com/badge_1/key.json'),
|
|
91
|
+
issued_on=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
|
100
92
|
)
|
|
101
93
|
|
|
102
|
-
signer =
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
signer = OB2Signer(privkey_pem=priv_pem, algorithm='RS256')
|
|
95
|
+
baked_svg = signer.sign_into_svg(assertion, image)
|
|
96
|
+
with open('/tmp/signed_badge.svg', 'wb') as f:
|
|
97
|
+
f.write(baked_svg)
|
|
105
98
|
```
|
|
106
99
|
|
|
100
|
+
For the frozen OpenBadges 1.0 legacy API (`Badge` / `Signer` / `Verifier`), import from `openbadgeslib.ob1` instead.
|
|
101
|
+
|
|
107
102
|
## Using the library — OpenBadges 3.0 (JWT-VC)
|
|
108
103
|
|
|
109
104
|
```python
|
|
@@ -21,14 +21,23 @@
|
|
|
21
21
|
License along with this library.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
# ── OpenBadges
|
|
25
|
-
|
|
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
|
|
26
30
|
Signer, Verifier, VerifyInfo,
|
|
27
31
|
Badge, BadgeSigned, Assertion,
|
|
28
32
|
BadgeStatus, BadgeImgType, BadgeType,
|
|
29
33
|
extract_svg_assertion, extract_png_assertion,
|
|
30
34
|
)
|
|
31
35
|
|
|
36
|
+
# ── OpenBadges 2.0 (strict) ──────────────────────────────────────────────────
|
|
37
|
+
from .ob2 import ( # noqa: F401
|
|
38
|
+
OB2Signer, OB2Verifier, OB2VerificationError,
|
|
39
|
+
)
|
|
40
|
+
|
|
32
41
|
# ── OpenBadges 3.0 ─────────────────────────────────────────────────────────────
|
|
33
42
|
from .ob3 import ( # noqa: F401
|
|
34
43
|
OB3Signer, OB3Verifier, OB3VerificationError,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""OpenBadges
|
|
2
|
-
from .
|
|
1
|
+
"""OpenBadges 1.0 (legacy) badge objects — compatibility shim, re-exports from openbadgeslib.ob1."""
|
|
2
|
+
from .ob1.badge import (
|
|
3
3
|
BadgeStatus, BadgeImgType, BadgeType,
|
|
4
4
|
Assertion, Badge, BadgeSigned,
|
|
5
5
|
extract_svg_assertion, extract_png_assertion,
|
|
@@ -34,8 +34,16 @@ from typing import List, Optional, Tuple, Union
|
|
|
34
34
|
from defusedxml.minidom import parseString
|
|
35
35
|
from png import Reader, signature as _png_signature
|
|
36
36
|
|
|
37
|
+
# OB 2.0 document-format identifiers.
|
|
37
38
|
ITXT_KEYWORD = b'openbadges'
|
|
38
39
|
SVG_ELEMENT = 'openbadges:assertion'
|
|
40
|
+
SVG_NS = 'http://openbadges.org'
|
|
41
|
+
|
|
42
|
+
# OB 3.0 document-format identifiers (differ from OB 2.0). Selected via the
|
|
43
|
+
# keyword-only args below so OB2 and OB3 bake/extract their own carriers.
|
|
44
|
+
ITXT_KEYWORD_OB3 = b'openbadgecredential'
|
|
45
|
+
SVG_ELEMENT_OB3 = 'openbadges:credential'
|
|
46
|
+
SVG_NS_OB3 = 'https://purl.imsglobal.org/ob/v3p0'
|
|
39
47
|
|
|
40
48
|
# Maximum bytes a compressed iTXt token is allowed to inflate to. A JWS/JWT-VC
|
|
41
49
|
# is a few KB; this cap stops a crafted zlib bomb from exhausting memory during
|
|
@@ -47,9 +55,9 @@ class DecompressionLimitExceeded(Exception):
|
|
|
47
55
|
"""Raised when a compressed iTXt token inflates beyond the allowed size."""
|
|
48
56
|
|
|
49
57
|
|
|
50
|
-
def _split_openbadges_itxt(data: bytes) -> Optional[bytes]:
|
|
51
|
-
|
|
52
|
-
if sep != b'\x00' or
|
|
58
|
+
def _split_openbadges_itxt(data: bytes, keyword: bytes = ITXT_KEYWORD) -> Optional[bytes]:
|
|
59
|
+
chunk_keyword, sep, rest = data.partition(b'\x00')
|
|
60
|
+
if sep != b'\x00' or chunk_keyword != keyword or len(rest) < 2:
|
|
53
61
|
return None
|
|
54
62
|
return rest
|
|
55
63
|
|
|
@@ -66,14 +74,16 @@ def _bounded_inflate(data: bytes, limit: int = MAX_ITXT_DECOMPRESSED) -> bytes:
|
|
|
66
74
|
|
|
67
75
|
# ── SVG ─────────────────────────────────────────────────────────────────────
|
|
68
76
|
|
|
69
|
-
def bake_svg(image_bytes: bytes, token: str, comment: Optional[str] = None
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
def bake_svg(image_bytes: bytes, token: str, comment: Optional[str] = None, *,
|
|
78
|
+
element: str = SVG_ELEMENT, namespace: str = SVG_NS) -> bytes:
|
|
79
|
+
"""Return *image_bytes* with an ``<element verify=token>`` node (and an
|
|
80
|
+
optional XML comment) appended to the root ``<svg>``. *element*/*namespace*
|
|
81
|
+
default to the OB 2.0 identifiers; OB 3.0 passes its own."""
|
|
72
82
|
svg_doc = parseString(image_bytes)
|
|
73
83
|
try:
|
|
74
84
|
svg_tag = svg_doc.getElementsByTagName('svg').item(0)
|
|
75
|
-
node = svg_doc.createElement(
|
|
76
|
-
node.attributes['xmlns:openbadges'] =
|
|
85
|
+
node = svg_doc.createElement(element)
|
|
86
|
+
node.attributes['xmlns:openbadges'] = namespace
|
|
77
87
|
node.attributes['verify'] = token
|
|
78
88
|
svg_tag.appendChild(node)
|
|
79
89
|
if comment:
|
|
@@ -83,24 +93,24 @@ def bake_svg(image_bytes: bytes, token: str, comment: Optional[str] = None) -> b
|
|
|
83
93
|
svg_doc.unlink()
|
|
84
94
|
|
|
85
95
|
|
|
86
|
-
def has_svg(image_bytes: bytes) -> bool:
|
|
87
|
-
"""Return True if *image_bytes* already carries an
|
|
96
|
+
def has_svg(image_bytes: bytes, *, element: str = SVG_ELEMENT) -> bool:
|
|
97
|
+
"""Return True if *image_bytes* already carries an *element* node."""
|
|
88
98
|
svg_doc = parseString(image_bytes)
|
|
89
99
|
try:
|
|
90
|
-
return bool(svg_doc.getElementsByTagName(
|
|
100
|
+
return bool(svg_doc.getElementsByTagName(element))
|
|
91
101
|
finally:
|
|
92
102
|
svg_doc.unlink()
|
|
93
103
|
|
|
94
104
|
|
|
95
|
-
def extract_svg(image_bytes: bytes) -> Optional[str]:
|
|
96
|
-
"""Return the embedded token string, or None if there is no
|
|
105
|
+
def extract_svg(image_bytes: bytes, *, element: str = SVG_ELEMENT) -> Optional[str]:
|
|
106
|
+
"""Return the embedded token string, or None if there is no *element* node.
|
|
97
107
|
|
|
98
108
|
Raises on malformed XML (left to the caller to map to its own error type).
|
|
99
109
|
"""
|
|
100
110
|
svg_doc = None
|
|
101
111
|
try:
|
|
102
112
|
svg_doc = parseString(image_bytes)
|
|
103
|
-
nodes = svg_doc.getElementsByTagName(
|
|
113
|
+
nodes = svg_doc.getElementsByTagName(element)
|
|
104
114
|
if not nodes:
|
|
105
115
|
return None
|
|
106
116
|
return nodes[0].attributes['verify'].nodeValue
|
|
@@ -124,28 +134,31 @@ def _serialize_png(chunks: List[Tuple[Union[str, bytes], bytes]]) -> bytes:
|
|
|
124
134
|
return out
|
|
125
135
|
|
|
126
136
|
|
|
127
|
-
def bake_png(image_bytes: bytes, token: str, text_comment: Optional[str] = None
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
def bake_png(image_bytes: bytes, token: str, text_comment: Optional[str] = None, *,
|
|
138
|
+
keyword: bytes = ITXT_KEYWORD) -> bytes:
|
|
139
|
+
"""Return *image_bytes* with the token stored in a *keyword* iTXt chunk (and
|
|
140
|
+
an optional ``tEXt`` comment chunk) inserted before IEND. *keyword* defaults
|
|
141
|
+
to the OB 2.0 identifier; OB 3.0 passes ``openbadgecredential``."""
|
|
130
142
|
chunks = list(Reader(bytes=image_bytes).chunks())
|
|
131
|
-
itxt_data =
|
|
143
|
+
itxt_data = keyword + pack('BBBBB', 0, 0, 0, 0, 0) + token.encode('utf-8')
|
|
132
144
|
chunks.insert(len(chunks) - 1, ('iTXt', itxt_data))
|
|
133
145
|
if text_comment:
|
|
134
146
|
chunks.insert(len(chunks) - 1, ('tEXt', text_comment.encode('utf-8')))
|
|
135
147
|
return _serialize_png(chunks)
|
|
136
148
|
|
|
137
149
|
|
|
138
|
-
def has_png(image_bytes: bytes) -> bool:
|
|
139
|
-
"""Return True if *image_bytes* already carries
|
|
150
|
+
def has_png(image_bytes: bytes, *, keyword: bytes = ITXT_KEYWORD) -> bool:
|
|
151
|
+
"""Return True if *image_bytes* already carries a *keyword* iTXt chunk."""
|
|
140
152
|
for tag, data in Reader(bytes=image_bytes).chunks():
|
|
141
153
|
tag_str = tag.decode('ascii') if isinstance(tag, bytes) else tag
|
|
142
|
-
if tag_str == 'iTXt' and _split_openbadges_itxt(data) is not None:
|
|
154
|
+
if tag_str == 'iTXt' and _split_openbadges_itxt(data, keyword) is not None:
|
|
143
155
|
return True
|
|
144
156
|
return False
|
|
145
157
|
|
|
146
158
|
|
|
147
|
-
def extract_png(image_bytes: bytes, max_decompressed: int = MAX_ITXT_DECOMPRESSED
|
|
148
|
-
|
|
159
|
+
def extract_png(image_bytes: bytes, max_decompressed: int = MAX_ITXT_DECOMPRESSED, *,
|
|
160
|
+
keyword: bytes = ITXT_KEYWORD) -> Optional[str]:
|
|
161
|
+
"""Return the embedded token string, or None if there is no *keyword*
|
|
149
162
|
iTXt chunk.
|
|
150
163
|
|
|
151
164
|
Parses the iTXt structure (keyword, compression flag/method, language tag,
|
|
@@ -159,7 +172,7 @@ def extract_png(image_bytes: bytes, max_decompressed: int = MAX_ITXT_DECOMPRESSE
|
|
|
159
172
|
if tag_str != 'iTXt':
|
|
160
173
|
continue
|
|
161
174
|
|
|
162
|
-
rest = _split_openbadges_itxt(data)
|
|
175
|
+
rest = _split_openbadges_itxt(data, keyword)
|
|
163
176
|
if rest is None:
|
|
164
177
|
continue
|
|
165
178
|
compression_flag = rest[0]
|
|
@@ -42,6 +42,12 @@ image = https://www.issuer.badge/issuer/badge_1/badge1.svg
|
|
|
42
42
|
criteria = https://www.issuer.badge/issuer/badge_1/criteria.html
|
|
43
43
|
verify_key = https://www.issuer.badge/issuer/badge_1/verify_rsa_key.pem
|
|
44
44
|
badge = https://www.issuer.badge/issuer/badge_1/badge.json
|
|
45
|
+
; OpenBadges 2.0 (-V 2): URL of this badge's CryptographicKey JSON-LD document
|
|
46
|
+
; (published by openbadges-publish as <badge>/key.json). Used as verification.creator.
|
|
47
|
+
crypto_key = https://www.issuer.badge/issuer/badge_1/key.json
|
|
48
|
+
; OpenBadges 2.0 hosted mode (openbadges-signer -V 2 -H): base URL under which
|
|
49
|
+
; per-recipient hosted assertion JSON files are published for verification.
|
|
50
|
+
hosted_assertions_base = https://www.issuer.badge/issuer/badge_1/assertions/
|
|
45
51
|
private_key = ${paths:base_key}/sign_rsa_key_1.pem
|
|
46
52
|
public_key = ${paths:base_key}/verify_rsa_key_1.pem
|
|
47
53
|
; key_type selects the algorithm for openbadges-keygenerator: RSA (default), ECC, or ED25519
|
|
@@ -58,6 +64,8 @@ image = https://www.issuer.badge/issuer/badge_2/badge2.svg
|
|
|
58
64
|
criteria = https://www.issuer.badge/issuer/badge_2/criteria.html
|
|
59
65
|
verify_key = https://www.issuer.badge/issuer/badge_2/verify_rsa_key.pem
|
|
60
66
|
badge = https://www.issuer.badge/issuer/badge_2/badge.json
|
|
67
|
+
crypto_key = https://www.issuer.badge/issuer/badge_2/key.json
|
|
68
|
+
hosted_assertions_base = https://www.issuer.badge/issuer/badge_2/assertions/
|
|
61
69
|
private_key = ${paths:base_key}/sign_rsa_key_2.pem
|
|
62
70
|
public_key = ${paths:base_key}/verify_rsa_key_2.pem
|
|
63
71
|
key_type = RSA
|
|
@@ -30,7 +30,7 @@ from email.mime.multipart import MIMEMultipart
|
|
|
30
30
|
from email.mime.text import MIMEText
|
|
31
31
|
from email.utils import formatdate
|
|
32
32
|
from email.header import Header
|
|
33
|
-
from .
|
|
33
|
+
from .ob1 import BadgeImgType
|
|
34
34
|
from .errors import BadgeImgFormatUnsupported
|
|
35
35
|
|
|
36
36
|
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
# Strict OpenBadges 2.0 (JWS-signed / hosted Assertions with conformant
|
|
24
|
+
# JSON-LD Badge Objects). The legacy pre-2.0 format lives in openbadgeslib.ob1.
|
|
25
|
+
|
|
26
|
+
from .models import (
|
|
27
|
+
OB2_CONTEXT,
|
|
28
|
+
Assertion, IdentityObject, Verification,
|
|
29
|
+
BadgeClass, Profile, CryptographicKey, RevocationList,
|
|
30
|
+
hash_identity,
|
|
31
|
+
)
|
|
32
|
+
from .signer import OB2Signer
|
|
33
|
+
from .verifier import OB2Verifier, OB2VerificationError
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'OB2_CONTEXT',
|
|
37
|
+
'Assertion', 'IdentityObject', 'Verification',
|
|
38
|
+
'BadgeClass', 'Profile', 'CryptographicKey', 'RevocationList',
|
|
39
|
+
'hash_identity',
|
|
40
|
+
'OB2Signer', 'OB2Verifier', 'OB2VerificationError',
|
|
41
|
+
]
|