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.
Files changed (85) hide show
  1. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/Changelog.txt +69 -0
  2. {openbadgeslib-1.3.0/openbadgeslib.egg-info → openbadgeslib-3.0.0}/PKG-INFO +27 -32
  3. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/README.md +26 -31
  4. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/__init__.py +11 -2
  5. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/badge.py +2 -2
  6. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/baking.py +37 -24
  7. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/config.ini.example +8 -0
  8. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/mail.py +1 -1
  9. openbadgeslib-3.0.0/openbadgeslib/ob2/__init__.py +41 -0
  10. openbadgeslib-3.0.0/openbadgeslib/ob2/models.py +376 -0
  11. openbadgeslib-3.0.0/openbadgeslib/ob2/signer.py +108 -0
  12. openbadgeslib-3.0.0/openbadgeslib/ob2/verifier.py +363 -0
  13. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/credential.py +84 -26
  14. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/signer.py +43 -11
  15. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/status.py +16 -2
  16. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/verifier.py +40 -25
  17. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_keygenerator.py +3 -3
  18. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_publish.py +95 -2
  19. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_signer.py +95 -6
  20. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_verifier.py +93 -8
  21. openbadgeslib-3.0.0/openbadgeslib/signer.py +4 -0
  22. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/util.py +1 -1
  23. openbadgeslib-3.0.0/openbadgeslib/verifier.py +4 -0
  24. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0/openbadgeslib.egg-info}/PKG-INFO +27 -32
  25. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/SOURCES.txt +12 -4
  26. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_cli_json.py +11 -11
  27. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_cli_smoke.py +29 -29
  28. openbadgeslib-1.3.0/tests/test_badge_io.py → openbadgeslib-3.0.0/tests/test_ob1_badge_io.py +10 -10
  29. openbadgeslib-1.3.0/tests/test_verify_operation.py → openbadgeslib-3.0.0/tests/test_ob1_verifier.py +12 -12
  30. openbadgeslib-3.0.0/tests/test_ob2_cli.py +215 -0
  31. openbadgeslib-3.0.0/tests/test_ob2_models.py +188 -0
  32. openbadgeslib-3.0.0/tests/test_ob2_signer.py +91 -0
  33. openbadgeslib-3.0.0/tests/test_ob2_verifier.py +233 -0
  34. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_credential.py +13 -7
  35. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_signer.py +27 -8
  36. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_status.py +29 -11
  37. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_verifier.py +98 -24
  38. openbadgeslib-1.3.0/openbadgeslib/signer.py +0 -4
  39. openbadgeslib-1.3.0/openbadgeslib/verifier.py +0 -4
  40. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/LICENSE.txt +0 -0
  41. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/MANIFEST.in +0 -0
  42. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/docs/README.md +0 -0
  43. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/_jws/__init__.py +0 -0
  44. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/_jws/exceptions.py +0 -0
  45. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/_jws/utils.py +0 -0
  46. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/confparser.py +0 -0
  47. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/errors.py +0 -0
  48. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/keys.py +0 -0
  49. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/logs.py +0 -0
  50. {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/__init__.py +0 -0
  51. {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/badge.py +0 -0
  52. {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/signer.py +0 -0
  53. {openbadgeslib-1.3.0/openbadgeslib/ob2 → openbadgeslib-3.0.0/openbadgeslib/ob1}/verifier.py +0 -0
  54. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/__init__.py +0 -0
  55. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/ob3/did.py +0 -0
  56. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/openbadges_init.py +0 -0
  57. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib/py.typed +0 -0
  58. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/dependency_links.txt +0 -0
  59. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/entry_points.txt +0 -0
  60. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/requires.txt +0 -0
  61. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/openbadgeslib.egg-info/top_level.txt +0 -0
  62. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/pyproject.toml +0 -0
  63. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/setup.cfg +0 -0
  64. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/config1.ini +0 -0
  65. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/conftest.py +0 -0
  66. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/images/sample1.png +0 -0
  67. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/images/sample1.svg +0 -0
  68. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/images/userimage01.svg +0 -0
  69. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/logo Python Espan/314/203a.svg" +0 -0
  70. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/logo Python Espa/303/261a.svg" +0 -0
  71. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/runtests.sh +0 -0
  72. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_confparser.py +0 -0
  73. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_docs.py +0 -0
  74. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_eddsa.py +0 -0
  75. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_jws.py +0 -0
  76. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_key_operation.py +0 -0
  77. /openbadgeslib-1.3.0/tests/test_signer_operation.py → /openbadgeslib-3.0.0/tests/test_ob1_signer.py +0 -0
  78. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_ob3_did.py +0 -0
  79. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_sign_ecc.pem +0 -0
  80. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_sign_rsa.pem +0 -0
  81. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_util.py +0 -0
  82. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_verify_ecc.pem +0 -0
  83. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/test_verify_rsa.pem +0 -0
  84. {openbadgeslib-1.3.0 → openbadgeslib-3.0.0}/tests/withoutxmlheader.svg +0 -0
  85. {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: 1.3.0
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 both **OpenBadges 2.0** (JWS compact
52
- serialisation) and **OpenBadges 3.0** (W3C Verifiable Credentials / JWT-VC).
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) with a JWS assertion (OB 2.0)
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 2.0 (default)
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 3.0
100
- openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E -V 3
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 2.0 (pin a trusted key with -l/--local or -k/--pubkey)
103
+ # 4a. Verify — OpenBadges 3.0
103
104
  openbadges-verifier -i /tmp/badge_1_recipient@example.com.svg \
104
- -r recipient@example.com -l 1
105
+ -r recipient@example.com -V 3 -k ./config/keys/verify_rsa_key_1.pem
105
106
 
106
- # 4b. Verify — OpenBadges 3.0
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 3 -k ./config/keys/verify_rsa_key_1.pem
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 openbadgeslib.ob2 import Badge, BadgeImgType, BadgeType, Signer
119
- from openbadgeslib.keys import KeyType
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
- badge = Badge(
129
- ini_name='my_badge',
130
- name='My Badge',
131
- description='Awarded for excellence',
132
- image_type=BadgeImgType.SVG,
133
- image=image,
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 = Signer(identity='recipient@example.com', badge_type=BadgeType.SIGNED)
144
- signed = signer.sign_badge(badge)
145
- signed.save_to_file('/tmp/signed_badge.svg')
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 both **OpenBadges 2.0** (JWS compact
11
- serialisation) and **OpenBadges 3.0** (W3C Verifiable Credentials / JWT-VC).
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) with a JWS assertion (OB 2.0)
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 2.0 (default)
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 3.0
59
- openbadges-signer -c ./config/config.ini -b 1 -r recipient@example.com -o /tmp/ -E -V 3
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 2.0 (pin a trusted key with -l/--local or -k/--pubkey)
62
+ # 4a. Verify — OpenBadges 3.0
62
63
  openbadges-verifier -i /tmp/badge_1_recipient@example.com.svg \
63
- -r recipient@example.com -l 1
64
+ -r recipient@example.com -V 3 -k ./config/keys/verify_rsa_key_1.pem
64
65
 
65
- # 4b. Verify — OpenBadges 3.0
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 3 -k ./config/keys/verify_rsa_key_1.pem
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 openbadgeslib.ob2 import Badge, BadgeImgType, BadgeType, Signer
78
- from openbadgeslib.keys import KeyType
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
- badge = Badge(
88
- ini_name='my_badge',
89
- name='My Badge',
90
- description='Awarded for excellence',
91
- image_type=BadgeImgType.SVG,
92
- image=image,
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 = Signer(identity='recipient@example.com', badge_type=BadgeType.SIGNED)
103
- signed = signer.sign_badge(badge)
104
- signed.save_to_file('/tmp/signed_badge.svg')
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 2.0 ─────────────────────────────────────────────────────────────
25
- from .ob2 import ( # noqa: F401
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.0 badge objects — compatibility shim, re-exports from openbadgeslib.ob2."""
2
- from .ob2.badge import (
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
- keyword, sep, rest = data.partition(b'\x00')
52
- if sep != b'\x00' or keyword != ITXT_KEYWORD or len(rest) < 2:
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) -> bytes:
70
- """Return *image_bytes* with an ``<openbadges:assertion verify=token>``
71
- element (and an optional XML comment) appended to the root ``<svg>``."""
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(SVG_ELEMENT)
76
- node.attributes['xmlns:openbadges'] = 'http://openbadges.org'
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 OpenBadges assertion."""
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(SVG_ELEMENT))
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 assertion node.
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(SVG_ELEMENT)
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) -> bytes:
128
- """Return *image_bytes* with the token stored in an ``openbadges`` iTXt
129
- chunk (and an optional ``tEXt`` comment chunk) inserted before IEND."""
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 = ITXT_KEYWORD + pack('BBBBB', 0, 0, 0, 0, 0) + token.encode('utf-8')
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 an OpenBadges iTXt chunk."""
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) -> Optional[str]:
148
- """Return the embedded token string, or None if there is no openbadges
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 .ob2 import BadgeImgType
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
+ ]