Skip to main content

Thin Python binding to Google's Longfellow zero-knowledge mdoc library

Project description

pylongfellow

CI Docs PyPI Python License: Apache 2.0

Overview

A thin Python binding to google/longfellow-zk, the zero-knowledge library that ISO 18013-5 / EUDI mdoc wallets use to prove attributes of a credential without revealing the credential. The underlying scheme is described in Anonymous credentials from ECDSA.

pylongfellow is a cffi wrapper over the library's extern "C" mdoc ABI (lib/circuits/mdoc/mdoc_zk.h). It binds the prove and verify calls and the few structs they take; it does not add a layer of its own. The binding is attribute-agnostic — it proves and verifies (namespace, id, cbor_value) statements over an mdoc and has no notion of what any attribute means. age_over_18 is just the attribute the examples and tests happen to use.

Status: pre-1.0. Upstream is explicitly experimental and its ABI and circuits can change between releases. The vendored upstream is hard-pinned (see Upstream); treat pylongfellow, like its upstream, as not production-ready.

For what each function does and the exact types, read the docstrings — they render to a docs site (mkdocstrings + MkDocs-Material).

Install

pip install pylongfellow

Wheels are published for CPython 3.11–3.14 on Linux x86_64 (manylinux and musllinux). On any other platform pip falls back to the source distribution, which builds the vendored C++ locally and needs a C++ toolchain — see Build from source.

The wheel's runtime dependencies are cffi, cryptography, cbor2, and zstandard. Every wheel ships both backends: the google/longfellow-zk cffi extension and the abetterinternet/zk-cred-longfellow (ISRG) cdylib. Construction selects one by registry name (see Backends); neither requires an extra or a separate install.

What it binds

Pylongfellow is the entry point: a client bound to one backend at construction. Data types and errors are in the pylongfellow.mdoc submodule; the spec-table functions are in the pylongfellow.backends.google_cpp module, which reads the google-cpp backend's compiled-in table. Each client method wraps one native entry point. The wrappers marshal inputs, copy results out, and turn non-success return codes into typed exceptions.

Python Role
Pylongfellow(*, backend) bind a backend, by registry name ("google-cpp", "isrg-rust") or instance
.generate_circuit(spec) produce the compressed circuit a spec names
.load_circuit(spec, compressed) load a circuit into the bound backend, return a CircuitHandle
.prove(handle, mdoc, issuer_pk, transcript, attrs, timestamp) holder side; produce a proof
.verify(handle, issuer_pk, transcript, attrs, timestamp, proof, doctype, *, device_namespaces=None) verifier side; raises on a bad proof, returns on success
google_cpp.circuit_id(circuit) recompute a circuit's canonical id (equals CircuitSpec.circuit_hash); google-cpp only
google_cpp.find_zk_spec(system, circuit_hash) look up a built-in CircuitSpec, or None; google-cpp only

A compressed circuit is bytes: get them from generate_circuit, or read a committed blob from disk. prove and verify do not take the bytes directly. Pass them once to load_circuit, which returns a CircuitHandle bound to a backend, and pass the handle to every prove and verify call. There is no default backend; construction names one. See Backends.

Two C structs are exposed as frozen dataclasses:

  • RequestedAttribute(namespace, id, cbor_value) — "attribute (namespace, id) holds this value." cbor_value is raw CBOR bytes (e.g. b"\xf5" is CBOR true); the binding does no encoding.
  • CircuitSpec(system, circuit_hash, num_attributes, version, block_enc_hash, block_enc_sig) — a circuit's identity. The spec is the small descriptor prover and verifier agree on; circuit_hash (SHA-256 hex) pins which circuit it is. len(attrs) must equal num_attributes. Every backend reads version and num_attributes; the google-cpp backend additionally requires the whole record to match its compiled-in spec table.

A non-success C return code raises mdoc.ProverError, mdoc.VerifierError, or mdoc.CircuitError. All three are subclasses of mdoc.Error, which is a subclass of LongfellowError:

LongfellowError
└── mdoc.Error
    ├── mdoc.ProverError      # .code: mdoc.ProverErrorCode or None
    ├── mdoc.VerifierError    # .code: mdoc.VerifierErrorCode or None
    └── mdoc.CircuitError     # .code: mdoc.CircuitGenerationErrorCode or None

