---
title: "Implementing an LRFS reader in 100 lines of Python | LLMind"
description: "A minimal Python reader for the LLM-Ready File Specification (LRFS v1.0). Parse XMP, canonicalize layers, verify HMAC-SHA256 signatures — under 100 lines, stdlib only."
url: https://llmind.org/blog/implementing-lrfs-reader-100-lines/
source_format: html
---
# Implementing an LRFS reader in 100 lines of Python

Published 2026-04-25

## Why a reader is the right starting point

Before you write an LRFS enrichment tool, build a reader. A reader is strictly smaller — it doesn't need to know how enrichment data was created, which model produced it, or what signing key was used. It just needs to extract layers from a file and verify that the signature is valid.

That simplicity is the point. A working reader proves that LRFS is implementable in your language and your environment. It exercises every code path that matters: XMP extraction, canonical byte construction, HMAC verification. If you can read and verify an enriched file, you understand the spec well enough to write one.

There's also a practical argument. Readers are where 90% of real-world value is created. Any AI tool, agent, or pipeline that wants to consume enriched files needs a reader. Only the producers — CLI tools, enrichment services, batch processors — need a writer. If you're building something that processes files other people have enriched, a reader is what you need, full stop.

This post walks through a complete LRFS reader in under 100 lines of Python, using nothing outside the standard library. It's not a sketch — it's the actual code, verified against the published test vectors.

## What a minimal reader does

An LRFS reader does three things in sequence.

**Step 1: Extract the XMP packet.** Every LRFS-enriched file embeds its metadata in an XMP packet — a well-formed XML block that's been embedded in the file's existing metadata structure. PDFs carry XMP in a `metadata` stream object. JPEGs carry it as a raw APP1 marker segment. PNGs carry it as an `iTXt` chunk. MP3s carry it inside an ID3 `TXXX` frame. The extraction mechanism is file-format-specific; for PDFs you'd reach for `pikepdf`, for images `Pillow`, for audio `mutagen`. This tutorial skips that step and works directly with XMP strings — that's the interesting part anyway.

**Step 2: Parse the RDF and extract LRFS layers.** Inside the XMP packet is an RDF document. The LRFS layers live inside an `rdf:Description` element whose children are in the namespace `https://llmind.org/ns/1.0/`. The reader needs to find those elements and collect them as a `{layer_name: value}` dictionary. Elements in other namespaces (Dublin Core, IPTC, other XMP schemas) are silently ignored — per the spec's versioning rules (§7), a v1.0 reader must not be confused by metadata written by other tools.

**Step 3: Canonicalize and verify.** LRFS defines a deterministic canonical form for the layer set (§3.3): exclude `llmind:signature`, sort the remaining layers alphabetically by name, then concatenate each name and value as null-terminated UTF-8 byte sequences. HMAC-SHA256 of that canonical input is the expected signature. The reader computes the expected HMAC and uses constant-time comparison to check it against the signature embedded in the file.

If step 3 passes, the reader has confirmed two things: the layers came from whoever holds the signing key, and nothing was modified after signing. That's the trust model.

## The code

The reader fits in three functions. No third-party libraries required.

```python
#!/usr/bin/env python3
"""
Minimal LRFS v1.0 reader — parse an XMP packet, canonicalize layers,
verify HMAC-SHA256 signatures. Python 3.11+ stdlib only.

Reference: https://llmind.org/spec/
"""

import hashlib
import hmac
import xml.etree.ElementTree as ET
from typing import Dict

LRFS_NS_V1 = "https://llmind.org/ns/1.0/"


def canonicalize(layers: Dict[str, str]) -> bytes:
    """
    Produce the canonical byte string for a set of LRFS layers, per
    LRFS v1.0 §3.3:

    1. Exclude the llmind:signature layer (if present).
    2. Sort remaining layers alphabetically by name.
    3. For each, append UTF-8 bytes of: name || 0x00 || value || 0x00.
    """
    names = sorted(n for n in layers if n != "llmind:signature")
    parts: list[bytes] = []
    for name in names:
        parts.append(name.encode("utf-8"))
        parts.append(b"\x00")
        parts.append(layers[name].encode("utf-8"))
        parts.append(b"\x00")
    return b"".join(parts)


def parse_xmp(xmp_packet: str) -> Dict[str, str]:
    """
    Extract llmind:* layers from an XMP packet. Returns {'llmind:text': ..., ...}.
    Raises ValueError on malformed XMP or missing xmpmeta element.

    A v1.0 reader MUST ignore elements in non-LRFS namespaces and MUST NOT
    misinterpret elements in a future major-version namespace (§7).
    """
    start = xmp_packet.find("<x:xmpmeta")
    end_tag = "</x:xmpmeta>"
    end = xmp_packet.rfind(end_tag)
    if start == -1 or end == -1:
        raise ValueError("XMP packet: missing <x:xmpmeta> element")
    doc = xmp_packet[start : end + len(end_tag)]
    root = ET.fromstring(doc)
    desc = root.find(".//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description")
    if desc is None:
        return {}
    layers: Dict[str, str] = {}
    for child in desc:
        tag = child.tag
        prefix = "{" + LRFS_NS_V1 + "}"
        if tag.startswith(prefix):
            local = tag[len(prefix) :]
            layers[f"llmind:{local}"] = child.text or ""
    return layers


def verify_hmac(layers: Dict[str, str], key: bytes, signature: bytes) -> bool:
    """
    Verify HMAC-SHA256(canonical_input, key) == signature, using
    constant-time comparison.
    """
    canonical = canonicalize(layers)
    expected = hmac.new(key, canonical, hashlib.sha256).digest()
    return hmac.compare_digest(expected, signature)
```

