c2pa-http
C2PA manifest discovery over HTTP: the c2pa-manifest Link header, per
RFC 8288, with a
Tower middleware.
When an asset is served over HTTP and carries no embedded Manifest Store, a
validator should look for a Link header carrying rel="c2pa-manifest". Its
target is where the C2PA Manifest Store can be retrieved.
[]
= "0.1"
What it does
link |
parse and serialise the header; no dependencies, &str in and out |
layer |
the Tower Layer that attaches it to every response |
Retrieving the manifest is left to you: the crate performs no network I/O, so it
makes no decisions about timeouts, redirects, or trust. Error::Inaccessible
exists so a caller that does fetch can report manifest.inaccessible through
the same type.
Serve it
use ManifestLinkLayer;
use ServiceBuilder;
let layer = new?;
let service = new.layer;
# Ok::
The header is appended, never set. A response may already carry Link
fields for preload, canonical, or pagination hints; replacing them would break
unrelated behaviour.
Read it
use link;
let header = r#"</style.css>; rel=preload, <https://a.example/m.c2pa>; rel="c2pa-manifest""#;
let found = extract?;
assert_eq!;
# Ok::
Embedded manifests, and the childlabel rule
A target may name a Manifest Store already embedded in the asset, via a JUMBF
URI fragment. Referencing a specific manifest inside the store is not permitted,
and a validator must ignore the childlabel portion — so it is discarded:
use link;
let header = r#"<https://a.example/i.jpg#jumbf=c2pa/urn:uuid:1234>; rel="c2pa-manifest""#;
let found = extract?;
assert!;
assert_eq!;
# Ok::
Parsing that holds up
Commas separate link-values and semicolons separate parameters — but both are
legal inside a <target> or a quoted string. A query string of ?ids=1,2,3 is
ordinary, and splitting naively on those characters is the classic way to
mis-parse this header. The scanner tracks both contexts.
Also handled: quoted and unquoted rel, case-insensitive matching, rel as a
space-separated token list, several Link fields on one response, backslash
escapes inside quoted parameters, and RFC 8288's rule that only the first
rel parameter counts.
Competing targets are rejected rather than guessed at: the specification defines
no precedence between two different c2pa-manifest links, so choosing one would
be inventing a rule. Duplicate links naming the same target are fine.
Header injection is impossible, without rejecting anything
A raw CR, LF, space, or angle bracket cannot legally appear in a URI at all —
RFC 3986 excludes them. So a string
carrying one is not a URI to be rejected; it is a URI that has not been encoded
yet. link::format percent-encodes it, which is both the spec-correct repair
and what makes injection impossible:
use link;
// A CR/LF payload lands inside the URI instead of starting a new header.
let header = format?;
assert!;
assert!;
# Ok::
> becomes %3E and can no longer close the target early; non-ASCII travels as
percent-encoded UTF-8. Encoding is idempotent — % is left untouched, so an
already-encoded URI is not double-encoded into %2520 — and every delimiter a
URI needs (? # / : @ & = + and the sub-delims) is preserved, so query strings
and fragments survive intact.
When you would rather be told that your input needed repairing, use
link::format_strict or ManifestLinkLayer::new_strict. For a target read from
configuration that is usually the better choice: a stray space becomes a startup
error instead of a silent %20 and a 404 at validation time.
Scope
This crate attaches and reads a header. It deliberately does not inspect request or response bodies to detect embedded provenance: that means buffering the whole body before forwarding it, which turns a streaming proxy into an unbounded memory sink and hands any client a denial of service. That belongs behind its own explicit opt-in with a mandatory size cap, not in the layer that writes a header.
Other languages
Python and JavaScript get the link parser — the Tower layer has no meaning off
a Rust service stack, but emitting and reading the header is exactly what a web
framework needs.
=
=
import from "c2pa-http";
res.;
Features
| feature | default | adds |
|---|---|---|
tower |
yes | the Layer/Service (http, tower-layer, tower-service, pin-project-lite) |
python |
no | PyO3 bindings for the PyPI distribution |
default-features = false leaves a dependency-free RFC 8288 parser usable under
any HTTP stack — hyper, axum, a Cloudflare Worker, or a hand-rolled server.
Related
| crate | method |
|---|---|
c2pa-html |
HTML: script and link elements in the document head |
c2pa-structured-text |
structured text: ASCII-armoured manifest in a comment |
c2pa-unstructured-text |
unstructured text: Unicode variation selectors |
For an HTML document carrying both an in-document manifest element and a Link
header, the specification gives the header precedence.
License
MIT OR Apache-2.0.