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

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


* 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.
