@ndn/packet
Version:
NDNts: Network Layer Packets
273 lines (272 loc) • 9.46 kB
JavaScript
import { assert, evict, getOrInsert, toHex } from "@ndn/util";
import { Interest } from "../interest_node.js";
import { SigInfo } from "../sig-info_node.js";
import { LLSign, LLVerify, Signer } from "./signing_node.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 = {}));