endojs / Jessie

Tiny subset of JavaScript for ocap-safe universal mobile code
Apache License 2.0
281 stars 16 forks source link

protect and suspect #43

Closed michaelfig closed 2 years ago

michaelfig commented 4 years ago

@erights please review and comment. Please also tag anybody else you think should have a look.

I would like to propose an addition to the Jessie Standard Library #29: a protect(x) and suspect(y) function.

The intent would be to use them as follows within a Jessie module:

// Boilerplate preamble.
'use jessie';
import { protect, suspect } from 'jessie-std';

// Idiom for importing from a module and marking it and its transitive
// outputs as suspected (not allowed to capture protected this-values)
import $i_foo from 'foo';
const foo = suspect($i_foo);

// Idiom for declaring an export and its transitive outputs as protected
// (hardened, and no this-capture).
export const bar = protect((a, b, c) => ({a, bc: [foo(b), foo(c)]}));

The goal is the airtight prevention of accidental this-capture, also (as a side benefit) preventing the escape of any unhardened bindings/values.

The Rules are:

  1. Primitives need no protection and are never suspect: A protect(x) or suspect(x) call on a primitive x returns x itself.
  2. Conveyance of protection over time: protect(x) for an object x, calls harden(x), then returns a membrane so that when function properties such as x.f are called, its inbound arguments are wrapped with x.f(...suspect(args)) then invokes protect(ret) on any outbound return (or thrown) value.
  3. Contagion of suspicion over time: suspect(y) for an object y, returns a membrane so that when function properties such as y.g are called, it first ensures that the outbound this value is not protected within the context defined by the makeModuleHelpers() call, then its outbound arguments are wrapped with y.g(...protect(args)) then invokes suspect(ret) on any inbound return (or thrown) value.
  4. The Jessie grammar will statically enforce that protect and suspect are used when necessary. At a minimum, all imported bindings/values must be suspected before use (except for imports from @agoric/jessie itself), and all module-level bindings (exported or not) must be protected (except for raw identifiers and the binding of protect and suspect).
michaelfig commented 4 years ago

In thinking about it, I would instead of makeProtectionContext() use the name makeModuleHelpers(), so that we could hang other useful context-dependent functions off of it.

I'm thinking a simple console object would be useful for this kind of thing. It's pure-ish in the sense that it would carry no observable state (write-only). console is useful to scope to a module. We could attenuate the global console to add a prefix identifying the module, and not propagate any difficult-to-implement extensions.

dckc commented 4 years ago

... We could attenuate the global console to add a prefix identifying the module

something like that (along with filtering some control characters) is pretty important to prevent spoofing.

cf

coder-mike commented 4 years ago
  1. Are protect and suspect alternatives to the proposed insulate pattern of protection? I.e. are users expected to protect at the module boundary level with protect rather than at the function boundary level with insulate?

  2. Are protect and suspect required for a program to be considered a valid Jessie program?

michaelfig commented 4 years ago
  1. Are protect and suspect alternatives to the proposed insulate pattern of protection? I.e. are users expected to protect at the module boundary level with protect rather than at the function boundary level with insulate?

Yes. That's the expectation. Do note that module-level bindings (not limited to exports) must be both const and protected in order to prevent the leakage of mutable state.

  1. Are protect and suspect required for a program to be considered a valid Jessie program?

I would assert this, but @erights has in the past suggested that we could admit Jessie programs that had the corresponding explicit harden calls in them. I believe this requirement is better served and easier to explain by the protect membrane, rather than a separate compilation step to automatically insert the harden calls.

So, in my view, they are needed to be valid Jessie, and enforced by the 'use jessie' directive. However, your runtime may do something different with its implementation of jessie-std, such as not actually creating a membrane. Being looser in an implementation is tolerable, but I'd expect not to add more requirements for the developer-facing API to which Jessie modules must conform once we have some reference implementations.

The use of a membrane grants us the most future flexibility.

coder-mike commented 4 years ago

I would say the following module leaks mutable state (and is not valid Jessie code):

export const obj = { x: 10 }; // x is leaked mutable state

Is this the kind of exported mutable state that protect is protecting against?

But isn't the following leaking essentially the same mutable state, as viewed from the outside, but is valid Jessie code (?) :

'use jessie';
import { makeModuleHelpers } from 'jessie-std';
const { protect, suspect } = makeModuleHelpers();

