Configuring syndicate-server gatekeepers

A gatekeeper in Syndicate terminology is an object that “upgrades” long-lived cryptographic names into live references to some underlying object. There are analogous objects in CapTP. (In CapTP, gatekeepers might be built-in to the protocol rather than separate objects. I need to find out more.)

The syndicate-server program includes an implementation of the gatekeeper protocol. This post builds up a short example syndicate-server setup involving gatekeepers. (The following is based on this example config file from the syndicate-server git repository.)

The plan

We will create a TCP listener on port 9222, which speaks unencrypted protocol and allows interaction with the default/system gatekeeper, which has a single noise binding for introducing encrypted interaction with a second gatekeeper, which finally allows resolution of references to other objects.

The outer/default/system gatekeeper will not expose any functionality apart from upgrading to an encrypted, authenticated reference to the inner gatekeeper. All the real services we want to expose will be available via the inner gatekeeper.

The stanzas below are written in the syndicate-server scripting language. They can be placed in a .pr file, e.g. example.pr, and run with syndicate-server -c example.pr.

Creating an inner gatekeeper

First, build a space where we place bindings for the inner gatekeeper to expose.

let ?inner-bindings = dataspace

Next, start, secure, and publish the inner gatekeeper.

<require-service <gatekeeper $inner-bindings>>
? <service-object <gatekeeper $inner-bindings> ?inner-gatekeeper> [
  <bind <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs],
                 secretKey: #[qLkyuJw/K4yobr4XVKExbinDwEx9QTt9PfDWyx14/kg],
                 service: world }>
   $inner-gatekeeper #f>
]

The <bind ...> assertion exposes the $inner-gatekeeper object via the outer gatekeeper. Clients have to speak the noise protocol and know the public key #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs] to resolve a reference to the inner gatekeeper. The noise protocol itself sets things up to rule out man-in-the-middle and prove to the client it really is talking to the correct server.

You can use the syndicate-macaroon tool (e.g. syndicate-macaroon noise --service world --random) to generate fresh keys for use in noise bindings.

Exposing the outer gatekeeper over unencrypted TCP

Now, expose the outer gatekeeper to the world, via TCP. The system gatekeeper is a primordial syndicate-server object bound to $gatekeeper.

<require-service <relay-listener <tcp "0.0.0.0" 9222> $gatekeeper>>

Let’s imagine we are running on a host called syndicate.example; this hostname will be used in URLs later.

Binding a service to the inner gatekeeper

Finally, let’s expose some behaviour accessible via the inner gatekeeper.

We will create a service dataspace called $world, and we will require clients to hold a sturdyref to it. We will configure the inner gatekeeper to resolve the sturdyref to the $world dataspace.

let ?world = dataspace

We need to generate a strong name, a sturdyref, that we will bind to $world. For this, we can use the syndicate-macaroon tool.

Let’s choose the passphrase hello as our secret that no-one but the service itself should know.

Running syndicate-macaroon mint --oid a-service --phrase hello yields the sturdyref <ref {oid: a-service, sig: #[JTTGQeYCgohMXW/2S2XH8g]}>. That will act as a root capability for $world once we bind it at the inner gatekeeper, using the corresponding sturdy.SturdyDescriptionDetail:

$inner-bindings += <bind <ref {oid: a-service, key: #"hello"}> $world #f>

Configuration done!

That’s it! All together, here is the configuration we set up:

let ?inner-bindings = dataspace

<require-service <gatekeeper $inner-bindings>>
? <service-object <gatekeeper $inner-bindings> ?inner-gatekeeper> [
  <bind <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs],
                 secretKey: #[qLkyuJw/K4yobr4XVKExbinDwEx9QTt9PfDWyx14/kg],
                 service: world }>
   $inner-gatekeeper #f>
]

<require-service <relay-listener <tcp "0.0.0.0" 9222> $gatekeeper>>

let ?world = dataspace

$inner-bindings += <bind <ref {oid: a-service, key: #"hello"}> $world #f>

Accessing the $world from some remote client

Now, we can hand out paths to $world involving an initial noise step and a subsequent sturdyref/macaroon step. Clients using such paths benefit from an encrypted, authenticated channel (the noise layer), and use of the $world service is authorized by possession of a correctly-signed sturdyref.

The gatekeeper.Route that a client might use would be something like:

<route [<ws "ws://syndicate.example:9222/">]
  <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs], service: world }>
  <ref { oid: a-service, sig: #[CXn7+rAoO3Xr6Y6Laap3OA] }>>

There’s more that sturdyrefs can do: since they are based on Macaroons, they can be attenuated offline (!) to limit the kinds of things clients can assert at $world and the kinds of messages they can send to $world. The example config file gets into more detail.