Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,7 @@ pub fn build(b: *std.Build) void {
b.step("test-ts-image-registry", "Compare compiled image registration and media texture ownership").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-image-registry-tests", &.{"compiled image registry"})).step);
b.step("test-ts-shell-coordination", "Compare compiled shell installation and rollback ownership").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-shell-coordination-tests", &.{"compiled shell coordination"})).step);
b.step("test-ts-shell-admission", "Compare complete compiled scene hierarchy and runtime descriptor admission").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-shell-admission-tests", &.{"compiled shell admission"})).step);
b.step("test-ts-authorization", "Compare compiled authorization and complete bridge responses against native references").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-authorization-tests", &.{"compiled authorization"})).step);
b.step("test-ts-bridge-descriptors", "Compare compiled bridge descriptors, exact identities and borrowed origin buffers").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-bridge-descriptor-tests", &.{"compiled bridge descriptors"})).step);
b.step("test-ts-system-admission", "Compare compiled OS-service descriptors, refusal and native buffer ownership").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-system-admission-tests", &.{"compiled system admission"})).step);
b.step("test-ts-font-registry", "Compare compiled font registration and refresh ownership").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-font-registry-tests", &.{"compiled font registry"})).step);
Expand Down
137 changes: 137 additions & 0 deletions packages/core/src/authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/** Complete byte-preserving authorization over native-owned policy tables.
* Lengths/counts are exact little-endian words, never hashes or truncated text.
* Native owns transport storage, JSON parsing, handlers and OS execution.
*/
type NscAuthorizationCommand = { name: Uint8Array; grants: Uint8Array[]; origins: Uint8Array[] };
class NscAuthorizationReader {
bytes: Uint8Array;
at: number;
constructor(bytes: Uint8Array) { this.bytes = bytes; this.at = 8; }
integer(): number {
if (this.at > this.bytes.length - 8) throw new Error("truncated authorization length");
const data = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
const low = data.getUint32(this.at, true), high = data.getUint32(this.at + 4, true);
this.at += 8;
if (high > 2097151) throw new Error("invalid authorization integer");
return low + high * 4294967296;
}
count(): number {
const value = this.integer();
if (value > this.bytes.length - this.at) throw new Error("invalid authorization extent");
return value;
}
field(): Uint8Array {
const count = this.count(), value = this.bytes.slice(this.at, this.at + count);
this.at += count;
return value;
}
list(): Uint8Array[] {
const count = this.count();
if (count > Math.floor((this.bytes.length - this.at) / 8)) throw new Error("invalid authorization table");
const values: Uint8Array[] = [];
for (let i = 0; i < count; i++) values.push(this.field());
return values;
}
commands(): NscAuthorizationCommand[] {
const count = this.count();
if (count > Math.floor((this.bytes.length - this.at) / 24)) throw new Error("invalid authorization commands");
const values: NscAuthorizationCommand[] = [];
for (let i = 0; i < count; i++) values.push({ name: this.field(), grants: this.list(), origins: this.list() });
return values;
}
finish(): void { if (this.at !== this.bytes.length) throw new Error("trailing authorization bytes"); }
}
function nscAuthorizationEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
function nscAuthorizationMember(values: Uint8Array[], value: Uint8Array, wildcard: boolean): boolean {
for (const candidate of values) {
if (wildcard && candidate.length === 1 && candidate[0] === 42) return true;
if (nscAuthorizationEqual(candidate, value)) return true;
}
return false;
}
function nscAuthorizationGrants(grants: Uint8Array[], required: Uint8Array[]): boolean {
for (const value of required) if (!nscAuthorizationMember(grants, value, false)) return false;
return true;
}
function nscAuthorizationPrefix(bytes: Uint8Array, prefix: readonly number[]): boolean {
if (bytes.length < prefix.length) return false;
for (let i = 0; i < prefix.length; i++) if (bytes[i] !== prefix[i]) return false;
return true;
}
function nscAuthorizationHttp(bytes: Uint8Array): number {
if (nscAuthorizationPrefix(bytes, [104, 116, 116, 112, 115, 58, 47, 47])) return 8;
if (nscAuthorizationPrefix(bytes, [104, 116, 116, 112, 58, 47, 47])) return 7;
return 0;
}
function nscAuthorizationExternal(action: boolean, allowed: Uint8Array[], url: Uint8Array): boolean {
if (!action) return false;
for (const pattern of allowed) {
if (pattern.length === 1 && pattern[0] === 42 || nscAuthorizationEqual(pattern, url)) return true;
if (pattern.length === 0 || pattern[pattern.length - 1] !== 42) continue;
const scheme = nscAuthorizationHttp(pattern);
if (scheme === 0) continue;
let slash = -1;
for (let i = scheme; i < pattern.length - 1; i++) if (pattern[i] === 47) { slash = i; break; }
if (slash <= scheme || url.length < pattern.length - 1) continue;
let same = true;
for (let i = 0; i < pattern.length - 1; i++) if (pattern[i] !== url[i]) { same = false; break; }
if (same) return true;
}
return false;
}
function nscAuthorizationBridge(enabled: boolean, grants: Uint8Array[], commands: NscAuthorizationCommand[], command: Uint8Array, origin: Uint8Array): boolean {
if (!enabled) return false;
for (const candidate of commands) {
if (!nscAuthorizationEqual(candidate.name, command)) continue;
return nscAuthorizationGrants(grants, candidate.grants) &&
(candidate.origins.length === 0 || nscAuthorizationMember(candidate.origins, origin, true));
}
return false;
}
function nscvAuthorization(request: Uint8Array): Uint8Array {
if (request.length < 8 || request[0] !== 88 || request[1] !== 1 || request[2]! > 6) throw new Error("invalid authorization header");
for (let i = 3; i < 7; i++) if (request[i]! > 1) throw new Error("invalid authorization flag");
if (request[7] !== 0) throw new Error("invalid authorization reserve");
const mode = request[2]!, reader = new NscAuthorizationReader(request);
const result = new Uint8Array(16), out = new DataView(result.buffer);
out.setUint32(0, 1, true); out.setUint32(4, mode, true);
let allowed = false, error = 0;
if (mode === 0) {
const grants = reader.list(), required = reader.field();
allowed = nscAuthorizationMember(grants, required, false);
} else if (mode === 1) {
const grants = reader.list(), required = reader.list();
allowed = nscAuthorizationGrants(grants, required);
} else if (mode === 2) {
const origins = reader.list(), origin = reader.field();
allowed = nscAuthorizationMember(origins, origin, true);
} else if (mode === 3 || mode === 6) {
const urls = reader.list(), url = reader.field();
if (mode === 6) {
const limit = reader.integer();
if (url.length === 0) error = 1;
else if (url.length > limit) error = 2;
else if (nscAuthorizationHttp(url) === 0) error = 1;
else for (const byte of url) if (byte <= 32 || byte === 127) { error = 1; break; }
}
allowed = error === 0 && nscAuthorizationExternal(request[3] === 1, urls, url);
if (mode === 6 && error === 0 && !allowed) error = 3;
} else if (mode === 4) {
const grants = reader.list(), commands = reader.commands(), command = reader.field(), origin = reader.field();
allowed = nscAuthorizationBridge(request[3] === 1, grants, commands, command, origin);
} else {
const security = reader.list(), grants = reader.list(), commands = reader.commands(), origins = reader.list();
const command = reader.field(), origin = reader.field(), permission = reader.field();
if (request[3] === 1) allowed = nscAuthorizationBridge(true, security.length > 0 ? security : grants, commands, command, origin);
else if (request[4] === 1 && request[5] === 1 && nscAuthorizationMember(origins, origin, true)) {
allowed = security.length === 0 || nscAuthorizationMember(security, permission, false) ||
nscAuthorizationMember(security, new Uint8Array([119, 105, 110, 100, 111, 119]), false);
}
}
reader.finish(); out.setUint32(8, allowed ? 1 : 0, true); out.setUint32(12, error, true);
return result;
}
1 change: 1 addition & 0 deletions packages/core/src/portable_sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"src/shell_admission.ts",
"src/system_admission.ts",
"src/bridge_descriptors.ts",
"src/authorization.ts",
"src/render_animation_lifecycle.ts",
"src/motion_sampling.ts",
"src/vector_raster.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/runtime_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export function native_window_policy(request: Uint8Array): Uint8Array {
if (request[0] === 85) return nscvShellAdmission(request);
if (request[0] === 86) return nscvSystemAdmission(request);
if (request[0] === 87) return nscvBridgeDescriptors(request);
if (request[0] === 88) return nscvAuthorization(request);
if (request[0] === 81) return nscvWindowLifecycle(request);
if (request[0] === 80) return nscvViewCoordination(request);
if (request[0] === 2) return nscvShellLayout(request);
Expand Down
67 changes: 67 additions & 0 deletions packages/core/test/authorization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import test from "node:test";
import { readFileSync } from "node:fs";
import { stripTypeScriptTypes } from "node:module";
const source = ["runtime_policy.ts", "authorization.ts"].map(n => readFileSync(new URL(`../src/${n}`, import.meta.url), "utf8")).join("\n");
const { nscvAuthorization: derive, native_window_policy: dispatch } = await import(`data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source) + "\nexport { nscvAuthorization };").toString("base64")}`);
type Bytes = string | Uint8Array;
type Command = { name: Bytes; grants?: Bytes[]; origins?: Bytes[] };
class Packet {
parts: Uint8Array[];
constructor(mode: number, flags = [0, 0, 0, 0]) { this.parts = [new Uint8Array([88, 1, mode, ...flags, 0])]; }
integer(value: bigint | number): this { const bytes = new Uint8Array(8); new DataView(bytes.buffer).setBigUint64(0, BigInt(value), true); this.parts.push(bytes); return this; }
field(value: Bytes): this { const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value; this.integer(bytes.length); this.parts.push(bytes); return this; }
list(values: Bytes[]): this { this.integer(values.length); for (const value of values) this.field(value); return this; }
commands(values: Command[]): this { this.integer(values.length); for (const value of values) this.field(value.name).list(value.grants ?? []).list(value.origins ?? []); return this; }
bytes(): Uint8Array { const bytes = new Uint8Array(this.parts.reduce((n, p) => n + p.length, 0)); let at = 0; for (const part of this.parts) { bytes.set(part, at); at += part.length; } return bytes; }
result(): [boolean, number] { const bytes = this.bytes(), actual = derive(bytes); assert.deepEqual(actual, dispatch(bytes)); const out = new DataView(actual.buffer); assert.equal(actual.length, 16); assert.equal(out.getUint32(0, true), 1); assert.equal(out.getUint32(4, true), bytes[2]); return [out.getUint32(8, true) === 1, out.getUint32(12, true)]; }
}
test("authorization preserves exact raw permission bytes and requires every grant", () => {
for (const value of ["", "window", "Window", "window\0", "*", new Uint8Array([255, 0, 254]), "x".repeat(8192)]) {
assert.deepEqual(new Packet(0).list([value]).field(value).result(), [true, 0]);
assert.deepEqual(new Packet(0).list(["*"]).field(value).result(), [value === "*", 0]);
assert.deepEqual(new Packet(1).list([value]).list([value, value]).result(), [true, 0]);
assert.deepEqual(new Packet(1).list([]).list([]).result(), [true, 0]);
assert.deepEqual(new Packet(1).list([value]).list([value, "missing"]).result(), [false, 0]);
}
});
test("origin authorization preserves wildcard scope and performs no normalization", () => {
for (const value of ["", "zero://inline", "https://A", new Uint8Array([255, 0]), "x".repeat(16384)]) {
assert.deepEqual(new Packet(2).list([value]).field(value).result(), [true, 0]);
assert.deepEqual(new Packet(2).list(["*"]).field(value).result(), [true, 0]);
assert.deepEqual(new Packet(2).list([]).field(value).result(), [false, 0]);
}
assert.deepEqual(new Packet(2).list(["https://a"]).field("https://A").result(), [false, 0]);
});
test("external-link patterns require an explicit action and preserve host/path boundaries", () => {
for (const [pattern, url, expected] of [["https://example.com/*", "https://example.com/docs", true], ["https://example.com/*", "https://example.com.evil/docs", false], ["https://example.com*", "https://example.com.evil/docs", false], ["https:///*", "https:///docs", false], ["HTTPS://a/*", "HTTPS://a/path", false], ["http://a/one*", "http://a/one/two", true], ["http://a/*/two*", "http://a/one/two", false], ["odd://a/*", "odd://a/x", false], ["odd://a/*", "odd://a/*", true], ["*", "", true]] as const) {
assert.deepEqual(new Packet(3, [1, 0, 0, 0]).list([pattern]).field(url).result(), [expected, 0]);
assert.deepEqual(new Packet(3).list([pattern]).field(url).result(), [false, 0]);
}
});
test("bridge admission uses the first duplicate command and does not widen permission wildcards", () => {
const commands = [{ name: "one", grants: ["window"], origins: ["zero://inline"] }, { name: "one" }];
for (const enabled of [0, 1]) for (const grants of [[], ["*"], ["window"]]) for (const origin of ["zero://inline", "elsewhere"]) {
assert.deepEqual(new Packet(4, [enabled, 0, 0, 0]).list(grants).commands(commands).field("one").field(origin).result(), [enabled === 1 && grants[0] === "window" && origin === "zero://inline", 0]);
}
assert.deepEqual(new Packet(4, [1, 0, 0, 0]).list([]).commands([{ name: "two" }]).field("two").field("").result(), [true, 0]);
});
test("builtin fallback preserves explicit policy precedence and the window compatibility grant", () => {
const commands = [{ name: "one", grants: ["clipboard"], origins: ["allowed"] }];
for (const explicit of [0, 1]) for (const js of [0, 1]) for (const present of [0, 1]) for (const grants of [[], ["window"], ["clipboard"], ["missing"]]) for (const origin of ["allowed", "denied"]) {
const expected = explicit === 1 ? (grants.length === 0 || grants[0] === "clipboard") && origin === "allowed" : js === 1 && present === 1 && origin === "allowed" && (grants.length === 0 || grants[0] === "window" || grants[0] === "clipboard");
assert.deepEqual(new Packet(5, [explicit, js, present, 0]).list(grants).list(["clipboard"]).commands(commands).list(["allowed"]).field("one").field(origin).field("clipboard").result(), [expected, 0]);
}
});
test("external URL admission preserves ordered errors and the complete byte limit", () => {
for (const [url, expected] of [["", 1], [" ".repeat(33), 2], ["HTTPS://a/x", 1], ["http://a/\0", 1], ["http://a/\x7f", 1], ["https://a/x", 3]] as const) assert.deepEqual(new Packet(6).list(["*"]).field(url).integer(32).result(), [false, expected]);
assert.deepEqual(new Packet(6, [1, 0, 0, 0]).list(["*"]).field("http://").integer(32).result(), [true, 0]);
assert.deepEqual(new Packet(6, [1, 0, 0, 0]).list(["*"]).field(new Uint8Array([104,116,116,112,58,47,47,255])).integer(32).result(), [true, 0]);
});
test("authorization refuses every truncated table and unsafe extent before reading bytes", () => {
const bytes = new Packet(5, [1, 1, 1, 0]).list(["window"]).list(["clipboard"]).commands([{ name: "one", origins: ["*"] }]).list(["zero://inline"]).field("one").field("zero://inline").field("clipboard").bytes();
for (let end = 0; end < bytes.length; end++) assert.throws(() => derive(bytes.slice(0, end)));
const extra = new Uint8Array(bytes.length + 1); extra.set(bytes); assert.throws(() => derive(extra));
for (const count of [4294967296n, 9007199254740992n, (1n << 64n) - 1n]) assert.throws(() => derive(new Packet(0).integer(count).field("").bytes()));
for (const [at, value] of [[0, 87], [1, 2], [2, 7], [3, 2], [7, 1]]) { const bad = bytes.slice(); bad[at!] = value!; assert.throws(() => derive(bad)); }
});
1 change: 1 addition & 0 deletions src/runtime/api.zig
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ pub fn App(comptime Runtime: type) type {
shell_admission_policy: ?*const fn ([]const u8, []u8) usize = null,
system_admission_policy: ?*const fn ([]const u8, []u8) usize = null,
bridge_descriptor_policy: ?*const fn ([]const u8, []u8) usize = null,
authorization_policy: ?*const fn ([]const u8, []u8) usize = null,
/// Portable command routing and status-item selection. Native retains
/// registry bytes, exact handles and OS capability execution.
command_policy: ?*const fn ([]const u8, []u8) usize = null,
Expand Down
Loading
Loading