Catch by class. .code carries the specific failure when the backend supplies one: the google/longfellow-zk backend always does, the abetterinternet/zk-cred-longfellow (ISRG) backend leaves it None. Do not branch on the code. The code enums mirror C ints and overlap, so only the exception class says which enum a code is from.

Four functions in pylongfellow.mdoc bind no C entry point: create_credential assembles an ISO 18013-5 DeviceResponse test credential under locally held keys, with caller-controlled issuer-signed claims and device namespaces; create_certificate, sign_device_authentication, and verify_device_authentication are its trust-chain and device-signature companions. They run on cryptography and cbor2 alone; signatures are in the API reference.

Usage

from pylongfellow import Pylongfellow, mdoc
from pylongfellow.backends.google_cpp import find_zk_spec

client = Pylongfellow(backend="google-cpp")

spec = find_zk_spec("longfellow-libzk-v1", circuit_hash)
compressed = client.generate_circuit(spec)       # or Path(...).read_bytes()
handle = client.load_circuit(spec, compressed)

attrs = [mdoc.RequestedAttribute("org.iso.18013.5.1", "age_over_18", b"\xf5")]  # CBOR true

proof = client.prove(handle, credential, issuer_pk, transcript, attrs, now)
client.verify(handle, issuer_pk, transcript, attrs, now, proof, doctype)   # raises on failure

Migrating from 0.2.x: the module-level mdoc functions are gone; operations are methods on a client bound to a named backend. prove and verify no longer take the circuit bytes and the trailing spec. Load the circuit once and pass the handle.

# 0.2.x
proof = mdoc.prove(circuit, credential, issuer_pk, transcript, attrs, now, spec)
# 0.3.0
client = Pylongfellow(backend="google-cpp")
handle = client.load_circuit(spec, circuit)
proof = client.prove(handle, credential, issuer_pk, transcript, attrs, now)

A complete, runnable version over a committed sample mdoc is in examples/prove_and_verify.py: find_zk_specgenerate_circuitcircuit_idload_circuitproveverify. It needs nothing but the package and the bundled fixture; circuit generation takes ~15s.

Backends

A client is bound to one backend at construction. Pylongfellow(backend=...) takes a registry name or a Backend instance and raises BackendUnavailableError when the backend's native dependency is not built. prove and verify dispatch through the backend a circuit was loaded into: the CircuitHandle carries it, so a handle works on any client. Two backends ship in every wheel.

google-cpp binds the vendored longfellow-zk C++ library. can_generate is True. It populates .code on the exceptions it raises, ignores device_namespaces on verify, and checks at load that spec.circuit_hash matches the circuit bytes.

isrg-rust binds abetterinternet/zk-cred-longfellow (ISRG) through its UniFFI bindings. can_generate is False; it raises GenerationUnsupportedError from generate_circuit, so circuits come from a google-cpp client or from disk. verify requires device_namespaces (the inner bytes of the tag-24 DeviceNameSpacesBytes) and raises ValueError without it. It leaves .code as None. Circuit identity checking is backend-native behaviour: this backend does not check spec.circuit_hash at load, so a wrong circuit of the same version and attribute count is not detected there; mismatched version or count surfaces as an error at prove/verify.

Select it by name:

client = Pylongfellow(backend="isrg-rust")
handle = client.load_circuit(spec, compressed)

In a dev checkout the backend builds with:

git submodule update --init vendor/zk-cred-longfellow
uv run python scripts/build_isrg_rust_backend.py # needs cargo 1.85+; ~4 min cold build

build_isrg_rust_backend.py runs cargo build and uniffi-bindgen and stages the generated Python module and the cdylib into src/pylongfellow/backends/_zk_cred/ (gitignored). The CMake build runs the same script, so wheel and sdist builds ship the same files. A backend whose native piece is absent raises BackendUnavailableError.

Engine init on the isrg-rust backend takes about 18 seconds per role and about 740 MB resident for a v6 1-attribute circuit. Init is lazy and cached on the handle. prove then takes about 1.3 seconds and verify about 0.8 seconds on the reference machine.

The differential tests exchange proofs between the two backends in both directions over the vendored v6 1-attribute circuit, and both backends verify zk-cred-longfellow's committed interop proof, which google/longfellow-zk generated. For the same statement the isrg-rust proof is larger than the google proof (562228 versus 323868 bytes).

zk-cred-longfellow is licensed MPL-2.0; see Licensing.

Logging

