OpenBadgesLib - Changelog
=========================

Newest first. Dates are ISO 8601 (YYYY-MM-DD).


* v3.0.1 - 2026-07-02

  - fix(cli): the OpenBadges 1.0 signer (`-V 1`) now writes the signed badge
    before appending to the audit log, and guards the log write, so a missing
    or unwritable `base_log` reports a clean error instead of crashing with a
    raw OSError and losing the already-signed badge (matches the OB2 path).


* v3.0.0 - 2026-07-01

  - BREAKING: the pre-2.0 wire format previously shipped as `-V 2` is relabelled
    OpenBadges 1.0 (`-V 1`) and frozen in the new `openbadgeslib.ob1` package (no
    @context/type, a `uid`, a `verify` object, string `hashed`, Unix-timestamp
    dates). `from openbadgeslib.ob2 import Signer/Verifier/Badge` no longer
    resolves — import from `openbadgeslib.ob1` (the top-level
    `openbadgeslib.signer` / `verifier` / `badge` shims still work).

  - BREAKING: the default `-V` is now `3` (was `2`) for openbadges-signer,
    -verifier, -publish and -keygenerator. Pass `-V 2` / `-V 1` explicitly for
    the older generations.

  - feat(ob2): new strict, spec-conformant Open Badges 2.0 implementation
    (`openbadgeslib.ob2`). Assertions are valid JSON-LD Badge Objects with
    `@context`, `type`, an IRI `id` (`urn:uuid:` for signed, the hosting URL for
    hosted), a boolean `hashed`, ISO 8601 dates, and a `verification` object.
    New `OB2Signer`, `OB2Verifier`, and dataclasses `Assertion`, `BadgeClass`,
    `Profile`, `CryptographicKey`, `RevocationList`.

  - feat(ob2): real HostedBadge verification — the assertion is fetched from its
    own `id` over HTTPS and scope-checked against the issuer origin (default
    same-origin, or the issuer's `startsWith` / `allowedOrigins`); the baked JWS
    is non-gating defence-in-depth. Select it with `openbadges-signer -V 2 -H`.

  - feat(ob2): SignedBadge verification resolves `verification.creator` to a
    published `CryptographicKey` and checks its `owner` / `publicKey` back-link
    to the issuer Profile.

  - feat(publish): `openbadges-publish -V 2` emits conformant hosted metadata —
    an issuer Profile with a `publicKey` array, a `BadgeClass` and a
    `CryptographicKey` (`key.json`) per badge, and a `RevocationList`.

  - feat(config): new optional badge keys `crypto_key` and
    `hosted_assertions_base` for the OB 2.0 signed / hosted flows.

  - OpenBadges 3.0 is unchanged.


* v2.0.0 - 2026-07-01

  - BREAKING (OB3): OpenBadges 3.0 credentials are now secured with the native
    OB 3.0 VC-JWT (spec §8.2): the JWT payload IS the credential (its members at
    the top level, no 'vc' claim wrapper), validFrom maps to 'nbf' (there is no
    'iat'), and the JOSE header carries the issuer's public key as a 'jwk'.
    Tokens issued by 1.x (the VCDM-1.1-style 'vc'-wrapper) are NOT compatible.

  - BREAKING (OB3): baked images use the OB 3.0 document-format identifiers —
    the PNG iTXt keyword 'openbadgecredential' and the SVG element
    <openbadges:credential> (namespace https://purl.imsglobal.org/ob/v3p0). OB3
    images baked by 1.x (OB2 identifiers) are not read by this verifier.

  - OB3 verifier now accepts credentials that are valid per the spec schema but
    were previously rejected: the AchievementCredential type alias, an issuer
    given as a string IRI (not only a Profile object), and a credentialSubject
    without an 'id' (identity conveyed via 'identifier').

  - OB3 verifier now enforces required structure it previously ignored: the
    @context (VC 2.0 + OB v3p0 pair) is validated, and the registered claims
    'iss' and 'nbf' are required (with 'sub' required when the subject has an id).

  - OB3 credentialStatus now honors statusPurpose: 'suspension' is reported
    distinctly from 'revocation', a non-revocation/suspension purpose (e.g.
    'message') no longer fails verification, and the entry's purpose is
    cross-checked against the fetched status list's declared purpose.

  - OpenBadges 2.0 is unchanged; only the OB3 path is affected.


* v1.3.0 - 2026-07-01

  - SECURITY: download_file() now blocks server-side request forgery. The URLs
    it fetches are attacker-influenced (an OB2 badge/issuer/revocationList URL,
    an OB3 did:web host, an OB3 credentialStatus list), so it now resolves the
    destination host and refuses any private, loopback, link-local, reserved,
    multicast, unspecified or carrier-grade-NAT (100.64/10) address; the check
    is re-applied to redirect targets. Previously a crafted badge could steer a
    verifier into GETting cloud-metadata endpoints or internal hosts. An
    allow_private=True opt-out is available for private deployments.

  - SECURITY: OB3Verifier.for_issuer_did() now binds the credential to the
    anchored DID — verify() requires the credential's issuer id to equal the
    DID whose key was resolved. did:web is not self-certifying, so without this
    a credential signed by the resolved key could claim a different (trusted)
    issuer and be accepted. Verifiers built directly from a public key are
    unchanged (the caller vouches for the key).

  - SECURITY: openbadges-verifier --json exit status now reflects issuer trust,
    not just signature validity: 0 = valid AND issuer-trusted, 2 = valid
    signature but untrusted issuer (an OB2 badge-embedded key or a self-asserted
    did:key), 1 = failure. In 1.2.0 a valid-but-untrusted badge exited 0, so CI
    gating on the exit code accepted signatures that do not prove issuer
    identity. NOTE: this changes the exit code for the valid-but-untrusted case
    from 0 to 2; the JSON body (valid/trusted/reason) is unchanged.

  - SECURITY: OB3 --resolve-did on a self-asserted did:key is now reported
    trusted:false (with a warning), mirroring OB2's badge-embedded-key case;
    only an operator-supplied key or a DNS/TLS-anchored did:web is trusted.

  - fix: an OB3 Bitstring Status List with statusSize > 1 (multi-bit entries)
    now fails closed instead of reading the wrong bit, which could have
    reported a revoked credential as valid.

  - fix: confparser now resolves a relative [paths] base against the directory
    containing the config file (not the process CWD). Previously only a bare
    '.' was handled and its value replaced wholesale, silently dropping the
    suffix of './data' or '../shared' and misplacing key/log/image trees. A '$'
    in the config directory path is handled correctly.

  - docs: Ed25519 (EdDSA) support is now documented across README and the wiki
    (key types, algorithms, keygenerator key_type, config example); the --json
    exit-status scheme is documented; and stale/inaccurate crypto-library and
    dependency notes were corrected.


* v1.2.0 - 2026-07-01

  - feat: openbadges-verifier gained a --json flag for machine-readable output.
    It prints a single JSON object ({valid, ob_version, recipient, reason, and
    version-specific fields such as OB2 trusted/status or OB3
    issuer/achievement/…}) instead of the human [+]/[-]/[~] lines, and exits 0
    when valid / non-zero otherwise — usable in CI and services without
    scraping stdout. The default (human) output and its exit codes are
    unchanged.

  - feat: OB3 issuer DIDs can now be resolved to a verification key. New
    ob3.resolve_did() and OB3Verifier.for_issuer_did() support did:key (Ed25519
    and P-256, self-certifying, offline) and did:web (fetches the DID document
    over HTTPS and reads its first verificationMethod's publicKeyJwk or
    publicKeyMultibase). openbadges-verifier gained a --resolve-did flag: when
    no trusted key is supplied for an OB3 badge, the issuer DID is read from the
    token and resolved, and the signature is checked against the resolved key.
    did:key needs no external trust; did:web trusts the host's DNS and TLS
    (documented in the Security Model).

  - SECURITY: OB3 credential revocation is now checkable. OB3Verifier.verify()
    gained an opt-in check_status=True (and openbadges-verifier a --check-status
    flag) that resolves each credentialStatus entry — Bitstring Status List v1.0
    and the legacy StatusList2021 — fetches the status list over HTTPS, and
    rejects a revoked/suspended credential. Previously OB3 had no revocation
    control at all (a revoked credential verified as VALID). The check is
    fail-closed when enabled (an unresolvable/malformed status list is a
    failure, not a pass) and the GZIP inflate of the bitstring is bounded. It
    verifies the published status bit only, not the status-list credential's own
    signature (documented).

  - feat: Ed25519 (EdDSA) keys are now supported end to end — key generation
    (set key_type = ED25519 in the badge profile), plus OB2 JWS and OB3 JWT-VC
    signing and verification. detect_key_type classifies an Ed25519 PEM
    explicitly (the ecdsa library would otherwise misread it as an ECC/NIST
    key), and the algorithm-pinning allowlists bind EdDSA to Ed25519 keys, so
    cross-type tokens are still rejected. cryptography is now an explicit
    dependency (it was already pulled in transitively by PyJWT[crypto]).


* v1.1.6 - 2026-07-01

  - fix: OB2 Verifier(verify_key=...) again accepts a live pycryptodome/ecdsa
    key object (not only PEM bytes); the construction-time key-type guard now
    wraps the key in key_to_pem() first, matching the verification path and
    OB3Verifier.

  - fix: a malformed config.ini (bad INI syntax, an unresolvable ${...}
    reference, an encoding mismatch, or a missing/empty [paths] base) now makes
    every CLI exit cleanly with a '[!] ...' message instead of a raw traceback;
    read_config_or_exit() catches the typed ValueError from read_conf().

  - SECURITY: OB2 revocation is now honored even when the issuer publishes an
    empty/falsy reason for a revoked serial. Previously a revoked badge whose
    revocation-list reason was "", null, false, or 0 was reported VALID
    instead of REVOKED (the revocation control failed open for those entries).

  - fix: _jws.verify_block() now rejects a non-string JWS header 'alg' (e.g. a
    JSON array/object) with a clean JWSException instead of leaking a raw
    TypeError from the algorithm-allowlist membership test.

  - fix: openbadges-verifier's OB2 path now reports a clean error instead of an
    uncaught traceback for an unsupported file extension (and any other library
    exception), by catching LibOpenBadgesException at the CLI boundary.

  - fix: BadgeSigned.read_from_file() now raises AssertionFormatIncorrect
    instead of a raw TypeError/AttributeError when the JWS body decodes to a
    valid-JSON non-object (array, string, number, or null).

  - fix: openbadges-init and openbadges-publish now exit cleanly with a
    '[!] <path> already exists' message instead of a raw FileExistsError
    traceback when the target directory/output path already exists.

  - fix: OB3 credential parsing now rejects a non-string required id/name
    field (vc.id, issuer.id, credentialSubject.id, achievement.id/name) with
    a clean OB3VerificationError, instead of leaking a raw AttributeError out
    of verify() when recipient binding lower-cases the id.

  - fix: OB2 check_revocation() now raises AssertionFormatIncorrect instead
    of a raw AttributeError when the badge/issuer JSON carries a non-string
    'badge'/'issuer'/'revocationList' URL field.

  - SECURITY: _jws.verify_block() now treats an RSA private key supplied
    where a public verify key is expected as a failed signature
    (SignatureError) instead of crashing with a raw AttributeError — closes
    a remotely-triggerable crash on the badge-embedded-key OB2 fallback path.

  - fix: openbadges-signer's -M/--mail-badge now reports a clean error
    instead of crashing when config.ini sets an SMTP username without
    use_ssl=True, when the mail template file is missing/unreadable
    (OSError), or when the badge section has no 'mail' key (KeyError); the
    already-signed badge is still saved in every case.

  - fix: OB3Signer.sign() now raises ErrorSigningFile instead of leaking a
    raw jwt.exceptions.InvalidKeyError when the requested algorithm doesn't
    match the given key's type.

  - fix: OB3Verifier.extract_token_from_png() now raises ErrorParsingFile
    instead of leaking a raw UnicodeDecodeError when a PNG's "openbadges"
    iTXt chunk contains malformed UTF-8 text.

  - fix: OB2 Verifier.__init__ now raises VerifierExceptions instead of a raw
    UnknownKeyType when a supplied verify_key is not a recognizable PEM key,
    matching the guard OB3Verifier already has.

  - fix: Badge.create_from_conf() now raises PrivateKeyReadError/
    PublicKeyReadError instead of a raw FileNotFoundError/OSError when a
    configured private_key/public_key path does not exist.

  - fix: openbadges-verifier's --local option now checks the configured
    public_key file exists before opening it, instead of leaking a raw
    FileNotFoundError past the CLI's error handling.

  - fix: OB3Verifier.verify() now rejects a vc.validFrom/validUntil claim that
    parses as a timezone-naive ISO 8601 date-time instead of leaking a raw
    TypeError when compared against the tz-aware expiration check.

  - fix: JWS header parsing (verify_block) now wraps malformed/non-object
    headers into SignatureError instead of leaking a raw json.JSONDecodeError,
    and every openbadgeslib._jws exception now inherits from
    LibOpenBadgesException so it can no longer escape verify()/sign() uncaught.

  - fix: BadgeSigned.read_from_file() now raises AssertionFormatIncorrect
    instead of a raw KeyError/AttributeError when the untrusted JWS body is
    missing required fields (image, badge, uid, recipient, issuedOn).

  - fix: Verifier.get_badge_status() now catches the ValueError download_file()
    raises for a non-HTTPS revocation/issuer/badge URL and returns a clean
    SIGNATURE_ERROR instead of letting it escape uncaught.

  - SECURITY: download_file() now rejects an HTTP redirect to a non-HTTPS
    target instead of transparently following it, closing a TLS scheme-
    downgrade gap in the HTTPS-only enforcement used for the OB2 verify key,
    issuer document, and revocation list.

  - SECURITY: download_file() now caps the response body at 5 MiB, bounding
    memory use against an attacker-influenced URL serving an oversized
    response.

  - fix: ConfParser.read_conf() now raises a clean ValueError instead of a
    raw KeyError/IndexError when config.ini is missing the [paths] section,
    its 'base' key, or has an empty 'base' value.

  - fix: OB3Verifier.verify() now raises OB3VerificationError instead of a
    raw AttributeError/TypeError when the JWT 'vc' claim is not an object, or
    its 'type' field is not a string/list.

  - fix: _parse_date() now raises a clean ValueError instead of a raw
    AttributeError when a vc date claim (validFrom/validUntil) is not a
    string.

  - fix: Verifier.check_expiration() now raises AssertionFormatIncorrect
    instead of a raw TypeError when the untrusted 'expires' claim is not a
    numeric timestamp.

  - fix: extract_png_assertion() now wraps baking.extract_png() the same way
    its SVG counterpart does, raising ErrorParsingFile instead of letting a
    raw exception escape on a malformed/garbage PNG.

  - fix: Badge.__init__ now raises PrivateKeyReadError/PublicKeyReadError
    instead of a raw ValueError/binascii.Error when a configured private or
    public key file is corrupt, truncated, or mismatched with its key type.

  - fix: BadgeMail.send() now catches the ValueError smtplib raises as its
    CRLF header-injection guard for a malformed from/recipient address,
    reporting a clean mail-failure message instead of crashing.

  - fix: ConfParser.read_conf() now eagerly resolves every ${...}
    ExtendedInterpolation reference at load time, raising a clean ValueError
    for a malformed reference instead of letting a raw configparser
    exception escape lazily, deep inside a CLI tool.

  - SECURITY: OB3Verifier.verify() now independently re-validates
    vc.validUntil/validFrom against wall-clock time, instead of relying only
    on the JWT 'exp' claim, which can be decoupled from the vc-level dates
    that downstream consumers actually read. An expired or not-yet-valid
    credential is now rejected regardless of the JWT claim.

  - fix: verify_block() now wraps a malformed (invalid-length) base64url JWS
    signature segment into SignatureError instead of leaking a raw
    binascii.Error.

  - fix: Verifier.check_jws_signature() now catches the JWSException base
    class instead of only SignatureError, so a JWS header missing 'alg' or
    a completely missing verification key resolves to a clean
    SIGNATURE_ERROR instead of an uncaught exception.

  - fix: BadgeSigned.read_from_file() now raises ErrorParsingFile instead of
    a raw KeyError/TypeError when the untrusted JWS body is missing 'verify'
    or has a non-object 'verify' field.

  - fix: Verifier.check_revocation() now raises AssertionFormatIncorrect
    instead of a raw AttributeError/TypeError when the fetched badge,
    issuer, or revocation-list JSON is valid JSON but not an object.

  - fix: Signer (OB2) and OB3Signer now wrap baking.has_svg/has_png/bake_svg/
    bake_png into ErrorSigningFile instead of letting a raw
    ExpatError/AttributeError/png.FormatError escape when the carrier image
    is malformed.

  - fix: openbadges-init and openbadges-publish now restore the process
    umask in a try/finally, so a failure partway through directory/file
    creation can no longer leave the umask permanently changed for the rest
    of the process.

  - fix: normalize_recipient_id() no longer double-prefixes an
    already-mailto: URI whose scheme is spelled in a different case, and
    OB3Verifier.verify()'s recipient binding now compares mailto: URIs
    case-insensitively (DIDs remain compared exactly), so a legitimate
    recipient is no longer wrongly rejected for a casing difference.

  - fix: ConfParser.read_conf() now raises a clean ValueError instead of a
    raw configparser exception for malformed INI syntax (duplicate
    section/option, a value line with no section header).

  - fix: openbadges-verifier now enforces -l/--local and -k/--pubkey as
    mutually exclusive (as documented in the CLI reference) instead of
    silently letting -l win when both are given.


* v1.1.5 - 2026-06-30

  - SECURITY: generated private key files are now created with exclusive writes
    and owner-only permissions (0600), avoiding accidental exposure under
    permissive umasks and preventing overwrite races.

  - SECURITY: openbadges-signer rejects unsafe badge/receptor filename
    components before composing the output path, blocking traversal through
    slash, backslash, drive, dot, empty, or NUL-containing values.

  - SECURITY: SMTP authentication now requires use_ssl=True, so configured
    credentials are not sent over plaintext SMTP.

  - fix: PNG badge detection now requires an exact openbadges iTXt keyword
    instead of treating unrelated chunks that merely start with "openbadges" as
    assertions.

  - Docs: remove the remaining legacy Sphinx RST reference pages. User and
    developer documentation is maintained in the GitHub Wiki, and the API
    reference is generated for GitHub Pages.


* v1.1.4 - 2026-06-29

  - SECURITY: harden OB3 JWT-VC parsing. OpenBadgeCredential.from_jwt_payload
    now validates the structure of the untrusted "vc" claim explicitly (every
    required object/field is checked, dates must be valid ISO 8601,
    credentialSubject may be an object or non-empty array) and raises a clear
    OB3VerificationError naming the offending field instead of surfacing a raw
    KeyError/TypeError.

  - fix: the OB3 verifier's iss/sub cross-check now normalises an array-form
    credentialSubject to its first element, so a validly-signed token carrying
    credentialSubject as an array no longer escapes verify() with a raw
    AttributeError; it verifies (or fails) as an OB3VerificationError.

  - packaging: advertise Python 3.13 support (add the trove classifier; it was
    already tested in CI and stated in the README).


* v1.1.3 - 2026-06-29

  - Typing: the package now ships a py.typed marker (PEP 561) and is fully
    annotated. mypy (disallow_untyped_defs) runs clean and is enforced in CI,
    so downstream type checkers can consume openbadgeslib's types.

  - Docs: fixed stale wiki references surfaced by an audit (OB2 verifier uses
    -r/--receptor; baking.py is a shared top-level module; dev extras and the CI
    gate now list mypy/pdoc).


* v1.1.2 - 2026-06-27

  - The -d/--debug flag now actually enables DEBUG-level console logging. It was
    previously parsed but ignored in openbadges-signer; it is now wired up and
    also added to openbadges-keygenerator and openbadges-verifier.

  - Packaging metadata refreshed: README is now Markdown, and the project URLs
    point to the GitHub Wiki (documentation) and the GitHub Pages API reference
    instead of the old homepage.

  - Docs & infra: the version is single-sourced from util.__version__; CI gates
    catch doc drift (CLI flags, wiki links); an auto-generated pdoc API site and
    an auto-synced wiki were added; Changelog normalised; CONTRIBUTORS.txt
    removed (see docs/authors.rst); pending work is now tracked in GitHub Issues.


* v1.1.1 - 2026-06-27

  - OB3: recipient identifiers are normalised through one shared helper, fixing
    a DID being corrupted into 'mailto:did:...'; the verifier CLI now delegates
    recipient binding to OB3Verifier.verify() instead of re-implementing it.

  - OB3: verify() cross-checks the JWT registered claims against the vc body -
    a token whose 'iss'/'sub' disagree with the credential issuer/subject is
    rejected - and OB3VerificationError now inherits from LibOpenBadgesException
    so one except clause covers both OB2 and OB3.

  - Refactor: shared keys.alg_for_key_type and CLI config helpers
    (read_config_or_exit / resolve_badge_section / _resolve_trusted_pubkey)
    remove the boilerplate duplicated across the entrypoints; first-party code
    imports openbadgeslib.ob2 directly rather than the back-compat shims; the
    oversized verify/main/_verify_ob3 functions were decomposed.

  - Type hints added on the OB2/core byte-vs-str boundaries.

  - Tests: OB2 signer CLI, the mail subsystem and read_from_file edge cases are
    now covered; 258 tests, 92% line coverage; the whole repo (source and
    tests) is flake8-clean.

  - CI: a GitHub Actions workflow runs flake8 and the test suite on Python
    3.10-3.13 for every push and pull request.


* v1.1.0 - 2026-06-27

  - SECURITY: signature verification now pins the accepted algorithm to the
    verification key's type instead of trusting the token header. OB3Verifier
    derives the RS*/ES* family from the key and passes it to jwt.decode;
    _jws.verify_block rejects any header alg that doesn't match the key. This
    blocks cross-type confusion and any none/HMAC downgrade.

  - SECURITY: SVG badge XML is parsed with defusedxml, defusing entity-expansion
    (billion-laughs) DoS on untrusted badges. New defusedxml dependency.

  - SECURITY: iTXt PNG token extraction caps zlib decompression at 256 KB to
    stop a decompression-bomb DoS during extraction (before verification).

  - SECURITY: the OB2 verifier CLI now distinguishes a trusted operator key
    (--local/--pubkey) from the badge's own embedded key. Without a trusted key
    it reports the signature as internally-consistent-only, not "[+] correct"
    (the embedded key proves nothing about issuer identity). --pubkey now
    applies to OB2 as well as OB3.

  - SECURITY: check_revocation guards the issuer / revocation-list downloads and
    JSON parsing, raising a clean VerifierException instead of a raw error.

  - Fixed: _jws base64url padding (wrong when the length was a multiple of 4);
    BadgeSigned.get_serial_num crashed on badges read from a file (str vs bytes);
    Verifier.check_identity crashed instead of skipping when no identity was
    given; extract_svg_assertion could raise NameError masking the real error;
    unknown key/image types now fail loudly instead of UnboundLocalError.

  - Refactor: a single keys.key_to_pem() replaces three drifting copies, and a
    new openbadgeslib.baking module is the one home for the SVG/PNG carrier
    format (OB2 and OB3 share it; the OB2 fixed-offset PNG reader is gone in
    favour of the structured iTXt parser).

  - Cleanup: removed the redundant setup.py, the stale committed dist/ artifacts
    and generated MANIFEST, and several never-raised exception classes; docs now
    cover OB3 on the landing page and document the SMTP/email-badge feature;
    test import hygiene tidied. 236 tests pass, 87% line coverage.


* v1.0.2 - 2026-06-18

  - SECURITY: OB2 verification now uses the operator-supplied trusted key when
    one is given, instead of always trusting the key the badge points to. A
    forged badge can no longer self-describe its own verification key.

  - SECURITY: download_file now refuses non-HTTPS URLs by default (the verify
    key is the OB2 root of trust); pass allow_insecure=True to override.

  - Fixed expiration check: a badge is now considered expired when its
    expiration date is in the past relative to *now*, not relative to its own
    issue date (expired badges were previously reported VALID).

  - Fixed openbadges-publish: it copied no verify key and stopped after the
    first badge (it read a non-existent [keys] section). It now reads each
    badge's public_key and iterates all badge_* sections.

  - Fixed SMTP: use_ssl is parsed as a boolean (the string 'False' was truthy);
    connection-level mail errors are caught instead of crashing a successful
    sign; the example config uses 'username' (matching the code), not 'login'.

  - openbadges-keygenerator now honours a key_type (RSA/ECC) field in the badge
    profile, so ECC key pairs are reachable from the CLI.

  - OB2 sign log and log files are opened in append mode (were truncated on
    every run).

  - OB3 credentials use the W3C VC 2.0 data model (validFrom/validUntil,
    /ns/credentials/v2 context); from_jwt_payload still reads VC 1.1 fields.
    OB3Verifier.verify() gained an optional expected_recipient argument and now
    asserts the credential type; PNG token extraction parses the iTXt chunk
    structure instead of a fixed offset.

  - Robustness: unsigned PNGs raise a clear error instead of crashing;
    read_from_file raises instead of calling sys.exit; identity is normalised to
    bytes; revocation handles issuers without a revocationList.

  - Tests: added CLI/publish/keygenerator/mail/revocation coverage and removed
    the coverage omit list that hid those modules; expiration tests rewritten to
    model real timestamps.


* v1.0.1 - 2026-04-22

  - OpenBadges 3.0 support (W3C Verifiable Credentials / JWT-VC):
    new openbadgeslib.ob3 subpackage with OpenBadgeCredential, Issuer,
    Achievement data model; OB3Signer for JWT-VC signing and SVG/PNG
    baking; OB3Verifier for JWT-VC verification and token extraction.

  - OpenBadges 2.0 implementation moved to openbadgeslib.ob2 subpackage.
    Top-level badge.py / signer.py / verifier.py kept as backward-compatible
    shims so existing code continues to work unchanged.

  - All CLI tools (openbadges-keygenerator, openbadges-signer,
    openbadges-verifier, openbadges-publish) gained a -V / --ob-version
    flag to select the specification version (2 or 3, default 2).

  - openbadges-verifier: new -k / --pubkey FILE option to supply the
    public key directly when verifying OB3 badges without a config file.

  - Python 3.10+ compatibility: removed distutils (dropped in 3.12),
    packaging migrated to pyproject.toml with setuptools.build_meta.

  - Replaced abandoned pycrypto with pycryptodome >= 3.20.

  - Replaced custom bundled JWS engine (3dparty/jws/) with PyJWT[crypto]
    algorithm classes (RS256/384/512, ES256/384/512). Internal module
    moved to openbadgeslib/_jws/. Old 3dparty/ directory removed.

  - TLS security fix: download_file relies on urllib's default TLS context
    (certificate validation on) instead of the deprecated PROTOCOL_TLSv1 /
    CERT_NONE settings.

  - pypng API update: signature constant renamed, chunk tags now bytes.

  - Copyright year range updated to 2014-2026 across all source files.

  - Bug fixes: verifier signature check logic, ECC private-key detection,
    hash_email str/bytes coercion.

  - Test suite: 203 tests, 89% line coverage.


* v0.4.2

  - Adding support to verifying external openbadges.

  - Adding a new parameter to show assertion before verifying.

  - OpenBadges URLs are verified before the signing process.


* v0.4

  - Support for PNG. The library can now verify and sign OpenBadges in PNG
    format.

  - Support for sending badges via mail.

  - Badge signatures are registered in a log file.

  - Mail code now supports SMTP auth. The config file needs modifications;
    see the example.


* v0.3

  - The email is no longer encoded in the filename when creating a new badge.

  - When creating a badge, "-o" is optional.

  - When specified, "-o" is supposed to be a directory.

  - We can specify "-E" or "--no-evidence" when creating a new badge.

  - When creating a new badge we now need to choose EXPLICITLY between
    providing evidence or not.

  - Badge sections in the INI file must now be prefixed with "badge_".
    UPGRADE: if your badge is called "ponente2014", rename it to
    "badge_ponente2014".

  - Each badge can now have a different key.
    UPGRADE: move your current key configuration to the badge section,
    renaming "private" to "private_key" and "public" to "public_key".
    UPGRADE: "openbadges-keygenerator -g" now requires a badge name.

  - "openbadges-verifier" changes its parameter from "-lk" to "-l" and now
    needs the name of the badge to locate its public key.

  - "openbadges-verifier" now accepts a parameter -x to specify the expiration
    for a badge.

  - "openbadges-verifier" now checks the revocation status and the expiration
    dates of badges.


* v0.2.1 - 2014-12-16

  - config.ini now allows configuring the badge json url and image url.

  - Signer now uses randomly salted emails by default in assertions.

  - Compatibility with more SVG formats.

  - Documentation in the "docs" directory.


* v0.2 - 2014-12-10

  - "openbadges-init".

  - New configuration format. If you are using the 0.1 format, you must
    migrate by hand.

  - Tests.

  - Use proper logging instead of simple "print" calls.

  - Massive cleanup of internal imports.

  - Do not change "sys.path".


* v0.1 - 2014-12-01

  - Initial release.
