ERights Home data / serial / jhu-paper 
Back to: Manipulating Authority at the Exits x On to: Manipulating Identity at the Entries

Selective Transparency
within the Subgraph


Lessons of Selective Transparency within the Subgraph

(*** To be written)

A Working Pile System

We repair the remaining problem of the last chapter, and solve the problem of selective transparency. The solution builds on rights amplification, so we import the rights amplification abstraction available in the E library and explained in the Ode.

? pragma.syntax("0.8")

? def makeBrandPair := <import:org.erights.e.elib.sealing.makeBrand>

Without further delay, here's a correct implementation of the pile system, with the differences underlined. Explanations follow.

# E sample
def makePileUriGetter(<file>) :near {
    def [magicSealer, magicUnsealer] := makeBrandPair("Pile System")
    def magicBrand := magicSealer.getBrand()

    def <pile> {
        to getSealer() :any { magicSealer }
        to optUncall(obj) :nullOk[__Portrayal] {
            if (Ref.isNear(obj) &&
                  obj.__optSealedDispatch(magicBrand) =~
                  sealedBox :notNull) {

                magicUnsealer.unseal(sealedBox)
            } else {
                null
            }
        }
        to optUnget(obj) :nullOk[String] {
            if (<pile>.optUncall(obj) =~
                  [==<pile>, `get`, [path]]) {
                path
            } else {
                null
            }
        }
        to get(absPath :String) :any {
            def common {
                to __optSealedDispatch(brand) :any {
                    switch (brand) {
                        match ==magicBrand {
                            magicSealer.seal([<pile>,
                                              "get",
                                              [absPath]])
                        }
                        match _ { null }
                    }
                }
                to getSealer() :any { magicSealer }
            }
            if (absPath.endsWith("/")) {
                def directory extends common {
                    to __optSealedDispatch(brand) :any {
                        super.__optSealedDispatch(brand)
                    }
                    to optUncall(obj) :nullOk[__Portrayal] {
                        if (<pile>.optUncall(obj) =~
                              [==<pile>, `get`, [`$absPath@relPath`]]) {

                            if (relPath.size() >= 1) {
                                return [directory, "get", [relPath]]
                            }
                        }
                        null
                    }
                    to optUnget(obj) :nullOk[String] {
                        if (directory.optUncall(obj) =~
                              [==directory, `get`, [path]]) {
                            path
                        } else {
                            null
                        }
                    }
                    to get(relPath :String) :any {
                        <pile>[absPath + relPath]
                    }
                }
            } else {
                def file := <file>[absPath]
                require(!file.isDirectory())
                def normalPile extends common {
                    to __optSealedDispatch(brand) :any {
                        super.__optSealedDispatch(brand)
                    }
                    to getText() :String {
                        file.getText()
                    }
                    to setText(newText :String) :void {
                        file.setText(newText)
                    }
                }
            }
        }
    }
}

