UNPKG

@ndn/packet

Version:

NDNts: Network Layer Packets

273 lines (272 loc) 9.47 kB
import { assert, evict, getOrInsert, toHex } from "@ndn/util"; import { Interest } from "../interest_browser.js"; import { SigInfo } from "../sig-info_browser.js"; import { LLSign, LLVerify, Signer } from "./signing_browser.js"; /** Validation policy for SigInfo fields in signed Interest. */ export class SignedInterestPolicy { owned = new WeakMap(); trackedKeys; records = new Map(); rules; constructor(arg1, ...rules) { let opts = {}; if (typeof arg1?.check === "function") { rules.unshift(arg1); } else { opts = arg1 ?? {}; } assert(rules.length > 0, "no rules"); this.trackedKeys = opts.trackedKeys ?? 256; this.rules = rules; } /** * Assign SigInfo fields on an Interest before signing. * @param key - Signing key object to associate state with; if omitted, use global state. */ update(interest, key = this) { const si = Signer.putSigInfo(interest); for (const rule of this.rules) { rule.update(si, getOrInsert(this.owned, key, () => ({}))); } } /** * Check SigInfo of an Interest. * @returns A function to save state after the Interest has passed all verifications. */ check({ sigInfo }) { if (!sigInfo) { throw new Error("SignedInterestPolicy rejects unsigned Interest"); } const key = (() => { const klName = sigInfo.keyLocator?.name; if (klName) { return `N:${klName.valueHex}`; } const klDigest = sigInfo.keyLocator?.digest; if (klDigest) { return `D:${toHex(klDigest)}`; } return "_:"; })(); const state = this.records.get(key) ?? {}; const saves = this.rules.map((rule) => rule.check(sigInfo, state)); return () => { for (const save of saves) { save(); } this.records.delete(key); this.records.set(key, state); evict(this.trackedKeys, this.records); }; } /** * Wrap an Interest to update/check SigInfo during signing/verification. * * @remarks * During signing, global state is being used because signer key cannot be detected. */ wrapInterest(interest) { return new Proxy(interest, { get: (target, prop, receiver) => { switch (prop) { case LLSign.OP: { return (signer) => { this.update(interest); return interest[LLSign.OP](signer); }; } case LLVerify.OP: { return async (verify) => { const save = this.check(interest); await interest[LLVerify.OP](verify); save(); }; } } return Reflect.get(target, prop, receiver); }, }); } /** * Wrap a Signer to update SigInfo when signing an Interest. * * @remarks * State is associated with the provided Signer. */ makeSigner(inner) { return { sign: (pkt) => { if (pkt instanceof Interest) { this.update(pkt, inner); } return inner.sign(pkt); }, }; } /** Wrap a Verifier to check the policy when verifying an Interest. */ makeVerifier(inner, { passData = true, passUnsignedInterest = false, } = {}) { return { verify: async (pkt) => { if (!(pkt instanceof Interest)) { if (passData) { return inner.verify(pkt); } throw new Error("SignedInterestPolicy rejects non-Interest"); } if (!pkt.sigInfo && passUnsignedInterest) { return inner.verify(pkt); } const save = this.check(pkt); await inner.verify(pkt); save(); }, }; } } class NonceRule { nonceLength; minNonceLength; trackedNonces; constructor({ nonceLength = 8, minNonceLength = 8, trackedNonces = 256, }) { assert(nonceLength >= 1); assert(minNonceLength >= 1); assert(trackedNonces >= 1); this.nonceLength = nonceLength; this.minNonceLength = minNonceLength; this.trackedNonces = trackedNonces; } update(si, state) { let nonceHex; do { si.nonce = SigInfo.generateNonce(this.nonceLength); nonceHex = toHex(si.nonce); } while (state.nonces?.has(nonceHex)); this.recordNonce(state, nonceHex); } check(si, state) { if (!si.nonce || si.nonce.length < this.minNonceLength) { throw new Error("SigNonce is absent or too short"); } const nonceHex = toHex(si.nonce); if (state.nonces?.has(nonceHex)) { throw new Error("SigNonce is duplicate"); } return () => this.recordNonce(state, nonceHex); } recordNonce(state, nonceHex) { state.nonces ??= new Set(); state.nonces.add(nonceHex); evict(this.trackedNonces, state.nonces); } } class SequencedRuleBase { field; name; max; constructor(field, name, max) { this.field = field; this.name = name; this.max = max; } check(si, state) { const value = si[this.field]; if (value === undefined) { throw new Error(`${this.name} is absent`); } const prev = state[this.field]; if (prev !== undefined && value <= prev) { throw new Error(`${this.name} reordering detected`); } return () => { state[this.field] = this.max(value, state[this.field]); }; } } class TimeRule extends SequencedRuleBase { maxClockOffset; constructor({ maxClockOffset = 60000, }) { super("time", "SigTime", (value, prev = 0) => Math.max(value, prev)); assert(maxClockOffset >= 0); this.maxClockOffset = maxClockOffset; } update(si, state) { si.time = Math.max(Date.now(), 1 + (state.time ?? 0)); state.time = si.time; } check(si, state) { const save = super.check(si, state); const now = Date.now(); if (Math.abs(now - si.time) > this.maxClockOffset) { throw new Error("SigTime offset is too large"); } return save; } } class SeqNumRule extends SequencedRuleBase { beforeInitialSeqNum; constructor({ initialSeqNum = 0n, }) { // eslint-disable-next-line unicorn/prefer-math-min-max super("seqNum", "SigSeqNum", (value, prev = 0n) => value > prev ? value : prev); this.beforeInitialSeqNum = initialSeqNum - 1n; } update(si, state) { state.seqNum ??= this.beforeInitialSeqNum; si.seqNum = ++state.seqNum; } } (function (SignedInterestPolicy) { /** * Create a rule to assign or check SigNonce. * * @remarks * This rule assigns a random SigNonce of `nonceLength` octets that does not duplicate * last `trackedNonces` values. * * This rule rejects an Interest on any of these conditions: * - SigNonce is absent. * - SigNonce has fewer than `minNonceLength` octets. * - SigNonce value duplicates any of last `trackedNonces` values. */ function Nonce(opts = {}) { return new NonceRule(opts); } SignedInterestPolicy.Nonce = Nonce; /** * Create a rule to assign or check SigTime. * * @remarks * This rule assigns SigTime to be same as current timestamp, but may increment if it * duplicates the previous value. * * This rule rejects an Interest on any of these conditions: * - SigTime is absent. * - SigTime differs from current timestamp by more than `maxClockOffset` milliseconds. * - SigTime value is less than or equal to a previous value. * * This check logic differs from NDN Packet Format v0.3 specification (as of 2020-September) in * that `maxClockOffset` is checked on every Interest rather than only the "initial" Interest. * It is the same behavior as ndn-cxx v0.7.1 implementation. * This logic offers better consistency as it has less dependency on internal state of the * SignedInterestPolicy. However, persistently sending more than 1000 signed Interests per second * would eventually push SigTime out of `maxClockOffset` range and cause rejections. */ function Time(opts = {}) { return new TimeRule(opts); } SignedInterestPolicy.Time = Time; /** * Create a rule to assign or check SigSeqNum. * * @remarks * This rule assigns SigSeqNum to `initialSegNum`, or increments from previous value. * * This rule rejects an Interest on any of these conditions: * - SigSeqNum is absent. * - SigSeqNum value is less than or equal to a previous value. */ function SeqNum(opts = {}) { return new SeqNumRule(opts); } SignedInterestPolicy.SeqNum = SeqNum; })(SignedInterestPolicy || (SignedInterestPolicy = {}));