Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ten-minute producer

This page extends the Five-minute app into a producer/consumer pair: one process serves a Data, another fetches it.

Prerequisites

A running forwarder at /tmp/ndn-fwd.sock. See Running the forwarder.

The producer

use ndn::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let keychain = KeyChain::ephemeral("/example")?;
    // connect registers the prefix; with_signer signs what we publish.
    let producer = Producer::connect("/tmp/ndn-fwd.sock", "/example")
        .await?
        .with_signer(keychain.signer()?);

    producer
        .publish_object("/example/hello".parse()?, b"hello, ndn".to_vec().into(), 0)
        .await?;

    // Keep the process alive while the forwarder serves requests.
    tokio::signal::ctrl_c().await?;
    Ok(())
}

publish_object(name, content, chunk_size) segments the object and serves it on demand, signing each segment with the configured signer (without with_signer it emits DigestSha256 — integrity, not authorship). chunk_size == 0 uses the default segment size.

Aggregated signing — one signature per object (extension)

For larger objects, signing every segment is wasteful: a FLIC-style manifest signs the object once. publish_object uses Aggregation::Auto, which switches to a manifest for medium objects (roughly 8–256 segments) and stays per-segment for small ones. To choose explicitly:

use ndn::{Aggregation, PublishOptions};

producer.publish_object_with(
    "/example/big".parse()?,
    content,
    PublishOptions { chunk_size: 0, aggregation: Aggregation::Manifest },
).await?;

How it works: the RDR …/32=metadata Data — already the single signed discovery object — carries the ordered per-segment SHA-256 hashes and is marked ContentType::Manifest. That one signature authenticates the whole object; segments are then served plain and authenticated by hash-match against the manifest. The manifest also doubles as a content listing.

This is transparent to the consumer: fetch_object detects a manifest from the metadata and verifies segments by hash automatically — the code below is unchanged whether the object was per-segment-signed or aggregated.

The hash list is built in the same pass that segments the object, so the per-segment commitments are essentially free on top of hashing the data once. The current manifest is a single flat packet (objects beyond ~256 segments fall back to per-segment signing); nested-manifest DAGs for very large files are a planned extension. This is an ndn-rs extension, not an adopted NDN community wire spec.

The consumer — verify what you fetch

The producer signed its Data; the consumer’s job is to check that signature against a trust anchor it has decided to pin. Verifying is not an optional extra step — it is how the consumer decides the data is authentic.

use ndn::prelude::*;
use ndn::{Consumer, KeyChain};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut consumer = Consumer::connect("/tmp/ndn-fwd.sock").await?;

    // Build a validator that pins the producer's certificate as a trust anchor.
    // Across processes the anchor is distributed out-of-band — a cert file, a
    // `did:ndn`, or NDNCERT enrollment; see "Identity and keys" below.
    let keychain = KeyChain::ephemeral("/consumer")?;
    keychain.add_trust_anchor(producer_cert); // the /example producer's anchor
    let validator = keychain.validator();

    // fetch_verified returns SafeData only if the signature checks out.
    let safe = consumer.fetch_verified("/example/hello", &validator).await?;
    println!("{}", String::from_utf8_lossy(safe.data().content().unwrap_or_default()));
    Ok(())
}

fetch_verified returns SafeData — proof the signature verified. The unauthenticated siblings fetch / fetch_object return raw Data/bytes and do not check the signature; use them only when you are handling trust yourself (see Security pitfalls). For a complete, runnable signed-and-verified exchange in one file, see example-secure-fetch.

What happens on the wire

The producer signs the Data and announces /example/hello. The consumer’s Interest is routed by the forwarder’s FIB to the producer’s face; the returned Data is cached in the Content Store, so repeat calls within the cache lifetime never reach the producer. The full PIT/FIB/CS story is in Interest and Data lifecycle.

Signing identities

KeyChain::ephemeral keeps the key in memory (regenerated each run) — fine for a demo. For a persistent identity backed by a key file, use KeyChain::open_or_create(path, name); for enrollment under a CA, see Identity and keys. The producer signs with the keychain’s default identity unless overridden via SigningInfo.

Next steps