Upstream logs to stderr (not Python logging, and it exposes no callback to bridge). The one control is the PYLONGFELLOW_LOG_LEVEL environment variable, read once when the extension loads — there is no Python API and no runtime reconfiguration, following the TF_CPP_MIN_LOG_LEVEL / GRPC_VERBOSITY convention.

Values (case-insensitive): error, warning, info, silent. The default is warning, which hides upstream's per-call info output but keeps genuine errors and warnings.

Build from source

The vendored upstream is a standard CMake project. Prerequisites (Debian/Ubuntu):

sudo apt install -y cmake libssl-dev libzstd-dev libstdc++-13-dev libgtest-dev libbenchmark-dev
git submodule update --init --recursive

The default c++ (g++) works. The isrg-rust cdylib additionally needs cargo 1.85+ (rustup). Build and test with uv:

uv sync                                   # builds both backends + dev group, writes uv.lock
uv run pytest                             # fast suite
uv run pytest -m "slow or not slow" --cov # full suite incl. real circuit generation

Each backend builds behind a CMake switch, default ON. A source install that omits one passes the switch as a config setting:

pip install pylongfellow -C cmake.define.PYLONGFELLOW_BUILD_ISRG=OFF   # google-cpp only
pip install pylongfellow -C cmake.define.PYLONGFELLOW_BUILD_GOOGLE=OFF # isrg-rust only

The omitted backend raises BackendUnavailableError at first use; everything else works unchanged. A PYLONGFELLOW_BUILD_GOOGLE=OFF build needs no C++ toolchain and none of the apt packages above, only cargo.

scikit-build-core drives the vendored CMake build and packages the cffi extension. The internal upstream object libraries are built position-independent and linked into a single shared object that cffi binds.

Two gotchas worth knowing:

  • uv sync won't rebuild on a C/C++-source-only change — it keys off version and dependencies, not native source mtimes, so it silently leaves the old .so installed. Force it with uv sync --reinstall-package pylongfellow.
  • Use a current uv. A stale one can serve a Python alpha (e.g. 3.14.0a4) whose GC segfaults at finalization after circuit generation; a shutdown SIGSEGV is the symptom. Update uv and check the interpreter version first.

