Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pact capabilities #31

Closed
sirlensalot opened this issue Mar 24, 2018 · 14 comments
Closed

Pact capabilities #31

sirlensalot opened this issue Mar 24, 2018 · 14 comments
Labels
enhancement Pact 3.0 Features for next major Pact release

Comments

@sirlensalot
Copy link
Contributor

sirlensalot commented Mar 24, 2018

See below for current design, the following is out of date

As described in upcoming Pact Capabilities paper. Capabilities takes keysets and subsumes them into a "shadow ADT" type, capability, which also includes module capabilities, pact capabilities, and custom user capabilities. API changes are as follows:

The Pact Capability Model

Keysets provide the inspiration. Keysets in Pact are managed with the
following operations:

define-keyset: this stores a keyset in the global environment under
some unique name.

read-keyset: the only way to construct keysets in user code,
read-keyset interrogates the JSON transaction payload at the specified
key to build a keyset from a special JSON schema found there, specifying
the constituent keys and predicate function.

enforce-keyset: this takes either a keyset object, or a string
referring to a keyset in the global environment, and enacts the
enforcement logic described above to ensure the keyset rule is matched
by public keys used to sign the transaction.

Seeing read-keyset as a constructor, and enforce-keyset as the
enforcement interface, we can generalize to support other capabilities
by providing constructors for the various types, but unifying
enforcement to a single built-in. First, the constructors:

create-module-capability: takes a string identifier to distinguish
different module-controlled resources. Module name is captured
implicitly.

create-pact-capability: takes a string identifier to distinguish
different pact-controlled resources. Pact name and ID are captured
implicitly.

define-capability: takes two arguments, the first holding the data
to be captured, and the second a string identifying a function to enact
the predicate logic.

The values returned by these constructors, as well as keysets, form an
ADT-like opaque datatype which cannot be pattern-matched in Pact code,
of type capability. A capability once created can only be used with
the sole enforcement function:

enforce-capability: takes any capability and enacts the necessary
logic required by its sub-type to execute predicate logic to enforce the
intent. Notably, the predicate logic is executed in the same "pure
environment" used by keysets to ensure no database manipulation can
occur during enforcement.

Example: A Hash-Timelocked Capability in Pact

Hash timelocks enforce an "OR" requirement that either the user provide
a hash preimage, or the original owner can reclaim the funds after a
timeout. The following example code creates a capability which could be
stored in a coin account for guarding access to that balance. Note that
the predicate logic expects the secret value to be in the JSON payload
under the key "secret".

