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

The security model

NDN’s central thesis is secure the data, not the channel: every Data packet is signed, and a consumer must verify that signature against a trust anchor before trusting the bytes. In most stacks that verification is a discipline — the API hands you the bytes and trusts you to call the validator. ndn-rs makes it a type transition the compiler enforces, so “did I actually verify this?” is answered by the type of the value in your hand, not by reading the call site. This is the design recorded in ADR 0002; this page is how it works in the code.

The security core is ndn-security; certificates layer on in ndn-cert and identity lifecycle in ndn-identity.

Two types, one invariant

The whole model rests on a pair of dual types in ndn-security.

SafeData (crates/security/ndn-security/src/safe_data.rs) is “this Data was verified.” Its fields are pub(crate), so nothing outside ndn-security can construct one with a struct literal. In a normal build the only way to obtain a SafeData is to run a Validator and have the signature check succeed — the crate mints it inside Validator::validate. A SafeData records not just the payload but how it earned trust, in a TrustPath:

TrustPath variantMeaning
CertChain(Vec<Name>)Validated via a certificate chain to a trust anchor.
LocalFace { uid }Trusted because it arrived on a local face with known process credentials.
DigestSha256Only a self-contained SHA-256(signed_region) == sig_value integrity check — no key, no anchor.

Unverified<T> (crates/security/ndn-security/src/unverified.rs) is the other half: “this was not verified.” It is freely constructible on purpose — unverified is the safe, pessimistic state to be in — and it has exactly two ways out:

  • .verify(validator).await — validate against a Validator, yielding SafeData on success. This is the safe path.
  • .trust_unchecked() — accept the value without verification, deliberately. The name is loud and greppable by design, so a security audit reduces to grepping for trust_unchecked. There is no silent path to usable bytes.

There is also .peek(), which borrows the inner value (e.g. to read a name and pick a validator) without consuming the wrapper or claiming any trust.

Integrity is not authentication

A bare SHA-256 digest proves the bytes were not corrupted; it says nothing about who produced them. ndn-rs refuses to conflate the two. Unverified::verify returns Err(VerifyError::UnauthenticatedDigest) for a packet that validates only as DigestSha256, even though the digest itself is arithmetically correct. Accepting integrity-only data is possible but must be asked for explicitly, via .verify_allowing_digest() — for local, in-process, or digest-addressed content where an unauthenticated-but-intact packet is genuinely acceptable.

The honest subtlety in the code: the Validator itself does report a valid DigestSha256 packet as ValidationResult::Valid with the DigestSha256 trust path — it labels the trust path truthfully. The refusal to treat that as “verified” is a consumer policy applied by Unverified::verify, not a claim that the digest failed. VerifyError has three variants:

VariantCause
Invalid(TrustError)Signature was cryptographically invalid, or the trust schema rejected the pair.
PendingThe signing certificate chain is not yet resolved (async fetch in flight).
UnauthenticatedDigestThe packet verified only as DigestSha256 — integrity, not identity.

The verification flow

graph LR
    F[fetch name] --> U[Unverified Data]
    U -->|trust_unchecked| Raw[raw Data - explicit bypass]
    U -->|verify validator| V{Validator.validate}
    V -->|Valid| S[SafeData]
    V -->|Invalid| E1[VerifyError.Invalid]
    V -->|Pending| E2[VerifyError.Pending]
    V -->|DigestSha256 only| E3[VerifyError.UnauthenticatedDigest]

On the application side this is wired through Node (crates/app/ndn-app/src/node.rs). A plain node.fetch(name) returns raw, unverified Data — the ergonomic-but-unverified path. node.verifying(validator) returns a VerifiedConsumer whose fetch returns SafeData, so the payload is signature-checked before it reaches your logic. The RDR object path funnels through one choke point (accept_content in crates/app/ndn-app/src/consumer.rs): with a validator present it runs Unverified::new(data).verify(&v) and errors if the Data does not authenticate; without one it takes the content as-is.

Validators default to deny

A Validator (crates/security/ndn-security/src/validator/mod.rs) dispatches each packet to the trust context selected by its name’s namespace (longest-prefix match) and validates against that context’s schema and anchors only — never “any anchor I happen to hold.” The default policy is deny: validation fails unless the selected context authorizes the (data_name, key_name) pair and the signature and certificate chain all check out.