How wheels are built

  • Wheels: cibuildwheel builds cp311–cp314 × {manylinux, musllinux}, x86_64, with the test suite run inside each build container as the gate; auditwheel repairs them. Each wheel carries the cffi extension and the isrg-rust cdylib; the cargo build runs once per libc family (the container is shared across the CPython passes, so cargo's cache makes the repeats cheap). A source distribution bundling both pinned upstreams ships alongside.
  • Publish: PyPI Trusted Publishing (OIDC, no long-lived tokens), which emits PEP 740 build attestations binding each wheel to its source commit.
  • Dev: uv (envs, lock), ruff (lint + format), mypy (strict), pytest, mkdocs.

Upstream

pylongfellow vendors google/longfellow-zk (Apache-2.0) as a git submodule, hard-pinned to a specific commit (currently v0.9, fe83ec6) and built from source into each wheel. It does not float: the upstream ABI and circuits can change between releases, and the test fixtures are pinned to a particular circuit and version.

The isrg-rust backend vendors abetterinternet/zk-cred-longfellow (ISRG, MPL-2.0) as a second git submodule, hard-pinned to 4f3d1b3, built from source into each wheel.

Not affiliated with Google or the European Commission — an independent binding to public libraries.

Licensing

pylongfellow's own code is Apache-2.0. The distribution's licence expression is Apache-2.0 AND MPL-2.0 because each artifact also carries compiled code from the two vendored upstreams:

  • google/longfellow-zk — Apache-2.0, compiled into the _longfellow extension.
  • abetterinternet/zk-cred-longfellow (ISRG) — MPL-2.0, compiled into the libzk_cred_longfellow cdylib.

All three licence texts ship in the wheel and sdist metadata (license-files). MPL-2.0 is file-level copyleft: its terms apply to the zk-cred-longfellow files only and place no obligations on pylongfellow's code or its users. The vendored files are unmodified; their source is the pinned upstream commit named above and is included in full in the sdist.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pylongfellow-0.4.0.tar.gz (20.4 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pylongfellow-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl (4.4 MB view details)

Uploaded CPython 3.14musllinux: musl 1.2+ x86-64

pylongfellow-0.4.0-cp314-cp314-manylinux_2_28_x86_64.whl (2.7 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.28+ x86-64

pylongfellow-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl (4.4 MB view details)

Uploaded CPython 3.13musllinux: musl 1.2+ x86-64

pylongfellow-0.4.0-cp313-cp313-manylinux_2_28_x86_64.whl (2.7 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.28+ x86-64

pylongfellow-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl (4.4 MB view details)

Uploaded CPython 3.12musllinux: musl 1.2+ x86-64

pylongfellow-0.4.0-cp312-cp312-manylinux_2_28_x86_64.whl (2.7 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.28+ x86-64

pylongfellow-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl (4.4 MB view details)

Uploaded CPython 3.11musllinux: musl 1.2+ x86-64

pylongfellow-0.4.0-cp311-cp311-manylinux_2_28_x86_64.whl (2.7 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.28+ x86-64

File details

Details for the file pylongfellow-0.4.0.tar.gz.

File metadata

  • Download URL: pylongfellow-0.4.0.tar.gz
  • Upload date:
  • Size: 20.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pylongfellow-0.4.0.tar.gz
Algorithm Hash digest
SHA256 a80894de214eae56ba5e25019b7ba1fc9f56fda7225995bd770b9bbddc2dd293
MD5 8d429105b6f546b33f38e55cd627f7b9
BLAKE2b-256 bba31669968db3a674229f91c14c6111c15fdf29a43e7467056f35c9632c2301

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0.tar.gz:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 06c9809035f2609c0caa2241bf89095cd2c8fd0bed3c1174bddc7a1d9e0735ab
MD5 c71d424938cb6f5c63f7e732ed72209c
BLAKE2b-256 5bc56d90a74568449ed6f98ef6658974d7fab4cf2590ad7b34c9b14ecd9ebbab

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp314-cp314-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 5c2bc121af0b56ef24500f2748d4fc455ea6f9458ff8eb75d4023f854e2be4b9
MD5 16d60377fa50e1ae88b3829384b112c2
BLAKE2b-256 d8688e38d024f1eed7587976a08213369db400877497ee17210365fe6d6f4e15

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp314-cp314-manylinux_2_28_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 548e8964e81551bc9b81e4c49fcdbf9faf2115728da3f1b8f90bd4a41e6e173a
MD5 bbf2be2017b3eb950149d329897b7372
BLAKE2b-256 4cce87a5990559e2862acc8bfcedbf37bd23ff9d1dd7d9440dbdf092f725c44f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp313-cp313-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp313-cp313-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 8fe7607cae8d2a3ffa25b25f3a2b327c9dcfd5d13ec026e3e44defc11e758d0e
MD5 294d0d47484d79fa74e49d59a8c709e4
BLAKE2b-256 395c6ef9c97119632c6ae3969c41d2ba3a9794bb4bc6e758abe4b1673f708bd3

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp313-cp313-manylinux_2_28_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 f038ae6e4a2ad9d1cdb8d5f7713f61942e2e44fa57a2bd54d77a149fd7bd3c32
MD5 1688c26c4bcaae9461839b929cf6a773
BLAKE2b-256 3e2db88185ea3d3337b886a4a2bd142ee6eadaba8103fda255d6c52c089ee3c1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp312-cp312-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp312-cp312-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 9be960db867ba7f136c4e67d69987823e66c0692c45f1756cd5c49c2c6e502e4
MD5 8dc36aa766124d0f4220dac05abf059d
BLAKE2b-256 47a592e0a1c471b44d2f032a86fba70558228e7fa3e392897a02ed8690c8bf22

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp312-cp312-manylinux_2_28_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 69be0323069dd12b7d5423daf0be7238ac4eb519efde0bc4ba38e7725b57c23b
MD5 a5005981235d7c39ec97cdcdecedebb5
BLAKE2b-256 ccef676316922297a18c5a414b799656d977b74b9530a2c627053e97d565e969

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylongfellow-0.4.0-cp311-cp311-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pylongfellow-0.4.0-cp311-cp311-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 eef82a928df5c3a583ae9d867b21839fce821fdeb813de2e5ede4e2d589d7b47
MD5 bb6e66a1c785bf52a4cb883073fe41e4
BLAKE2b-256 0dee850781af2e57686d94e082c08d7683eada70beee7cfa899781f0eb9d1525

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylongfellow-0.4.0-cp311-cp311-manylinux_2_28_x86_64.whl:

Publisher: release.yml on pipe23-org/pylongfellow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page