A few things worth noting. `canonicalize()` handles the signature-layer exclusion automatically — when you pass the full layer dict (including `llmind:signature`), it strips that key before sorting. This means you never have to remember to pre-filter the dict before calling it. `parse_xmp()` uses `xml.etree.ElementTree`'s Clark notation for namespace matching (`{namespace_uri}local_name`), so it naturally ignores any XMP element that doesn't belong to the LRFS v1.0 namespace — it won't accidentally pick up Dublin Core titles or IPTC keywords that happen to sit alongside the LRFS layers. And `verify_hmac()` uses `hmac.compare_digest()` for the final comparison, which runs in constant time regardless of where the mismatch occurs — this matters if the verifier is in a path that could be timing-attacked.

## Verification against the published test vectors

LRFS publishes ten happy-path test vectors at [/spec/test-vectors/](https://llmind.org/spec/test-vectors/). Each vector is a JSON file with a specific input (layers, signing key) and expected output (canonical bytes, HMAC signature). Any implementation can load these vectors and assert that its output matches.

Here's the code that verifies the reader above against vector `hmac-sha256/001-basic`:

```python
import base64
import hashlib
import hmac
import json
from pathlib import Path


def main() -> None:
    """Verify the reader against published LRFS test vector hmac-sha256/001-basic."""
    vector_path = Path("public/spec/test-vectors/hmac-sha256/001-basic.json")
    vector = json.loads(vector_path.read_text())

    layers = vector["inputs"]["layers"]
    key = bytes.fromhex(vector["inputs"]["key_hex"])
    signature = base64.b64decode(vector["expected"]["signature_base64"])

    ok = verify_hmac(layers, key, signature)
    print(f"Vector hmac-sha256/001-basic verify: {'PASS' if ok else 'FAIL'}")

    # Negative check: a mangled signature must NOT verify.
    bad_sig = bytes(b ^ 0xFF for b in signature)
    not_ok = verify_hmac(layers, key, bad_sig)
    print(f"Vector hmac-sha256/001-basic with mangled sig verify: {'PASS' if not not_ok else 'FAIL'}")


if __name__ == "__main__":
    main()
```

The vector for `001-basic` has a single layer: `{"llmind:text": "hello world"}`. The canonical input per §3.3 is `llmind:text\x00hello world\x00` — 24 bytes. The expected signature is `/0gu5E01jiqjQGWTJhSPcfQfG+xssi3nSlO7K6UWPiY=` (base64). Running this code produces byte-identical output to the reference implementation.

Your reader's correctness is no longer a matter of opinion. It's a JSON file away.

The [/spec/test-vectors/](https://llmind.org/spec/test-vectors/) page also lists failure-mode vectors from Spec 7: tampered signatures, malformed XMP, unknown namespace versions, and layers with missing required fields. A production reader needs to handle those correctly too — the test suite tells you exactly what "correctly" means. The vectors are the cross-implementation contract.

## Porting to other languages

The algorithm is straightforward enough that you can port it to any language with an HMAC-SHA256 implementation and an XML parser. The three functions map cleanly to any language that has:

-   A sorted collection primitive
-   UTF-8 string encoding to bytes
-   HMAC-SHA256 (every standard crypto library)
-   Namespace-aware XML parsing

**Rust:** `sha2` + `hmac` crates for cryptography, `quick-xml` for the XMP parsing. Rust's `std::collections::BTreeMap` gives you sorted-by-key iteration for free, which simplifies `canonicalize`.

**Go:** `crypto/hmac` + `crypto/sha256` from the standard library cover verification. `encoding/xml` handles the XMP parsing, though you'll need to write a namespace-aware decoder to match the Python `parse_xmp` behavior.

**Node.js:** `crypto.createHmac('sha256', key)` for the HMAC side. DOMParser or `fast-xml-parser` for the XMP. JavaScript object keys don't sort automatically, so `Object.keys(layers).sort()` is the equivalent of Python's `sorted()`.

**Swift:** `CryptoKit.HMAC<SHA256>` for verification. `XMLParser` for the XMP, with the delegate pattern handling namespace-aware element collection.

The test vectors are the cross-language contract. If your Rust reader produces a different HMAC than this Python reader for the same input, one of the two is wrong — and the vectors tell you which. You don't have to trust your implementation; you can prove it.

## Next steps

If your reader passes all ten happy-path vectors and handles the failure-mode cases correctly, it's conformant. The [/spec/implementations/](https://llmind.org/spec/implementations/) directory is set up to list conformant readers and writers across languages. A single entry in that directory — language, conformance level, link to source — is enough to get listed.

The [/spec/](https://llmind.org/spec/) has the full normative text if you need to go deeper on canonicalization edge cases, namespace versioning, or the ed25519 signing variant. The [/glossary/lrfs/](https://llmind.org/glossary/lrfs/) entry is the short version if you want the conceptual overview first.

Start with the code above. Run it against the vectors. Then port it to whatever language your users are actually building in. The file format is open. The test vectors are public. The only thing that's missing is your implementation.
