-
Notifications
You must be signed in to change notification settings - Fork 1
Python API OB3
Programmatic guide to the OpenBadges 3.0 layer of openbadgeslib: building W3C Verifiable Credentials, signing them as JWT-VCs, baking them into images, and verifying them. Everything here lives in the openbadgeslib.ob3 package. For strict Open Badges 2.0 (JWS) see Python API OB2, and for the legacy pre-2.0 format see Python API OB1.
The full, always-up-to-date class/function reference is generated from the docstrings: API Reference.
from openbadgeslib.ob3 import (
Achievement, Issuer, OpenBadgeCredential,
OB3Signer, OB3Verifier, OB3VerificationError,
)Three dataclasses describe the credential. You build them top-down: an Issuer, an Achievement, then the OpenBadgeCredential that ties them to a recipient.
The profile of who awards the badge.
| Field | Required | Notes |
|---|---|---|
id |
yes | Issuer identifier (typically an HTTPS URL or DID). |
name |
yes | Human-readable issuer name. |
url |
no | Issuer home page. |
email |
no | Contact email. |
image_url |
no | Logo; serialised as {"id": ..., "type": "Image"}. |
The badge class / achievement definition.
| Field | Required | Notes |
|---|---|---|
id |
yes | Achievement identifier. |
name |
yes | Achievement name. |
description |
yes | What the badge represents. |
criteria_narrative |
yes | Serialised as criteria.narrative. |
image_url |
no | Badge image; serialised as an Image object. |
tags |
no | List of strings; serialised under tag. Defaults to []. |
The credential itself. Several fields are auto-filled in __post_init__:
| Field | Required | Default |
|---|---|---|
issuer |
yes | — |
recipient_id |
yes | A mailto: URI or a DID (see normalization below). |
achievement |
yes | — |
id |
no |
urn:uuid:<uuid4> when omitted. |
name |
no | Falls back to achievement.name. |
issuance_date |
no |
datetime.now(timezone.utc) when omitted. |
expiration_date |
no | Sets validUntil / JWT exp when present. |
evidence_url |
no | Adds an Evidence entry. |
Useful serialisation methods:
-
to_vc()— the bare Verifiable Credential JSON object (VC Data Model 2.0, with the OB 3.0 context), no JWT wrapper. -
to_jwt_payload()— the OB 3.0 native JWT-VC payload: the credential's members at the top level (novcwrapper) plus the registered claimsiss,sub,jti,nbf(andexpwhen expiring). -
OpenBadgeCredential.from_jwt_payload(payload)— classmethod that reconstructs a credential from a decoded payload (accepts bothvalidFrom/validUntiland the olderissuanceDate/expirationDatenames).
OB3Signer(privkey_pem, algorithm='RS256')privkey_pem may be PEM bytes, a PEM string, or a key object. algorithm must be one of RS256, RS384, RS512, ES256, ES384, ES512, EdDSA — anything else raises ValueError at construction. Use an RS* algorithm with an RSA key, an ES* algorithm with an EC key, and EdDSA with an Ed25519 key.
| Method | Returns | Purpose |
|---|---|---|
sign(credential) |
str |
Compact JWT-VC string. |
sign_into_svg(credential, svg_bytes) |
bytes |
JWT-VC baked into an <openbadges:credential verify="…"/> element. |
sign_into_png(credential, png_bytes) |
bytes |
JWT-VC baked into an iTXt chunk keyed openbadgecredential. |
The baking format matches OB 2.0, so existing badge viewers can extract the token regardless of version. See Signing and Verification for the shared baking concepts and Keys and Errors for generating compatible keys.
OB3Verifier(pubkey_pem)On construction the verifier detects the key type and pins the accepted JWS algorithms to that key family (RS* for RSA, ES* for EC, EdDSA for Ed25519). The token header can never dictate the algorithm, so alg:none, an HMAC downgrade, or cross-type confusion are all rejected up front. An unsupported key type raises OB3VerificationError.
verify(token, expected_recipient=None, check_status=False) -> OpenBadgeCredentialverify() checks the signature, expiry and structure, validates that the payload's top-level type is an OpenBadgeCredential (a VerifiableCredential plus OpenBadgeCredential/AchievementCredential — the native VC-JWT payload has no vc wrapper), and cross-checks the registered iss/sub claims against the credential's issuer/subject. On success it returns a fully reconstructed OpenBadgeCredential; any failure raises OB3VerificationError.
Status checking is opt-in. Verification is otherwise fully offline; pass check_status=True (the --check-status CLI flag) to additionally fetch each credentialStatus list over HTTPS and reject a revoked or suspended credential. The check is fail-closed. See Security Model.
Recipient binding is opt-in. By default verify() does not tie the credential to a recipient. Pass expected_recipient (a bare email, a mailto: URI, or a DID) to additionally require that credentialSubject.id matches; otherwise the caller must compare credential.recipient_id itself. See Security Model for why this matters.
Token extraction helpers are static methods:
OB3Verifier.extract_token_from_svg(svg_bytes) -> strOB3Verifier.extract_token_from_png(png_bytes) -> str
A missing assertion raises OB3VerificationError; unparseable XML raises ErrorParsingFile.
The signer and verifier share one normalization rule so they always agree: a bare email gains a mailto: scheme, while a value that already has a scheme — including a DID — is returned unchanged.
recipient@example.com -> mailto:recipient@example.com
mailto:recipient@example.com -> mailto:recipient@example.com (unchanged)
did:example:abc123 -> did:example:abc123 (unchanged)
So expected_recipient='recipient@example.com' and expected_recipient='mailto:recipient@example.com' both match a credential issued to mailto:recipient@example.com, and a DID is never mangled into mailto:did:....
OB 3.0 allows a second proof format besides VC-JWT: a JSON-LD credential with
an embedded W3C Data Integrity proof. OB3LdpVerifier verifies those
(cryptosuite eddsa-rdfc-2022; requires the [ldp] extra — see
Installation) with the same API and trust model as OB3Verifier:
from openbadgeslib.ob3 import OB3LdpVerifier
# Trusted key pinned by the operator:
credential = OB3LdpVerifier(pubkey_pem=pub_pem).verify(
document, expected_recipient='recipient@example.com', check_status=False)
# Or resolve the key from the proof's verificationMethod (did:key offline,
# did:web over HTTPS). A did:key is self-asserted: internal consistency only.
credential = OB3LdpVerifier().verify(document)
# Anchor to an issuer DID (issuer AND verificationMethod must belong to it):
credential = OB3LdpVerifier.for_issuer_did('did:web:issuer.example').verify(document)document may be the JSON string/bytes extracted from a baked image or an
already-parsed dict; the return value and every failure mode
(OB3VerificationError) match the JWT verifier, and expected_recipient /
check_status behave identically. When the key is not pinned, a proof whose
verificationMethod does not belong to the credential's DID issuer is
rejected fail-closed.
For advanced uses (e.g. non-OB3 Verifiable Credentials or the official W3C
test vectors) the crypto core is exposed as
verify_data_integrity_proof(document, pubkey_pem, *, expected_proof_purpose='assertionMethod', extra_contexts=None),
which checks only the proof itself. @context documents are never fetched
from the network — canonicalization uses the contexts bundled with the
library (see Security Model); extra_contexts extends that allowlist per
call. An unsupported cryptosuite (e.g. ecdsa-sd-2023) fails closed naming
the supported ones.
The issuance counterpart of OB3LdpVerifier (cryptosuite eddsa-rdfc-2022;
requires an Ed25519 key and the [ldp] extra). Its API mirrors
OB3Signer, but the output is the signed JSON document (a dict with the
proof embedded under proof), not a compact JWT:
from openbadgeslib.ob3 import OB3LdpSigner
signer = OB3LdpSigner(priv_pem) # did:key verificationMethod
signed = signer.sign(credential) # dict with embedded proof
svg = signer.sign_into_svg(credential, svg_bytes) # baked badge image
png = signer.sign_into_png(credential, png_bytes)Without a verification_method argument the proof carries a did:key
derived from the signing key's public half — self-asserted, so verifiers must
pin the public key. Issuers publishing a DID document should pass the method
id openbadges-publish -V 3 publishes, which verifiers resolve as trusted:
signer = OB3LdpSigner(priv_pem,
verification_method='did:web:issuer.example#badge_1')A non-Ed25519 key raises ErrorSigningFile at construction; signing without
the [ldp] extra raises it with the install hint. The schema-agnostic core is
add_data_integrity_proof(document, privkey_pem, verification_method, *, proof_purpose='assertionMethod', created=None, extra_contexts=None) — the
mirror of verify_data_integrity_proof, able to reproduce the official W3C
vc-di-eddsa test vectors byte for byte (created is injectable for
deterministic output; contexts come from the same bundled allowlist).
openbadgeslib.ob3.status_list writes what check_credential_status reads, and StatusRegistry tracks which credential owns which index:
from datetime import datetime, timezone
from openbadgeslib.ob3 import (
StatusRegistry, build_status_list_credential,
sign_status_list_credential, status_entry,
)
LIST_URL = 'https://example.com/issuer/badge_1/revocation.jwt'
# At issue time: allocate an index and attach the credentialStatus entry.
registry = StatusRegistry.load('status/badge_1.json')
index = registry.allocate(credential.id, credential.recipient_id,
credential.issuance_date)
registry.save() # persist BEFORE signing
credential.credential_status = [status_entry(LIST_URL, 'revocation', index)]
# At publish time: rebuild and sign the list from the registry.
registry.revoke(credential.id, datetime.now(tz=timezone.utc), reason='oops')
registry.save()
vc = build_status_list_credential('https://example.com/issuer/', LIST_URL,
'revocation', registry.revoked_indices(),
registry.size_bits)
token = sign_status_list_credential(vc, priv_pem, 'RS256') # host at LIST_URLStatusRegistry also offers suspend/unsuspend, find (by jti or recipient email) and suspended_indices. Transitions raise the typed exceptions in openbadgeslib.errors (AlreadyRevoked, NotSuspended, StatusListFull, ...); revocation is permanent by design. encode_bitstring is exposed for tooling that only needs the raw encodedList.
from openbadgeslib.keys import public_jwk_from_pem
from openbadgeslib.ob3 import build_did_document, did_web_from_url
did = did_web_from_url('https://example.com/issuer/') # did:web:example.com:issuer
doc = build_did_document(did, [('badge_1', public_jwk_from_pem(pub_pem))])The document round-trips through resolve_did. Order the methods deliberately: resolvers (this library's included) typically read only verificationMethod[0].
OB3VerificationError is the single exception for every verification failure (invalid signature, expired token, disallowed algorithm, recipient mismatch, wrong credential type, malformed payload, missing embedded token). It subclasses LibOpenBadgesException, so one except can catch both OB2 and OB3 failures. Token extraction may additionally raise ErrorParsingFile for unreadable images. See Keys and Errors for the full exception hierarchy.
from openbadgeslib.ob3 import OB3VerificationError
try:
credential = verifier.verify(token, expected_recipient='recipient@example.com')
except OB3VerificationError as exc:
print("Verification failed:", exc)Build a credential, sign it into an SVG with an RSA key, then extract and verify it — pinning both the algorithm (via the public key's type) and the recipient.
from openbadgeslib.ob3 import (
Achievement, Issuer, OpenBadgeCredential,
OB3Signer, OB3Verifier, OB3VerificationError,
)
# 1. Build the credential
issuer = Issuer(
id='https://example.com/issuer',
name='Example Org',
url='https://example.com',
)
achievement = Achievement(
id='https://example.com/achievements/1',
name='Python Wizard',
description='Awarded for mastering openbadgeslib',
criteria_narrative='Sign and verify an OB 3.0 credential',
image_url='https://example.com/badge.svg',
tags=['python', 'openbadges'],
)
credential = OpenBadgeCredential(
issuer=issuer,
recipient_id='recipient@example.com', # normalised to mailto: on sign
achievement=achievement,
)
# 2. Sign into an SVG image (RSA key -> RS256 token)
privkey_pem = open('test_sign_rsa.pem', 'rb').read()
svg_bytes = open('badge.svg', 'rb').read()
signer = OB3Signer(privkey_pem, algorithm='RS256')
signed_svg = signer.sign_into_svg(credential, svg_bytes)
open('badge-signed.svg', 'wb').write(signed_svg)
# 3. Extract and verify
pubkey_pem = open('test_verify_rsa.pem', 'rb').read()
verifier = OB3Verifier(pubkey_pem) # algorithm pinned to RSA here
token = OB3Verifier.extract_token_from_svg(signed_svg)
try:
restored = verifier.verify(token, expected_recipient='recipient@example.com')
print('Verified credential for', restored.recipient_id)
print('Achievement:', restored.achievement.name)
except OB3VerificationError as exc:
print('Verification failed:', exc)For PNGs, swap sign_into_png / extract_token_from_png and pass PNG bytes. EC keys work the same way with algorithm='ES256'.
- Python API OB2 — the strict 2.0 JWS/hosted API.
- Python API OB1 — the legacy pre-2.0 JWS API.
- Keys and Errors — key generation, key objects, and the exception hierarchy.
- Security Model — algorithm pinning, recipient binding, and the threat model.
openbadgeslib · LGPLv3 (library) / BSD (CLI) · Issues
Getting Started
Concepts
Reference
Guides
Project