Skip to content

Commit

Permalink
lvs: printUserFns
Browse files Browse the repository at this point in the history
  • Loading branch information
yoursunny committed Nov 19, 2024
1 parent 83e1fbd commit 51f25db
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 21 deletions.
43 changes: 40 additions & 3 deletions pkg/lvs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

This package is part of [NDNts](https://yoursunny.com/p/NDNts/), Named Data Networking libraries for the modern web.

This package implements [python-ndn Light VerSec (LVS)](https://python-ndn.readthedocs.io/en/latest/src/lvs/lvs.html) binary format.
It is still in design stage and not yet usable.
This package implements [Light VerSec (LVS)](https://python-ndn.readthedocs.io/en/latest/src/lvs/lvs.html).
In particular, this package can import the [LVS model binary format](https://python-ndn.readthedocs.io/en/latest/src/lvs/binary-format.html) and convert to NDNts native trust schema format in `@ndn/trust-schema` package.

## Compile LVS Model with python-ndn and Import into NDNts

This package can only import LVS binary format, but does not support LVS textual format.

To compile LVS textual format to binary format, you need to use python-ndn:

Expand All @@ -19,4 +23,37 @@ pip install 'python-ndn[dev] @ git+https://github.com/named-data/python-ndn@61ae
python ./pkg/lvs/compile.py <~/lvs-model.txt >~/lvs-model.tlv
```

The compiled binary TLV will be importable into NDNts in the future.
To import the LVS binary format, decode the TLV into **LvsModel** structure.
The example below works with the model given in [python-ndn LVS tutorial](https://github.com/named-data/python-ndn/blob/96ae4bfb0060435e3f19c11d37feca512a8bd1f5/docs/src/lvs/lvs.rst#tutorial).

```ts
import { LvsModel, toPolicy, printUserFns } from "@ndn/lvs";

// other imports for examples
import { Decoder } from "@ndn/tlv";
import { printESM } from "@ndn/trust-schema";
import assert from "node:assert/strict";
import fs from "node:fs/promises";

const model = Decoder.decode(await fs.readFile("./test-fixture/tutorial.tlv"), LvsModel);
```

## Translate to TrustSchemaPolicy

Use **toPolicy** to translate the LVS model to **TrustSchemaPolicy** as defined in `@ndn/trust-schema` package.
The policy and the referenced user functions can be printed as ECMAScript modules with **printESM** and **printUserFns** functions.

```ts
const policy0 = toPolicy(model, toPolicy.forPrint);
console.group("lvsPolicy.mjs");
console.log(printESM(policy0));
console.groupEnd();
console.group("lvsUserFns.mjs");
console.log(printUserFns(policy0));
console.groupEnd();
```

1. Save the output of `printESM` as `lvsPolicy.mjs`.
2. Save the output of `printUserFns` as `lvsUserFns.mjs`.
3. Fill in the skeletons of user functions.
4. Import `policy` from `lvsPolicy.mjs` in your application.
1 change: 1 addition & 0 deletions pkg/lvs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@ndn/tlv": "workspace:^",
"@ndn/trust-schema": "workspace:^",
"@ndn/util": "workspace:*",
"mnemonist": "^0.39.8",
"tslib": "^2.8.1"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion pkg/lvs/src/mod.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./print-userfn";
export * as lvstlv from "./tlv";
export * from "./translate";
export { LvsModel } from "./tlv";
export { toPolicy, type UserFn, type Vtable, type VtableInput } from "./translate";
38 changes: 38 additions & 0 deletions pkg/lvs/src/print-userfn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { TrustSchemaPolicy } from "@ndn/trust-schema";
import { assert } from "@ndn/util";

import { neededFnsMap } from "./translate";

/**
* Print user functions as ECMAScript module.
* @param policy - Policy generated by {@link toPolicy}.
*
* @throws Error
* Policy is not generated by {@link toPolicy}.
*/
export function printUserFns(policy: TrustSchemaPolicy): string {
const neededFns = neededFnsMap.get(policy);
assert(neededFns, "policy is not translated by toPolicy");

const lines: string[] = [
"import { assert } from \"@ndn/util\";",
"/** @typedef {import(\"@ndn/packet\").Component} Component */",
];
for (const [fn, nargSet] of neededFns) {
lines.push(
"",
"/**",
` * LVS user function ${fn}.`,
" * @param {Component} value",
" * @param {ReadonlyArray<Component>} args",
" * @returns {boolean}",
" */",
`export function ${fn}(value, args) {`,
` assert([${Array.from(nargSet).toSorted((a, b) => a - b).join(", ")}].includes(args.length));`,
" // TODO",
" return false;",
"}",
);
}
return lines.join("\n");
}
79 changes: 64 additions & 15 deletions pkg/lvs/src/translate.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,74 @@
import { type Component, Name } from "@ndn/packet";
import { pattern as P, type printESM, TrustSchemaPolicy } from "@ndn/trust-schema";
import { assert } from "@ndn/util";
import DefaultMap from "mnemonist/default-map.js";

import { type ConsOption, type Constraint, type LvsModel, type Node, type PatternEdge, type UserFnCall, ValueEdge } from "./tlv";

export function toPolicy(model: LvsModel, vtable: VtableInput = {}): TrustSchemaPolicy {
vtable = vtable instanceof Map ? vtable : new Map(Object.entries(vtable));
export type UserFn = (value: Component, args: readonly Component[]) => boolean;
export type Vtable = ReadonlyMap<string, UserFn>;
export type VtableInput = Vtable | Record<string, UserFn>;

/**
* Translate LVS model to TrustSchemaPolicy.
* @param model - LVS model.
* @param vtable - User functions.
* @returns Executable policy.
*
* @throws Error
* Malformed LVS model.
* Missing user functions.
*/
export function toPolicy(model: LvsModel, vtable?: VtableInput): TrustSchemaPolicy;

/**
* Translate LVS model to TrustSchemaPolicy without linking user functions.
* @param model - LVS model.
* @param forPrint - {@link toPolicy.forPrint} symbol.
*
* @returns Possibly incomplete policy.
* If the LVS model references user functions, the policy will not execute successfully.
* The policy can be serialized with {@link printESM} and {@link printUserFns}.
*/
export function toPolicy(model: LvsModel, forPrint: typeof toPolicy.forPrint): TrustSchemaPolicy;

export function toPolicy(model: LvsModel, arg2: VtableInput | typeof toPolicy.forPrint = {}): TrustSchemaPolicy {
const vtable: Vtable = arg2 instanceof Map ? arg2 :
new Map(arg2 === toPolicy.forPrint ? [] : Object.entries(arg2));
const translator = new Translator(model, vtable);
translator.translate();
if (arg2 !== toPolicy.forPrint) {
const { missingFns } = translator;
if (missingFns.length > 0) {
throw new Error(`missing user functions: ${missingFns.join(" ")}`);
}
}
return translator.policy;
}
export namespace toPolicy {
export const forPrint = Symbol("@ndn/lvs#toPolicy.forPrint");
}

export type UserFn = (value: Component, args: readonly Component[]) => boolean;
export type Vtable = ReadonlyMap<string, UserFn>;
export type VtableInput = Vtable | Record<string, UserFn>;
export const neededFnsMap = new WeakMap<TrustSchemaPolicy, ReadonlyMap<string, ReadonlySet<number>>>();

class Translator {
constructor(
private readonly model: LvsModel,
private readonly vtable: Vtable,
) {}
) {
neededFnsMap.set(this.policy, this.neededFns);
}

public readonly policy = new TrustSchemaPolicy();

public get missingFns(): string[] {
return Array.from(this.neededFns.keys()).filter((fn) => !this.vtable.get(fn));
}

private readonly tagSymbols = new Map<number, string>();
private readonly patternNames = new Map<string, number>();
private readonly wantedNodes = new Set<number>();
public readonly neededFns = new Set<string>();
private readonly neededFns = new DefaultMap<string, Set<number>>(() => new Set<number>());
private lastAutoId = 0;

public translate(): void {
Expand Down Expand Up @@ -134,7 +177,7 @@ class Translator {
}

private trCall(call: UserFnCall): P.VariablePattern.Filter {
this.neededFns.add(call.fn);
this.neededFns.get(call.fn).add(call.args.length);
return new LvsFilter(this.vtable, call.fn, Array.from(call.args, (a) => {
if (a.value !== undefined) {
return a.value;
Expand Down Expand Up @@ -184,10 +227,11 @@ class LvsFilter implements P.VariablePattern.Filter, printESM.PrintableFilter {
const { indent, imports } = ctx;
imports.get("./lvsuserfns.mjs").add("* as lvsUserFns");

const lines: string[] = [];
lines.push(`${indent}{`);
lines.push(`${indent} accept(name, vars) {`);
lines.push(`${indent} const args = [`);
const lines: string[] = [
`${indent}{`,
`${indent} accept(name, vars) {`,
`${indent} const args = [`,
];
for (const b of this.binds) {
if (typeof b === "string") {
lines.push(`${indent} vars.get(${JSON.stringify(b)})?.get(0),`);
Expand All @@ -197,9 +241,14 @@ class LvsFilter implements P.VariablePattern.Filter, printESM.PrintableFilter {
}
}
lines.push(`${indent} ];`);
lines.push(`${indent} return args.every(a => !!a) && lvsUserFns.${this.fn}(name.at(0), args);`);
lines.push(`${indent} }`);
lines.push(`${indent}}`);
if (this.binds.length > 0) {
lines.push(`${indent} void vars;`);
}
lines.push(
`${indent} return args.every(a => !!a) && lvsUserFns.${this.fn}(name.at(0), args);`,
`${indent} }`,
`${indent}}`,
);
return lines.join("\n");
}
}
16 changes: 16 additions & 0 deletions pkg/lvs/test-fixture/tutorial.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// https://github.com/named-data/python-ndn/blob/96ae4bfb0060435e3f19c11d37feca512a8bd1f5/docs/src/lvs/lvs.rst#tutorial

// The platform prefix definition. The pair of quotes means that it can only be matched by the identical component.
#platform: "ndn"/"blog"
// The certificate name suffix definition. Each underscore can be matched by an arbitrary pattern except that contains slash.
#KEY: "KEY"/_/_/_
// The root certificate definition, i.e., /ndn/blog/KEY/<key-id>/<issuer>/<cert-id>.
#root: #platform/#KEY
// Admin's certificate definition. The non-sharp patterns, role and adminID, are sent from the application. Each pattern can match an arbitrary components, but the matched components for the same pattern should be the same. The constraint shows that the component "_role" must be "admin". The underscore means that the matched components for the pattern "_role" may not be identical in the chain. The admin's certificate must be signed by the root certificate.
#admin: #platform/_role/adminID/#KEY & {_role: "admin"} <= #root
// author's certificate definition. The ID is verified by a user function. Both constraints must be met. It can only be signed by the admin's certificate.
#author: #platform/_role/ID/#KEY & {_role: "author", ID: $isValidID()} <= #admin
// author's and reader's certificate definition. The role can be either "reader" or "author". The ID is verified by a user function. Both constraints must be met. It can only be signed by the admin's certificate.
#user: #platform/_role/ID/#KEY & {_role: "reader"|"author", ID: $isValidID()} <= #admin
// article's trust schema. The component "year" is verified by a user function. The article can be signed by the admin's certificate or one author's certificate.
#article: #platform/ID/"post"/year/articleID & {year: $isValidYear()} <= #admin | #author
Binary file added pkg/lvs/test-fixture/tutorial.tlv
Binary file not shown.
10 changes: 8 additions & 2 deletions pkg/lvs/tests/pyndn.t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import "@ndn/packet/test-fixture/expect";

import { Certificate, generateSigningKey, KeyChain } from "@ndn/keychain";
import { Component, Data, Name, ValidityPeriod } from "@ndn/packet";
import { TrustSchema, TrustSchemaSigner } from "@ndn/trust-schema";
import { printESM, TrustSchema, TrustSchemaSigner } from "@ndn/trust-schema";
import { expect, test, vi } from "vitest";

import { toPolicy, type UserFn } from "..";
import { printUserFns, toPolicy, type UserFn } from "..";
import { pyndn0, pyndn1, pyndn2, pyndn3, pyndn4 } from "../test-fixture/lvstlv";

test("pyndn0", () => {
Expand Down Expand Up @@ -82,6 +82,12 @@ test("pyndn2", () => {

test("pyndn3", () => {
const model = pyndn3();
expect(() => toPolicy(model)).toThrow(/missing user functions.*\$fn/);

const policyPrintable = toPolicy(model, toPolicy.forPrint);
expect(policyPrintable.match(new Name("/x/y"))).toHaveLength(0);
expect(printESM(policyPrintable)).toContain("$fn");
expect(printUserFns(policyPrintable)).toContain("$fn");

const $fn = vi.fn<UserFn>();
const policy = toPolicy(model, { $fn });
Expand Down
2 changes: 2 additions & 0 deletions pkg/trust-schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ The compiler implementation is found to have several limitations.
python-ndn library authors defined **Light VerSec (LVS)**, a lightweight modification of VerSec that focuses on signing key validation.
Its [syntax and semantics](https://python-ndn.readthedocs.io/en/latest/src/lvs/lvs.html) are similar to VerSec.
For ease of processing, LVS introduced some restrictions on identifier names and token ordering.
This package can import a subset of LVS models from its textual format via `versec.load()` function.
See `@ndn/lvs` package for more complete LVS support via its binary format.

## Trust Schema Representation

Expand Down

0 comments on commit 51f25db

Please sign in to comment.