Getting started with plugins

Updated: January 18th, 2018

Plugins are a powerful system allowing users to securely run sensitive business logic inside SmartKey.

For example, plugins may be used to

  • impose custom, arbitrarily complex access control policies on keys in SmartKey
  • require approval from a quorum before a key in SmartKey is used
  • ensure that an RSA key in SmartKey may only sign certificates with a particular attribute
  • require that a key in SmartKey may only be wrapped with a certificate signed by a particular CA
  • implement a secure higher-level cryptographic operation, including custom audit logs, to expose to outside apps

Currently, Lua is the only language supported.

How to use

Like apps and security objects, plugins are identified by UUID.

Plugins are invoked using a REST API similar the cryptographic and key management APIs.

To invoke a plugin, make a POST request to https://<API endpoint>/sys/v1/plugins/<uuid>. The POST request body must be either valid JSON or empty.

The plugin may return:

  • 200 OK with a JSON response body,
  • 204 No Content with empty response body, or
  • a 4xx/5xx error with a plain text error message response body.

How to develop

Standard plugins define a function run that takes a single argument and is called when the plugin gets invoked.

If the POST request body is JSON, the JSON is decoded into a Lua object and passed to run. If the POST request body is empty, nil is passed to run.

For example, if a plugin is invoked with the JSON {"a":1,"b":[2,3]}, the function run is passed the Lua object { a = 1, b = {2, 3} }.

On success, the function run may return nil or any encodable Lua object:

  • If run returns nil, the plugin replies with 204 No Content and empty body.
  • Otherwise, the returned object is encoded as JSON and the plugin replies with 200 OK.

On error, the function run returns two results – nil and the error. The error may be:

  • a string consisting of the error message, or
  • an Error object with a custom status code and message, e.g. Error.new { status = 404, message = "key not found" }.

If the error is a string, the plugin replies with 400 Bad Request.

Finally, if run raises and exception, the plugin replies with 500 Internal Server Error.

For example, consider the following plugin:

function run(input)
   if type(input) ~= 'table' or type(input.name) ~= 'string' then
      return nil, 'invalid input'
   end

   return {
      greeting = "hello " .. input.name
   }
end

If the plugin is invoked by POSTing {"name":"world"}, then

  • input is the Lua object { name = "world" },
  • run returns the Lua object { greeting = "hello world" }, and
  • the plugin replies with 200 OK and the JSON {"greeting":"hello world"}.

Plugins may use the SmartKey cryptographic and key management APIs though an object oriented API similar to the REST API.

For example,

function run(input)
   local key = assert(Sobject { name = "Test Key" }) -- Get a key by name
   -- If "Test Key" isn't found, `assert` will throw the error, and the client will see a 500 Internal Server Error.

   local key2 = assert(Sobject { id = 'd1ebab99-c815-422c-9f7d-9ad1b8c5de97' }) -- Get a key by ID

   local key3, err = Sobject { name = input.name } -- Get a key by name, handling errors manually
   if not key then return nil, "Key " .. input.name .. " not found" end -- blame the client if error (400 Bad Request)

   local encrypt_result, err = key:encrypt {
      mode = 'CBC',
      plain = 'aGVsbG8gd29ybGQ=' -- Like the REST API, plugins may pass binary data as a base64-encode string
   }
   if not key then return nil, "Error encrypting with Test Key: " .. tostring(err) end

   -- Plugins may also pass binary data as a `Blob`, which is more powerful
   local plain = Blob.from_bytes('hello world')
   assert(plain:bytes() == 'hello world')
   assert(plain:base64() == 'aGVsbG8gd29ybGQ=')
   assert(plain == Blob.from_base64('aGVsbG8gd29ybGQ='))

   local encrypt_result, err = key:encrypt {
      mode = 'CBC',
      plain = plain,
   }
   if not key then return nil, "Error encrypting with Test Key: " .. tostring(err) end

   return {
      cipher = encrypt_result.cipher, -- Binary data is returned as a `Blob`, which encodes to base64
      iv = encrypt_result.iv,
   }
end

A plugin may get information about the entity that invoked it, the “principal”:

function run(input)
   local principal = principal() -- either `{ app = '<uuid>' }`, `{ user = '<uuid>' }`, or `{ plugin = '<uuid>' }`
   local principal_type = principal:type() -- 'app', 'user', or 'plugin'
   local principal_id = principal:id() -- '<uuid>'

   local principal_entity = principal:entity() -- the app, user, or plugin object

   return "Plugin was invoked by a " .. principal_type .. " with name " .. principal_entity.name
end

How to configure

Like SmartKey apps, SmartKey plugins are entities that may be in multiple groups.

A plugin may use a security object if and only if it shares a group with the security object.

An app, user, or another plugin may invoke a plugin if and only if it shares a group with the plugin.

In a typical configuration, a plugin will be in a privileged group A that contains the keys the plugin will use, as well as a less privileged group B that contains the apps that will invoke the plugin.

Since the apps in the less privileged group B aren’t in the privileged group A, the apps may only access the keys by invoking the plugin, which may enforce access control policies, invariants, etc.