-
Notifications
You must be signed in to change notification settings - Fork 1
Signing and Verification
This page explains, end to end, how openbadgeslib signs an Open Badge and how it later proves that signature is genuine. It covers both badge generations — OB2 (a detached JWS) and OB3 (a JWT-VC) — and the shared "baking" step that hides the signed token inside an SVG or PNG image so a single picture file is the verifiable credential.
For the trust assumptions behind all of this (which key is trusted, why downloaded keys are not), see Security Model. For key types, formats and the exception hierarchy, see Keys and Errors.
Both versions follow the same three-stage pipeline:
-
Build a token — a signed string. OB2 builds a compact JWS over a badge assertion; OB3 builds a JWT-VC whose payload carries a W3C Verifiable Credential under a
vcclaim. -
Bake it into an image — the token is written into the badge image as an SVG element or a PNG chunk, using the shared
openbadgeslib.bakingmodule. OB2 and OB3 use the exact same on-disk carrier format, so any conformant viewer can pull the token out regardless of which version produced it. - Reverse it on verify — extract the token from the image, then cryptographically check the signature with a trusted public key and report a status.
The signing algorithm is bound to the key type, in both directions: an RSA key produces and verifies RS256, an EC key produces and verifies ES256. The verifier never lets the token header choose the algorithm — see Security Model for why that matters.
The low-level JWS engine in openbadgeslib/_jws/__init__.py is backed by PyJWT's algorithm implementations and registers six algorithms:
| Key family | Algorithms |
|---|---|
| RSA |
RS256, RS384, RS512
|
| ECC |
ES256, ES384, ES512
|
In practice the signers emit the 256-bit variant — RS256 for RSA, ES256 for ECC. The wider family is accepted on verification for interoperability, but every verifier pins the allowed set to the key's own type, so an RSA key can never validate an ES* token and vice versa. There is deliberately no symmetric (HS*) or alg: none entry.
For OB2 the unit of trust is a signed assertion serialized as a JWS. The work happens in openbadgeslib/ob2/signer.py.
Signer.generate_jws() assembles two dicts — a JOSE header and a payload:
- The header's
algcomes from the key type (alg_for_key_type(...)). - The payload is the OpenBadges assertion: a
uid, a hashedrecipient(sha256$…of the email plus a salt,hashed: true),image,badge, anissuedOntimestamp, and averifyblock. Theverifyblock istype: signedpointing at the public-key URL by default, ortype: hostedpointing at the badge JSON URL whenbadge_typeisBadgeType.HOSTED. Optionalexpiresandevidenceare added when present.
generate_assertion() then signs header + payload via _jws.sign(...) and stores the base64url-encoded header, body and signature on an Assertion object. The signature is computed over base64url(header) . base64url(payload).
from openbadgeslib.ob2.signer import Signer
signer = Signer(identity='recipient@example.org')
signed = signer.sign_badge(badge_obj) # raises ErrorSigningFile if already signed
# signed.signed holds the baked image bytesA determinism switch exists for reproducible output: Signer(deterministic=True) fixes the salt and zeroes uid/issuedOn so the same inputs produce byte-identical badges (useful for tests).
Verification lives in openbadgeslib/ob2/verifier.py. Verifier.get_badge_status() runs the full gauntlet in order and returns a VerifyInfo(status, msg):
-
Signature —
check_jws_signature()calls_jws.verify_block(...). It verifies against the operator-supplied trusted key when one was given, and only falls back to the key the badge points to when none was. -
Revocation —
check_revocation()walks badge JSON → issuer JSON → optionalrevocationList. A missing list simply means "not revoked". -
Expiration —
check_expiration()comparesexpiresagainst now. -
Identity —
check_identity()re-hashes the supplied email with the badge salt and compares to the assertion's recipient. With no identity supplied it skips this step (signature-only verification).
from openbadgeslib.ob2.verifier import Verifier
v = Verifier(verify_key=trusted_pub_key, identity='recipient@example.org')
info = v.get_badge_status(badge)
print(info.status, info.msg)See Python API OB2 for the full object model and CLI Reference for the openbadges-signer / openbadges-verifier commands.
For OB3 the unit of trust is a JWT-VC — a JWT whose payload nests a W3C Verifiable Credential under the vc claim. Signing is in openbadgeslib/ob3/signer.py.
OB3Signer.sign() calls credential.to_jwt_payload() and hands it to PyJWT's jwt.encode(...) with the chosen algorithm (default RS256; RSA keys must use RS*, EC keys ES*). The result is a compact header.payload.signature JWT string.
from openbadgeslib.ob3.signer import OB3Signer
signer = OB3Signer(privkey_pem, algorithm='ES256') # ES256 for an EC key
token = signer.sign(credential) # compact JWT-VC string
svg = signer.sign_into_svg(credential, svg_bytes) # or sign_into_png(...)Verification is in openbadgeslib/ob3/verifier.py. OB3Verifier.verify():
-
Reads the header and rejects the token unless its
algis in the set allowed for this key's type (RS*for RSA,ES*for ECC) — the token cannot pick its own algorithm. -
Decodes with
jwt.decode(...), which checks the signature and expiry (ExpiredSignatureError→OB3VerificationError("Credential has expired")). -
Validates structure — the payload must carry a
vcclaim whosetypeincludesOpenBadgeCredential(a token missingvcis flagged as a possible OB2 JWS), and the JWTiss/subregistered claims must match the credential's issuer id andcredentialSubject.idwhen present. -
Optionally binds the recipient — pass
expected_recipient(an email, amailto:URI, or a DID) and verification additionally requirescredentialSubject.idto match; without it, only the signature, expiry and structure are checked.
from openbadgeslib.ob3.verifier import OB3Verifier
v = OB3Verifier(pubkey_pem)
credential = v.verify(token, expected_recipient='recipient@example.org')Every failure raises OB3VerificationError (a subclass of LibOpenBadgesException, so one except catches both OB2 and OB3 errors). See Python API OB3 for the credential model, and OB2 vs OB3 for how the two generations differ.
Both versions share openbadgeslib/baking.py. Keeping one implementation stops the OB2 and OB3 paths from drifting into two slightly different readers. The carrier format is identical for both versions.
bake_svg() parses the SVG, appends an <openbadges:assertion> element to the root <svg>, and stores the token in its verify attribute (plus a namespace declaration and an optional XML comment):
<openbadges:assertion xmlns:openbadges="http://openbadges.org" verify="eyJhbGciOi…"/>extract_svg() reverses it: it finds the openbadges:assertion node and returns the verify attribute value, or None if the element is absent. has_svg() reports whether an assertion is already present (so re-signing is refused). XML is parsed with defusedxml to neutralize entity-expansion attacks.
bake_png() inserts an iTXt chunk (keyword openbadges) just before the trailing IEND chunk, with an optional tEXt comment chunk, and re-serializes the PNG with correct per-chunk CRCs:
iTXt: "openbadges" \0 <comp_flag> <comp_method> <lang> \0 <translated> \0 <token>
extract_png() does not trust a fixed byte offset — it parses the real iTXt structure (keyword, compression flag/method, language tag, translated keyword, then text), so tokens baked by any conformant tool, including compressed ones, are recovered. has_png() reports presence of the chunk.
Because extraction runs on untrusted input before any signature check, compressed iTXt text is inflated through a bounded decompressor capped at 256 KiB (MAX_ITXT_DECOMPRESSED); a crafted zlib bomb raises DecompressionLimitExceeded instead of exhausting memory.
The OB3 verifier exposes the reverse step directly as extract_token_from_svg() and extract_token_from_png(), which call the baking helpers and turn a missing token into an OB3VerificationError (and a parse failure into ErrorParsingFile). The OB2 path reads the assertion back through its badge/Assertion objects. Either way the flow is the same: extract the token from the image, then verify the signature with a trusted key.
See Security Model for the threat model behind the bounded inflate and key-trust rules, and Keys and Errors for the exceptions raised along the way.
openbadgeslib · LGPLv3 (library) / BSD (CLI) · Issues
Getting Started
Concepts
Reference
Guides
Project