Besides the E already explained, this code uses the following constructs.

  • We have already seen some of E's Miranda Methods -- methods all objects are expected to respond to, and for which default implementations are provided. __optSealedDispatch(brand) is another one, whose semantics is explained below. Its default implementation is to return null.

  • When an object is defined with the syntax
        def <name> extends <expression> {...
    then the expression is evaluated, bound to the variable super in the scope of the object definition, and the object is defined to forward (delegate) all non-Miranda Methods it doesn't define to this super object. This can be used to implement inheritance, but here we use it only for delegation.

  • Since __optSealedDispatch is a Miranda Method, in order to delegate it to the super object, we must do so manually.

  • The sealer / unsealer pair introduced by the Ode is actually a triple. The third element is the brand, which can be obtained from the sealer, the unsealer, or any SealedBox created by the sealer. The brand conveys no authority. It provides only unforgeable uniqueness for uniquely labeling its triple.

  • E's switch expression contains match clauses rather than cases. Each match clause has a pattern as header and an expression as body. These patterns are matched against the specimen in sequence until one succeeds, whereupon that match's expression is evaluated to produce the value of the switch. The pattern _ matches anything, so a
        match _ {...
    at the end of a switch functions as a default case.

__optSealedDispatch(brand)

Recall we said earlier that a verb (message name, selector, order code) in E is just a string, so that anyone can send a message using any verb they like to any object they have. For capability programming this is usually an adequate choice, but it is not the only possible choice, and it is not always adequate. In the language T [ref Rees-T], verbs are true capabilities -- they are first class anonymous objects with unforgeable selfish identity. The names that appear as verbs in program text are just yet more variable names, to be lexically looked up in the caller's scope to find the actual verb object. Inspired by T, Joule goes even farther, with the ability to send a message being a sealer, the message itself being a SealedBox encapsulating its arguments, and the ability to receive a message and bind the arguments being an unsealer. The message dispatch mechanism mechanism is indexed by the brand.

Beyond providing the Miranda __optSealedDispatch method whose default implementation simply returns null, E has no built-in support for any such fancier message dispatch mechanism. Rather, we establish the convention that obj.__optSealedDispatch(brand) means "obj, if I had the unsealer for this brand, what would you like to say to me?". obj can only respond for those brands for which it has the sealer. For these, is dispatches to a match-clause that seals its response using the corresponding sealer, and returns the resulting SealedBox. The switch expression is analogous to the sequence of method definitions in an object definition, where each match-clause is analogous to a method. The caller of obj can only receive obj's response by unsealing this box, which requires the unsealer.

In our convention, the role of the sealer and unsealer are the reverse of Joule's. The two conventions are formally equivalent -- either can easily simulate the other with exactly the same security properties. Joule's convention is more convenient for use as a general purpose messaging mechanism. But since E already has a messaging mechanism for normal use, where rights amplification isn't needed, our convention seems to be more convenient for the remaining cases where it is.


Updoc starts afresh on each page, so each chapter needs its own setup.

? def makeSurgeon := <elib:serial.makeSurgeon>
? def surgeon := makeSurgeon.withSrcKit("de: ").diverge()

? def <pile> := makePileUriGetter(<file>)
# value: <pile__uriGetter>

? var ehomePath := interp.getProps()["e.home"]
? if (! ehomePath.endsWith("/")) {
>     ehomePath += "/"
> }
? ehomePath
# value: "c:/Program Files/erights.org/"

? def <ehome> := <pile>[ehomePath]
# value: <directory>

? surgeon.addLoader(<pile>, "pile__uriGetter")
# not yet - ? surgeon.addLoader(<ehome>, "ehome__uriGetter")

? def iAmEHome2 {
>     to __optUncall() :__Portrayal {
>         [<pile>, "get", [ehomePath]]
>     }
> }
# value: <iAmEHome2>

? def d1 := surgeon.serialize(<ehome>)
# value: "de: <pile>[\"c:/Program Files/erights.org/\"]"

? def d2 := surgeon.serialize(iAmEHome2)
# value: "de: <pile>[\"c:/Program Files/erights.org/\"]"

? d1 == d2
# value: true

? surgeon.unserialize(d1)
# value: <directory>

Selective Transparency by Rights Amplification

Both ehome__uriGetter and iAmEHome2 serialize to the same depiction, which unserializes to an object equivalent to ehome__uriGetter. This is as it should be, since both have adequate authority and knowledge -- both have the pile__uriGetter, and both know the path string "c:/Program Files/erights.org/". If we added a get(String) method to iAmEHome2, it would have most of the useful abilities of ehome__uriGetter, so one might think it is benignly substitutable for ehome__uriGetter. However, it lacks ehome__uriGetter's crucial inability -- a client of the ehome__uriGetter is not able to obtain from it the pile__uriGetter, nor any directory or normalPile not within its subtree, nor even its path. In order to be portrayable by default, iAmEHome2, must divulge this knowledge and authority to the minimalUncaller, which only knows to ask by calling the __optUncall() method. The minimalUncaller's means of asking an object to portray itself is a means any other client of the object can use to obtain that portrayal.

By contrast, ehome__uriGetter is not portrayable by default -- the minimalUncaller cannot contribute a portrayal of this object. Instead, the ehome__uriGetter's portrayal is obtained, and contributed to the traversal, by the pile__uriGetter in its role as an uncaller on the uncallers list. The pile__uriGetter asks by calling __optSealedDispatch(magicBrand). The ehome__uriGetter returns the same portrayal iAmEHome2 freely offers, but returns it in a box sealed by the magicSealer. Any of its clients can perform this call and obtain this box, but the contents of the box -- the portrayal containing the power and knowledge that must normally be encapsulated -- can only be obtained by using the magicUnsealer, which only the pile__uriGetter has.

Note that each call to makePile__uriGetter creates a separate instance of a pile system. Two separately created pile systems that are otherwise equivalent are mutually opaque. Their loaders will not enable objects from the other to be serialized. Unlike Java's package scope or Dylan's module scope, the sealer/unsealer pattern of rights amplification is dynamic and instance-based.

? def <nile> := makePileUriGetter(<file>)
# value: <pile__uriGetter>

? def neprops := <nile>[ehomePath + "eprops.txt"]
# value: <normalPile>

? neprops.getText().size()
# example value: 13898

? surgeon.serialize(neprops)
# problem: Can't uneval <normalPile>

In the code for the pile system above, we make a questionable design choice to illustrate a point -- by defining the getSealer() methods, all the objects created by a given instance of the pile system freely offers their magicSealer to their clients. The above security properties depend only on the magicUnsealer remaining encapsulated, which it does. In this pattern, the magicSealer functions like a public encryption key, and the magicUnsealer like a private decryption key.

Had we left out these getSealer() methods, then the magicSealer would also be private to each pile system instance, and the act of sealing would also have the meaning of ensuring authenticity -- no object outside a pile system instance could create a sealed box which that pile system's magicUnsealer would unseal. Had we made this choice, we could have simply sealed absPaths into the boxes rather than full portrayal-triples, since the loader could trust anyone it could understand. We would use this to implement optUnget directly, and then implement optUncall out of optUnget rather than the other way around. This would seem much more natural.

However, this choice would prevent us from repairing iAmEHome2. Here's a repaired one:

# E sample
def magicSealer := <pile>.getSealer()
def magicBrand := magicSealer.getBrand()
def iAmEHome3 {
    to __optSealedDispatch(brand) :any {
        switch (brand) {
            match ==magicBrand {
                magicSealer.seal([<pile>, "get", [ehomePath]])
            }
            match _ { null }
        }
    }
}
? surgeon.serialize(iAmEHome3)
# value: "de: <pile>[\"c:/Program Files/erights.org/\"]"

Like the ehome__uriGetter trusted by this pile system, iAmEHome3, though it is not trusted by the pile system, nevertheless demonstrates to it that it has adequate knowledge and authority to be serialized as ehome__uriGetter is, while simultaneously denying this knowledge and authority from its own clients, whom it may not trust. This only works because the full portrayal is sent through the sealed box, not just the path.

This dispatch provides a further flexibility -- by adding more match clauses to the switch, iAmEHome3 can portray itself differently depending on "who" is asking -- or rather, on which unsealer they would use to unseal the box and obtain the portrayal.

Secure Persistence

iAmEHome3 demonstrates how encapsulated objects may generally be made persistent. A persistence system establishes a subgraph recognizer using an uncaller using a magicUnsealer. The corresponding magicSealer is made generally available to the population of objects that may wish to persist. Those that do, use this magicSealer to divulge their portrayal, given that they trust (are constructed to trust) that their encapsulated knowledge and powers may be made available to anyone with the corresponding magicUnsealer.

Corresponding Concepts in Conventional Serialization

JOSS achieves selective transparency in one sense trivially, by resort to special primitives (native methods) for violating object abstraction. JOSS itself has direct access to the private variables of objects declared Serializable. In another sense, JOSS works well with the form of rights amplification provided by Java -- sibling communication. For example, within a single package we can define

  • a subclass of ObjectOutputStream which overrides replaceObject(..) to obtain the portrayal the original object would like to offer only to this serializer, and
  • a base class for such original objects to inherit from, defining a package scoped method which will return this portrayal.

By package scoping the access to the portrayal, only other methods defined within the same package, like the above replaceObject(..), can invoke this method and access this portrayal. This technique for expressing rights amplification naturally leads to static protection domains, in which all instances of this serializer class can amplify all objects that inherit from this base class. Fortunately, this is easily repaired: Each base class can record which serializer instance it is supposed to be amplified by, and each serializer can check if it is that one. Since all instances of the same class run the same code, and since an object implicitly "trusts" its own code, the instance that should be able to amplify an object can trust its siblings not to, even though they could.

 
Unless stated otherwise, all text on this page which is either unattributed or by Mark S. Miller is hereby placed in the public domain.
ERights Home data / serial / jhu-paper 
Back to: Manipulating Authority at the Exits x On to: Manipulating Identity at the Entries
Download    FAQ    API    Mail Archive    Donate

report bug (including invalid html)

Golden Key Campaign Blue Ribbon Campaign