(module htlc 'htlc-admin

  (defun make-htlc (hashvalue timeout owner counter)
    (define-capability
      { "hash": hashvalue,
        "timeout": timeout,
        "time": (get-system-time)
        "ownerkeyset": owner,
        "counterkeyset": counter
      }
      'enforce-htlc))

  (defun enforce-htlc (data)
    (bind data { "hash" := hashvalue,
                 "timeout" := timeout,
                 "time" := starttime,
                 "ownerkeyset" := owner,
                 "counterkeyset" := counter}
      (enforce-one [
        (and (= hashvalue (hash (read-key "secret")))
             (enforce-keyset counter)),
        (and (>= timeout (- (get-system-time) starttime))
             (enforce-keyset owner))])))

)
@sirlensalot sirlensalot added the Pact 3.0 Features for next major Pact release label Mar 24, 2018
@sirlensalot
Copy link
Contributor Author

Frrom https://www.cs.ucsb.edu/~chris/teaching/cs290/doc/eros-sosp99.pdf, "EROS: a fast capability system", Shapiro, Smith, Farber 1999:

A capability is an unforgeable pair made up of an object identifier and a set of authorized operations (an interface) on that object [9]. UNIX file descriptors [51], for example, are capabilities.

In a capability system, each process holds capabilities, and can perform those operations authorized by its capabilities. Security is assured by three properties:

  1. capabilities are unforgeable and tamper proof,
  2. processes are able to obtain capabilities only by using authorized interfaces, and
  3. capabilities are only given to processes that are authoized to hold them.

A protection domain is the set of capabilities accessible to a subsystem. An essential idea of capability-based system design is that both applications and operating system should be divided into cleanly separated components, each of which resides in its own protection domain.

Subject to constraint by an external reference monitor (Figure 1), capabilities may be transferred from one protection domain to another and may be written to objects in the persistent store. Access rights, in addition to data, can therefore be saved for use across instantiations of one or more programs.

@sirlensalot
Copy link
Contributor Author

The previous cite helps motivate the Pact system as a capability system; as it is sometimes suggested that capabilities must also support

  1. Dynamic revocation of capabilities by the issuing process

  2. The ability to "chain" capabilities such that a recipient of some composite "parent" capability can in turn grant a sub-capability, with all being governed by the parent capability (thus the revocation stuff being important).

The Pact system as proposed instead fits the EROS description of simply binding a program object to an interface/set of authorized operations.

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 14, 2018

Investigating key-auth semantics in Pact as a lens onto the similarities and differences in a typical capability system.

In Pact, rights enforcement employs a "enforce-before" semantic where the (non-re-entrant) code that follows is thereby "protected" by the previous call.

   (enforce-keyset 'authorized-user)
   (do-protected-task)
   (do-something-else)

Indeed, early versions of Pact had with-keyset to make the enforcement scope clear, but there was no clear meaning of what the enclosed code meant beyond the "enforce-before" semantic. In the following code, the failure of the keyset enforcement in with-keyset would prevent (do-something-else) from executing, thereby making the scoping meaningless.

   (with-keyset 'authorized-user
       (do-protected-task))
   (do-something-else)

Finally, the "rights system" at play here is static during a given transaction: (enforce-keyset 'authorized-user) will always return the same result during a given transaction, and given Pact's atomic semantics, really means that the enforcement could happen anywhere in the code (at the cost of wasted work thrown away in the transaction rollback process).

In a typical capability system, the rights are focused on some resource plus what operation(s) might be allowed. In the example above, let's say that (do-protected-task) updates a single column name of a single row identified by row-id in table users. Conceivably, a capability system could offer any combination of resource+operation grants that would work for this:

# Resource Operation
1 users table update any column in any row
2 column name in users table update column in any row
3 row for key row-id in users table update any column in row
4 column name, row row-id update cell

The code would at some point request the desired right. The mechanism for granting the right would be the keyset enforcement. Upon success, a token would be produced for use authorizing the particular task. The following pseudocode uses example 1, table update access.

  (let ((token (request-access user-table UPDATE)))
    (do-protected-task token))
  (do-something-else)

with the idea that request-access somehow knew to enforce the 'authorized-user keyset. Interestingly, the let block scopes the token so that there is no way the authorization could bleed into do-something-else. Indeed, if this second operation needed the same rights it would now have to accept the token argument, which at a minimum makes the need for the right clear, but also reifies the grant itself.

An advantage of this could be the further rationalization and/or delegation of rights in a system. As of Pact 2.6.0, all module functions are "public" which requires duplicating enforce-keyset calls within "delegate" functions that might be reused and do some sensitive task. A solution is to have "private" functions but it doesn't address public functions which could be called by other functions. do-protected-task without a token argument can be called externally, which would defeat the rights system constructed in the first examples.

However, adding the token as an argument, and assuming that the token object is unforgeable, essentially forbids the external world from invoking do-protected-task token. Reasoning about control just becomes the reachability of the token, and the inability of tokens to exist outside of the Pact environment would effectively prevent blockchain-level invocation of the function.

If we add this notion of a token, what has been called "capabilities" up to this point instead become the predicates-against-the-environment by which access to a token is granted.

A potential drawback of tokens is their proliferation. It might make sense to instead be able to "install" tokens into the environment itself, something like the following:

  (request-access user-table UPDATE) ;; install token
  (do-protected-task) ;; needs token to be installed
  (do-something-else)

Indeed, this style could allow that instead of having an opaque request-access system call, these would be "plain old functions" that enforce predicates before "installing" the required token:

  (defcap UPDATE_USERS "capability to update user table")
  (defun request-user-table-update ()
     (enforce-keyset 'authorized-user)
     (grant UPDATE_USERS) ;; `grant` installs UPDATE_USERS into the environment
                                    ;; `revoke` would remove it
  )
   (defun do-protected-task ()
     (enforce-capability UPDATE_USERS) ;; this checks the environment for presence of UPDATE_USERS
     (update 'users ...))
    ...
   (request-user-table-update) 
   (do-protected-task)
   ...

FURTHER WORK:

  • presumably, capabilities would have to be scoped, probably within the module that defines them, such that grant and revoke only work in the defining module code.
  • caps cannot be persistable. However they could be passed between pact steps via yield and resume.
  • the old capabilities here should now be called predicates or guards.

@sirlensalot
Copy link
Contributor Author

Examining row keysets through the capability lens:

Current Pact enforces row keysets by reading them before performing actions:

(defun debit (id amt)
  (with-read accounts id { "balance" := bal, "keyset" := ks }
    (enforce-keyset ks)
    ...
    (update accounts id { "balance": (- bal amt) })))

A capability would represent here the ability to debit the balance column of the row at id. This is a new requirement, where the capability object needs to match up with some data value. Following the "request, grant, enforce" pattern above, pseudocode would be:

(defcap DEBIT)
(defun request-debit (id)
  (enforce-keyset (at 'keyset (read accounts id)))
  (grant DEBIT id))
(defun debit (id amt)
  (with-read accounts id { "balance" := bal }
    (enforce-capability DEBIT id)
    ...
    (update accounts id { "balance": (- bal amt) })))
...
  (request-debit id)
  (debit id 10)

This implies that a capability object needs to accept some data to service data-driven rights, as opposed to the static UPDATE_USERS cap above. This pseudocode assumes that grant, revoke and enforce-capability accept an additional data argument, here a string.

@sirlensalot
Copy link
Contributor Author

The question to address now is how capability objects are themselves managed. In the examples above they are managed by the environment via the grant mechanism, which will install the capability and optional data into the environment for testing against with enforce-capability. There is no capability object the user ever touches, which is very good in one way: it eliminates the possibility of the capability being transferred as an object itself, as it cannot leave the system.

The first question is "who can grant a capability". In a lot of capability discussions (and the main one I'll refer to is [1]) there are two big assumptions:

  1. Typically, capabilities are presented within a managed (ie "not C++", no pointer hacking, for unforgeability) OO paradigm. Thus a capability is married to reachability itself of the object in question, and insofar as an object "holds" a capability (ie in a private field) encapsulation enforcement prevents other objects from reaching it.

  2. Also, the ability for capabilities to support modulation (ie revocation) is necessarily a feature only present in a mutable computation environment, namely OO. To quote from [1]:

In the ocap model, if I’ve given them one of my capabilities, then now they have it too, so what can I do if I don’t want them to have it any more? The answer is that I didn’t give them my capability. Instead I gave them a new capability that I created, a reference to an intermediate object that holds my capability but remains controlled by me in a way that lets me disable it later. We call such a thing a revoker, because it can revoke access. A rudimentary form of this is just a simple message forwarder that can be commanded to drop its forwarding pointer.

In an immutable environment like Pact, while we can certainly imitate this functionality behind the scenes with some kind of reference object, this would introduce unfamiliar and undesirable semantics. Environmental management allows for modulation through built-in processes, with the advantage that the semantics cannot be violated by the programmer.

The most obvious modulation is revocation upon transaction completion. This is basically inviolable, but the capability discussion for Pact has always wanted a way to transfer rights between steps of a pact, so that e.g. a signer involved in a first step can make some right available to a later step that would not be signed. Generally, the "pact predicate" described handles this in a tighter fashion, and can of course be married to a corresponding grant. In any case, transaction-scope rights fits most use cases well, but an explicit revoke can also be provided just in case.

Environment-based management also allows a tight relation between a capability and the module that defines it. In the examples above, the grantable capabilities have been defined using defcap within the module that is granting, with the notion that these are not grantable by code outside the module. This resembles Pact's encapsulation model over tables (tables are freely accessible by module code, and outside require the module-admin capability using the module admin keyset) and thus provides a simple authorization scheme where external grants and revokes can only happen by admins, and for the same reasons, like money getting trapped: an admin can mitigate a bug by re-granting a capability (revoking not making a ton of sense yet as we haven't conceived of capabilities outlasting a transaction yet).

Thus the "who" granting a capability is the autonomous code of the module that defined it. Note that the "predicates + capabilities" design taking shape here assumes some of the responsibility that in OO-style capabilities would be magically endowed to this or that object reference: in this capability model, you have to "re-access" a capability in each transaction that needs it, usually enforcing a keyset. Thus the notion of delegation in OO-style capabilities here moves to designing a predicate that will allow the delegated actor or process to produce the proper environmental conditions to allow the module to grant the desired capability.

1 - http://habitatchronicles.com/2017/05/what-are-capabilities/

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 22, 2018

The code snippets above share the following characteristics:

  1. Definition of a capability (defcap UPDATE_USERS, defcap DEBIT id).
  2. Some kind of predicate that must be tested in order to grant the capability. The testing of the predicate and granting would be offered as some "request capability" operation.
  3. The ability to associate some data with a capability (DEBIT id) use this data in the guard enforcement code.
  4. The ability to enforce that the capability has been granted (enforce-capability).

What is desirable is a way to simplify and reduce these steps as much as possible. Step #4 is necessarily distinct as there could be many code locations where a capability is enforced; however for 1-3 we would like to eliminate errors and confusion about how to specify.

Extending defcap is the answer, in a manner that strongly resembles function application. Here, a defcap special form is a function with arguments that is invoked by enforce-capability, such that the defcap body is evaluated only if the capability has not yet been granted (or has been revoked). Since the predicates fail the transaction, this implies that the grant operation will happen only once per transaction.

NB the following code examples are deprecated as enforce-capability is not the design anymore, see below.

Rewriting the examples above looks like the following:

UPDATE_USERS defcap

  (defcap UPDATE_USERS () 
     (enforce-keyset 'authorized-user))

  (defun do-protected-task ()
     (enforce-capability UPDATE_USERS)
     (update 'users ...))

Here, enforce-capability would test if UPDATE_USERS is granted, and if not apply UPDATE_USERS as a function. Failure fails the transaction; success grants UPDATE_USERS. Future calls to enforce-capability will find the capability already granted and return immediately.

(DEBIT id) defcap

  (defcap DEBIT (id)
    (enforce-keyset (at 'keyset (read accounts id))))

  (defun debit (id amt)
    (enforce-capability DEBIT id)
    (with-read accounts id { "balance" := bal }
      (update accounts id { "balance": (- bal amt) })))

Similarly here, (enforce-capability DEBIT id) would first test if the tuple of (DEBIT,id) was already granted. If not, the DEBIT function is applied as above to guard the grant. Again, later calls to (enforce-capability DEBIT id) would find the capability already granted and return immediately.

A possible enhancement could be to offer a (with-capability CAP ARGS BODY) with the implication that the grant on CAP would be revoked. Since this can easily be added later and desired behavior created using revoke, this is not being proposed now.

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 22, 2018

Returning to the original design, we see these now as predicates or guards, not capabilities, and can further motivate their design by the following observations:

  1. While storing capabilities in a database is incredibly dangerous, storing predicates in the database simply allows a "row-level flexibility" in applying some security scheme, that is then enforced anew in every transaction needing the grant. The current proposed design offers a straightforward mechanism for ensuring uniform enforcement using the guard read in from the database, alongside other guards if desired.

  2. Predicate-based granting fits the blockchain scheme well. In [1], under "Distributed Systems", we see a description of a crypto-backed mechanism using SAML or OAuth2 Bearer Tokens. We simply note here that requiring the user to sign every transaction, possibly with multiple keys that may represent the various access mechanisms needed, is a far more secure design as it requires no ambient "single sign-on service", fitting the notion that a blockchain is a self-contained security system. By allowing a predicate to activate a capability we are capable of offering identical schemes with superior transparency to OO-based capability systems.

  3. The polymorphism becomes simply a convenience to allow keysets to be stored alongside other predicates in the same row of a database column. Normally a "pact-id OR user-keyset" scheme would require NULL fields in a monotyped schema; by making predicates an algebraic type we avoid NULLs, flags and other control-flow, buggy mechanisms.

  4. "Module" predicates no longer makes sense (if they ever did), as grants are firmly encapsulated into module code. The need to "export" a capability will need to be separately described and motivated before we will abandon the attractive simplicity and security of module encapsulation. EDIT: see discussion below re admin operations and capability.

  5. "Predicate" is too general of a term, and implies a boolean function, whereas the predicates here are intended to fail the transaction upon predicate failure. Thus we will call these predicates guards. Since keyset already has constructors (define-keyset and read-keyset), we merely need to provide the special pact-oriented constructor alongside the generalized one:

EDIT: these were named define-XXX but changing to create-XXX to reflect that define-keyset implies a registry operation, whereas read-keyset is the actual constructor (if there was one, it would be called create-keyset).

(defun create-user-guard:guard (data:<a> predfun:string)
    "Defines a custom guard predicate, where DATA will be passed to PREDFUN at time 
of enforcement. If PREDFUN is unqualified it is expected to be in the declaring module 
or a builtin, otherwise the string must present the fully-qualified function name.")

(defun create-pact-guard:guard (name:string)
  "Defines a guard predicate by NAME that captures the results of `pact-id`. 
At enforcement time, the success condition is that at that time `pact-id` must 
return the same value. In effect this ensures that the guard will only succeed 
within the multi-transaction identified by the pact id.")

(defun enforce-guard (guard:guard) 
  "Execute GUARD to enforce whatever predicate is modeled. Failure will 
fail the transaction.")

EDIT: see below for create-module-guard.

read-keyset will return type guard instead of keyset. define-keyset operates by side-effect so its signature will be unchanged.

1 - http://habitatchronicles.com/2017/05/what-are-capabilities/

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 22, 2018

We provide the signatures for capability management itself.

(EDIT: changed capability to be app form within with-capability)
(EDIT: new comment below for with-capability special form).

Special form: defcap capability params body
Define and parameterize a _capability_ as a unique token comprised of the product of
CAPABILITY and PARAMS. BODY is executed in the context of 'with-capability' to validate
installing the token in the environment ("granting" the token).
Special form: with-capability capability body
Specifies and requests grant of CAPABILITY which is an application of a `defcap`
production; given the unique token specified by this application, ensure
that the token is granted in the environment during execution of BODY. If token is not
present, the CAPABILITY is applied, with 
successful completion resulting in the installation/granting of the token, which 
will then be revoked upon completion of BODY. 
Nested `with-capability` calls for the same token will detect the presence of 
the token, and will not re-apply CAPABILITY, but simply execute BODY.

Rewriting the examples above looks like the following:

UPDATE_USERS defcap

  (defcap UPDATE_USERS () 
     (enforce-keyset 'authorized-user))

  (defun do-protected-task ()
     (with-capability (UPDATE_USERS)
       (update 'users ...)))

(DEBIT id) defcap

  (defcap DEBIT (id)
    (enforce-keyset (at 'keyset (read accounts id))))

  (defun debit (id amt)
    (with-capability (DEBIT id)
      (with-read accounts id { "balance" := bal }
        (update accounts id { "balance": (- bal amt) }))))

While the above discussion was not sure whether to go with a scoped form, the awkwardness of the CAPABILITY + PARAMS product answered the question syntactically, due to the un-Pact-like placement of a function-like def name in the second position; enforce-capability and revoke would require special treatment (ie varargs) to not be ungainly. with-capability has the significant advantage of scoping the environmental side-effect of token install, which enhances safety analysis significantly.

EDIT: as noted above, using application syntax for capabilities simplifies things enough for a revoke to be tenable, but with-capability is attractive enough to avoid wanting to go back to the "happens before", unscoped variant.

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 22, 2018

This design now presents a unified model of capabilities and guards as a flexible and safe solution. However the very attractiveness of with-capability, that is, tightly-scoped rights, means this solution does not solve a performance issue associated with module upgrade and other data-heavy admin transactions.

The problem being, in admin data load (either at contract install, or during module schema migration/upgrade), every write to the tables will require the guarding keyset to be tested. Worse, with generalized governance, the governance function would have to be called again and again, which may be pathological in cases where the governance uses the database.

The intention was that the token system would address this by installing the token at first challenge and allowing the implicit transactional scope to allow the further interactions. Unfortunately this is far less well-scoped than the with-capability design presented here, which has the huge advantage of not leaking the capability into the calling context. Here, if a user was to call third-party code after gaining the module-admin token, this code would run with elevated rights. The current solution -- check on every access -- does not have this problem.

For now we will proceed with this transaction-scoped install of the module admin token in order to make generalized governance practical. We note that a transaction that is capable of being granted module admin is already a very sensitive operation requiring care, so ensuring that any calls to third-party code will not be able to abuse the elevated rights is added to the user's responsibility here. However, we should consider also requiring that top-level code that is not a module upgrade be encased in a dedicated special form, e.g. with-module-admin, to scope these elevations. One reason to avoid this is breaking existing code.

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 22, 2018

We need to consider ifwith-capability is safe to call outside of the declaring module -- earlier "set and forget" designs meant that module scoping would be employed to prevent code outside the declaring module from referencing the capability. It is perhaps unnecessary to prevent, as the module will have to guard all sensitive code with with-capability anyway, so acquiring a capability outside of the module's codebase will only result in "earlier" acquisition of the right. For now we will not enforce any kind of module-code-only requirement on using with-capability.

EDIT see below, module-only is very useful.

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 24, 2018

The module guard indeed can have valid uses (for instance, giving governance control over unallocated coins/tokens), and as it is a guard, it does not suffer from the scoping issues of granting the module admin capability, as presumably the only reason to deploy the guard is to leverage governance to access some other scoped capability.

(defun create-module-guard:guard (name:string)
  "Defines a guard by NAME that enforces the module admin predicate.")

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 24, 2018

Implementation Notes

Haskell implementation DRAFT

The Term type TKeySet KeySet Info is replaced with TGuard Guard Info, with the following types defined:

data Guard
  = GPact PactGuard
  | GModule ModuleGuard
  | GKeySet KeySet
  | GKeySetRef KeySetName
  | GUser UserGuard
  deriving (Eq)

This achieves the objective of making keysets interoperable with other guards in Pact code. The design problem to solve is how to deal with the type at the Pact surface level.

keyset type and guard type

The Pact type keyset is specified in the current keyset builtins (read-keyset, define-keyset, enforce-keyset [which also accepts a string, more on this later], and is persistable to the database using the Persist codec. Database backward compatibility is easily supported. A significant concern is type-handling: should keyset simply be an alias for the new guard type?

User code specialization on guards considered harmful

Should we allow code to specialize ever on guard subtype? The argument against specialization is to focus on the black-box feature of guards: write them once and then simply enforce them. It is hard to conceive of a good use case for basing code actions on a type of guard, and easy to think of reasons why this would be a terrible idea. The one exception is to look at Bitcoin, where pubKeyScripts are ostensibly black boxes, but in fact the protocol client is very aware of the various flavors of scripts. But here we are concerned with smart contracts -- the various Pact-hosting binaries can directly access the Haskell types as needed if they decide to limit guard types as a security mitigation, a la Bitcoin. At the smart contract level, we can entrust the black boxes to faithfully execute, and prevent users from writing Pact code that distinguishes on guards.

By this logic, enforce-keyset can simply be a synonym for enforce-guard, with the implication that if a string argument is offered this would look up the keyset. If we were to maintain that enforce-keyset only allows the keyset subtype, users would have to get into precisely the kind of guard introspection we seek to discourage. On the other hand, the risk that enforce-keyset would enforce the wrong guard is certainly possible in cases where user code is juggling a bunch of different guards, but we can convincingly argue that this sounds like pretty poorly-factored code to begin with.

DECISION: treat enforce-keyset as a synonym for enforce-guard.

Implications of making keyset type an alias for guard

define-keyset expects a keyset, and is used almost always with read-keyset which returns keyset. If read-keyset returns instead a guard, this implies that define-keyset no longer stores keysets in the registry, but any guard.

Looking at the Haskell code above, we see that a full guard implementation requires a case for keyset names, in order to support the string usage of enforce-keyset consistently. In general, the use of keyset references instead of concrete keyset objects allows for ease of management (namely, rotation) of keysets used in multiple places. The current design is the first time these string references can be used in the database, as previously the pattern was to stick concrete keysets in the database. (This is still an acceptable case though for the common practice of "a new key for every account", so we will always offer concrete keyset storage and enforcement).

define-keyset registry: does it make sense for other guards?

The keyset registry is intuitive as (a) it is needed for module admin and (b) it allows straightforward rotation using define-keyset. Does it make sense though to extend this to guards in general?

For instance, putting a pact guard in a registry sounds like a bad idea, as the registry will outlive the pact. A module guard seems strange, since this has its own rotation semantic (ie through governance), and smells bad. The use case that makes some sense is user guards, as these can be crafted to allow more complicated keyset schemes (e.g., admin signs, plus 1 out of 3 from some other keyset). But in general it seems that "rotating" a non-keyset guard is awkward.

It would seem then that the type system should distinguish at the user level between keyset and guard. In Pact code, guard will indicate any kind of guard, and keyset will indicate a keyset subtype of guard. At such time that we deem it necessary to expose guard subtypes to user code, some syntax can be introduced like guard<pact> etc (this might correspond with introducing parameterization in general), and keyset would be a synonym for guard<keyset>.

define-keyset and read-keyset types will remain unchanged, and implementing code will emit and accept only keyset guards.

typeof will reflect this current state of affairs by only emitting guard for non-keyset subtypes, and keyset for keysets. The actual Type variables emitted by the typechecker etc will match the known subtypes.

Lastly, we will need an explicit way to create a keyset-name guard for storing in the database:

(defun keyset-ref-guard:guard (keyset-ref: string)
  "Creates a guard for the keyset registered as KEYSET-REF with 'define-keyset'. 
Concrete keysets are themselves guard types; this function is specifically to store
references alongside other guards in the database, etc.")

@sirlensalot
Copy link
Contributor Author

sirlensalot commented Nov 25, 2018

An attractive feature would be to offer a way to require a capability with no attempt to obtain it if it is not already acquired. This would make guarding factored functions like debit possible, as calling debit without credit violates mass conservation; however if transfer performed with-capability (DEBIT id), and debit had a way to require-capability (DEBIT id) but not acquire it, debit is now effectively prohibited from calling outside transfer.

(Note that this might be better factored as a TRANSFER src dest capability nesting DEBIT id, such that debit requiring TRANSFER src dest and entering with-capability DEBIT id.)

However, this requires revisiting the notion above that with-capability CAP can be called outside the module that hosts the defcap for CAP: for require-capability to have any force, it must be impossible for external code to enter into the capability in the first place. We can guard capabilities with module-admin just like table access: outside calls to with-capability are only permitted with module-admin.

(defun require-capability (capability)
  "Specifies and tests for existing grant of CAPABILITY, failing if not found
in environment.")

New comment for with-capability noting the module encapsulation:

Special form: with-capability capability body
Specifies and requests grant of CAPABILITY which is an application of a 'defcap'
production. Given the unique token specified by this application, ensure
that the token is granted in the environment during execution of BODY. 
'with-capability' can only be called in the same module that declares the 
corresponding 'defcap', otherwise module-admin rights are required.
If token is not present, the CAPABILITY is applied, with successful completion 
resulting in the installation/granting of the token, which will then be revoked 
upon completion of BODY. Nested 'with-capability' calls for the same token 
will detect the presence of the token, and will not re-apply CAPABILITY, 
but simply execute BODY.

@sirlensalot
Copy link
Contributor Author

Implemented in #324

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Pact 3.0 Features for next major Pact release
Projects
None yet
Development

No branches or pull requests

1 participant