Trust policy is a TrustSchema (crates/security/ndn-security/src/trust_schema.rs):

  • TrustSchema::new() is empty — it rejects everything.
  • TrustSchema::hierarchical() requires the data name and key name to share a top-level component (full hierarchy enforcement is the chain walk’s job; the schema fixes the namespace). This is the default a KeyChain hands out.
  • TrustSchema::accept_all() accepts any signed packet regardless of name relationship — for the AcceptSigned profile and tests only.

KeyChain::validator() builds a validator over the identity’s anchors with TrustSchema::hierarchical(), and KeyChain::trust_only(prefix) builds a consumer-side validator that trusts only certificates under a given anchor prefix. You reach an accept-all posture only by asking for it by name.

Interoperable trust schemas (LVS)

Beyond ndn-rs’s native SchemaRules, a schema can import a LightVerSec (LVS) model from the TLV binary format used by python-ndn, NDNts (@ndn/lvs), and ndnd, via TrustSchema::from_lvs_binary(wire) (crates/security/ndn-security/src/lvs.rs). The two rule sources are OR’d: allows returns true if either the native rules or the imported LVS model permits the pair. The import is fail-closed — a schema that uses user functions ($eq, $regex, …) is rejected with LvsError::UserFunctionsNotSupported rather than loaded and silently mis-enforced, unless a handler registry is supplied via from_lvs_binary_with_user_fns.

The signing side

Signing is symmetric to validation and also flows through KeyChain (crates/security/ndn-security/src/keychain.rs), the single entry point for NDN security in both applications and the forwarder. Construct one with KeyChain::ephemeral(name) (in-memory, self-signed, ideal for tests and short-lived producers), KeyChain::open_or_create(path, name) (file-backed PIB that generates on first run and reloads thereafter), or KeyChain::from_parts (for framework code that builds a SecurityManager first).

The Signer trait (crates/security/ndn-security/src/signer.rs) abstracts the signing key; concrete signers include Ed25519Signer, EcdsaP256Signer (for ndn-cxx interop, which lacks Ed25519), HmacSha256Signer, and the plain/keyed Blake3Signer/Blake3KeyedSigner. A signer exposes sig_type() and both an async sign() and a defaulted sign_sync() (the default refuses rather than panics, so signers whose keys live behind an async boundary surface a recoverable error).

Which key signs a given packet is chosen by a SignerSelection (crates/security/ndn-security/src/signing_info.rs), wrapped in a SigningInfo and passed to KeyChain::sign_packet:

SignerSelectionResolves to
Identity(name)The identity’s default key.
Key(name)A specific named key from the key store.
Cert(name)The key behind a named certificate (which pins the KeyLocator).
HmacKey(name)A named HMAC key (kept distinct from Key for future HMAC-only policy).
DigestDigestSha256 — integrity only, no key.
Suggested { for_name }The schema’s recommended signer (falls back to the default key until LVS-driven selection is wired).

KeyChain::sign_packet routes Digest through the sign_digest_sha256 fast path and resolves every other selection against the local PIB.

The forwarder’s invariant

Type-enforced verification pays off at the forwarder too. The design goal in ADR 0002 is that only SafeData is forwarded on a validating face. In ndn-engine this is realised by the validation stage (crates/forwarding/ndn-engine/src/stages/validation.rs): on a face that carries a Validator, a Data packet that fails validation is dropped (Action::Drop(DropReason::ValidationFailed)) and never propagates, while a packet that validates sets ctx.verified = true on its PacketContext. The Content Store admission gate (crates/forwarding/ndn-engine/src/stages/cs.rs) then admits only ctx.verified Data, so unverified bytes cannot poison the cache for downstream consumers.

One honest nuance for contributors: at the forwarder the trust verdict travels as a boolean PacketContext::verified flag rather than as the SafeData type itself — the SafeData/Unverified type discipline is the application-layer enforcement, and the pipeline mirrors it with the flag. Both express the same invariant: data that cannot be authenticated does not flow where authenticated data is required.

See also