[E-Lang] Rights Amplification: The Next Layer Up
Mark S. Miller
markm@caplet.com
Tue, 05 Sep 2000 17:30:07 -0700
[Dean, please read carefully. It's rather different than what we agreed on.]
The Problem
Now that we've committed to Java 2, I'm taking the opportunity to rewrite
the implementation of Object-Pluribus (the "proxy-comm" system) to take
advantage of the new weak pointers. The rewrite is looking substantially
simpler than the current implementation, and it has full message pipelining!
Of course, any rewrite is an opportunity for opportunities. The biggest
thing missing from E is persistence, and both persistence and comm have at
their core serialization. Objects need to serialize themselves very
differently to these two streams: objects encapsulating precious secrets or
authority need to not serialize them to the comm stream, of course, as that
is being transmitted to untrusted machines. OTOH, it is these precious
things that most desperately want to be serialized to the persistence
stream, so they won't disappear when Windows crashes.
So far, E has been built with special knowledge of these two in the TCB.
However, a rewrite is also an opportunity for shrinking the TCB. The comm
system was designed to not need any special privileges, and indeed, that is
how it should be run since it represents untrusted other machines.
Therefore, I've been trying to write it so it can be written in E, and (once
there's an E compiler) I will try to rewrite it again in regular
unprivileged E code.
So instead of two serialization subsystems (comm & persistence), we can have
any number. We need to enable objects to serialize themselves differently
depending on which subsystem is asking. However, the protocol enabling this
customization must not be abusable by other objects. In particular, a
normal client of an object must not be able to say
"Please serialize your crown jewels on to this here stream. It really
is the persistence stream. Trust me."
On the other hand, the persistence subsystem itself must find a
way to make essentially this same request, and be believed.
The Solution
The answer, of course, is rights amplification. E's sealer/unsealer pairs
already provide most of the kernel mechanism needed. The remaining kernel
mechanism, presented below, provides a universal method for attempting to
rights-amplify any object: getOptSealedFacet/1. (A universal method, or a
Miranda Method, is one that all objects are assumed to respond to.) This
ability is useful for many "meta-level" issues besides persistence:
* equality: sameness testing & hashing
* verifying an object instantiates an audited expression
and therefore has the properties that auditor ensures
* equivalent of Java's package-scope pattern of rights amplification
* debugging (obviously requires rights amplification)
* application-specific needs for rights amplification, as in the MintMaker's
purses. (It's really ugly for normally clients to see that there's a
getDecr method, but be unable to use it.)
http://www.erights.org/elib/capability/ode/ode-capabilities.html#simple-money
The rest is just normal non-universal protocol design, patterns of use, and
some syntactic sugar.
(A naming note: I use "opt" for "optional" in the names of methods which may
return "null" to indicate absence of an answer, and I use "opt" in the names
of variables with may hold "null" to indicate absence of a value. Note that
"map get(foo)" is ok, even if "foo" may map to "null", since the "null" is a
valid answer, rather than the absence of an answer.)
New Universal Method: getOptSealedFacet(brand)
We define one new universal method. When written without sugar, a method
body would typically look something like:
to getOptSealedFacet(brand :Brand) :(nullOk(SealedBox)) {
switch (brand) {
match ==(FooSealer brand) {
FooSealer seal(...my facet for Foo...)
}
match ==(BarSealer brand) {
BarSealer seal(...my facet for Bar...)
}
# simple story used for now, to be corrected later
# match _ { null }
# the real story, to be explained later
match _ { meta getDefaultOptSealedFacet(self, brand) }
}
}
The request with the Foo brand means,
"Gimme a box which will unseal with the FooUnsealer. (Ie, a box sealed by
the FooSealer.) Inside the box should be whatever you feel appropriate
to provide to me, assuming I actually have the FooUnsealer. If you can't
(because you don't have the FooSealer) or won't, return null."
The Foo brand therefore represent a particular request, just as the unary
message name "foo" would. Having the FooSealer represents the ability to
meaningfully answer these requests. Having the FooUnsealer represents the
ability to meaningfully ask these requests.
A server (the one receiving the request) without a FooSealer can still react
to the making of the request, just not in a way that will satisfy to the
client. However, the client must understand that such a server may still
grab the brand and engage in other side effects, so a failed request may
still have effects. Neither of these should be problems for clients, as
long as they are aware of the issue.
On the flip side, while the server does know that the request came from a
client (someone who could send to the server), the server must not assume
anything more. In particular, the server must not assume the request came
from someone with a FooUnsealer. Therefore, the server should not perform
any side effects at all in its getOptSealedFacet method itself, but rather
only in the objects that this method places inside the sealed boxes. This
should not be a problem for servers, as long as they are aware of this issue.
Uses
Returning to serialization, a serialization subsystem would normally make
its Sealer widely available but keep its Unsealer to itself. Sealers that
are part of "system" services, such as comm and persistence, would be made
available in the "universal" name space. And object that wanted to
customize its serialization differently on each of these may write:
match ==(CommSealer brand) {
CommSealer seal(...my ambassador...)
}
match ==(PersistenceSealer brand) {
PersistenceSealer seal(...my next incarnation...)
}
to return an object to be serialized in lieu of itself. The replaceObject()
method of a given serialization stream may do:
to replaceObject(original) :any {
def optBox := original getOptSealedFacet(myUnsealer brand)
if (optBox == null) {
original
} else {
myUnsealer unseal(optBox)
}
Of course the programmer normally wants the language implementation to
magically take care of many meta-level issues for him. For example,
PassByCopy objects should serialize their instance variables by default
without the programmer needing to say anything further. Likewise for
debugging, equality, and auditor verification. That's why getOptSealedFacet
should default, not to returning null, but to
match _ { meta getDefaultOptSealedFacet(self, brand) }
"meta" is a keyword, and provides a syntactic escape for just such purposes.
The E BNF just specifies that "meta" may be followed by a message as if you
were synchronously calling it. However, the parsing actions dispatch on the
message name & arity to enforce specific rules and create an expression
parse node of a specific type. For getDefaultOptSealedFacet, the first
"argument" must be the name of a lexically enclosing object definition.
This expression provides the default system-defined meta-level behavior
for those cases not overridden by the programmer. This requires the TCB to
have prior knowledge of certain sealers, which is fine. The object itself may
not have access to some of these sealers, such as for equality, in which
case it can only override requests with those brands to deny service.
getDefaultOptSealedFacet() itself will default to returning null.
Next: A Pinch of New Syntactic Sugar
Many phone conversations with Dean and MarcS, and memories of Joule's
"Energetic Secrets", contributed enormously to these ideas. Thanks!
As always, fire away!
Cheers,
--MarkM