// obj is an object with a mutable `x` property
export const obj =  protect((() => {
  let x = 10;
  return {
    get x() { return x; }
    set x(value) { x = value; }
  };
})());

(disclaimer: I haven't read the implementation of protect yet, so I may be misunderstanding its intended purpose)

michaelfig commented 4 years ago

Getters and setters are not in Jessie.

The idea of a method closing over some internal-to-a-function state is fine, because the state is not shared among clients of the module. An exported mutable slot (rather than a "maker" function that closes over state) can be a communications channel between otherwise unconnected callers. The module-level state is a violation of ocap principles, but the "maker" state is not.

michaelfig commented 4 years ago

Ah, I missed your IIFE. No, the IIFE call is not valid Jessie. Module-level definitions cannot be anything except for (hardened) pure data (of which lambdas are one kind of pure data). The only way for Jessie code to evaluate something is for an external client to call them.

michaelfig commented 4 years ago

Which leaves the question of how the makeModuleHelpers() call is allowed. I think it can be the only exception to this rule, just as the destructuring of its return value is an exception.

michaelfig commented 4 years ago

I think I would prefer:

'use jessie';
import { protect, suspect } from 'jessie-std';

Which would make this exception to the rules less weird for Jessie writers. It should be up to the host for how to implement jessie-std for a given module. The plain-Javascript implementation of jessie-std would not necessarily be very nice, but still be possible.

michaelfig commented 4 years ago

I changed the summary and description to use the above instead of the (old):

'use jessie';
import { makeModuleHelpers } from 'jessie-std';
const { protect, suspect } = makeModuleHelpers();
coder-mike commented 4 years ago

Ok, that makes it clearer.

The Jessie readme says that getters and setters are in Jessie

image

But really these are syntactic convenience. Exactly the same thing is possible with explicit functions like getX and setX. But you're saying that arbitrary expressions are not allowed at the module level, so the IIFE is illegal -- that makes sense.

coder-mike commented 4 years ago

If the semantics of protect from 'jessie-std' are important to the security guarantees that Jessie provides, how do you statically verify that protect resolves to the correct implementation? E.g. that the jessie-std library hasn't been spoofed?

A Jessie module is meant to have the same semantics when running as SES code, but SES doesn't guarantee any particular resolution of the module specifier 'jessie-std', especially in the face of Compartment import hooks.

michaelfig commented 4 years ago

If the semantics of protect from 'jessie-std' are important to the security guarantees that Jessie provides, how do you statically verify that protect resolves to the correct implementation? E.g. that the jessie-std library hasn't been spoofed?

I would say that if somebody has obtained your Jessie source code and is running it under an environment that doesn't implement protect properly, they get what they pay for. Your Jessie code cannot be in any way privileged except for what is passed to it from other modules, so "you" (the Jessie module author) don't miss anything if "your" code misbehaves, it's the user of the Compartment API that does.

That's the flexibility of Compartments, and a good reason not to use them lightly if you care about correctness of the code that you run.

coder-mike commented 4 years ago

I have to get my head around that :-)

I think what confuses me is that protect looks like it's effectively part of the language. It's required on the syntax level when declaring anything at the module scope (correct me if I'm wrong), in the same way that class is required on the syntax level in JS to declare a class, for example. But the meaning of the class syntax doesn't/can't change depending on the larger program in which the module containing the class is running, whereas the meaning of protect can.

Perhaps it would help my understanding if I could see a description of the security guarantees that protect gives us (i.e. why we need protect at all -- what would be broken if Jessie didn't demand it?). Does such a description exist anywhere?

michaelfig commented 4 years ago

But the meaning of the class syntax doesn't/can't change depending on the larger program in which the module containing the class is running, whereas the meaning of protect can.

Not really. The meaning of class can change if you preprocess the sources before passing them to an evaluator. Compartment is an evaluator, so it is not surprising that it can "break" code that is passed to it for evaluation.

What matters is that the Jessie guarantees are maintained, relative to that host. Compartment is creating a new host, with potentially compatible or incompatible changes compared to the original JS host.

michaelfig commented 4 years ago

Perhaps it would help my understanding if I could see a description of the security guarantees that protect gives us (i.e. why we need protect at all -- what would be broken if Jessie didn't demand it?). Does such a description exist anywhere?

The wider context is how SES is based on object-capabilities. If you're not yet familiar with ocaps, I'd refer you to @dckc's excellent https://github.com/dckc/awesome-ocap

michaelfig commented 2 years ago

Closing as: explored but a different approach will likely be used.