How do I seal a capability?

One capability may be sealed with another capability, using the cheri_seal CHERI macro. The first parameter of cheri_seal is the capability to be sealed. The second parameter is the capability we are using to perform the seal, analogous to the private key in a cryptographic signing operation. The result of successful cheri_seal is a sealed capability.

The example code below uses the OS canonical sealing capability, security.cheri.sealcap which can be used to seal any valid capability value.

Unsealing a capability

Once a capability has been sealed, it cannot be dereferenced or modified. Effectively a sealed capability is immutable. The only valid operation we can perform is to unseal the capability, using the cheri_unseal macro.

This is the dual of the cheri_seal macro above. With cheri_unseal(x, y), x is the sealed capability we want to unseal, and y is the sealing capability (the same one that we used to perform the seal).

// seal.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cheriintrin.h>
#include <sys/sysctl.h>

char * get_secret() {
  char * buf = (char *)malloc(32);
  memset(buf, 0, 32);
  strcpy(buf, "Shh! This is a secret!");
  return buf;
}

void * get_system_sealer() {
  void * sealcap;
  size_t sealcap_size = sizeof(sealcap);
  if (sysctlbyname("security.cheri.sealcap", &sealcap, &sealcap_size, NULL, 0) < 0)
  {
      fprintf(stderr, "Fatal error. Cannot get `security.cheri.sealcap`.");
      exit(1);
  }
  return sealcap;
}

void untrusted_3rd_party_func(void * data) {
  // this function will only print metadata of the sealed capability
  // and that's safe
  printf("[untrusted func] sealed: %#p, valid: %d\n", data, cheri_is_valid(data));
}

int main() {
  char * buf = get_secret();
  printf("[main] buf: %#p, valid: %d\n", buf, cheri_is_valid(buf));

  // we can seal the capability using `cheri_seal`
  void * sealer = get_system_sealer();
  printf("[main] sealer: %#p, valid: %d\n", sealer, cheri_is_valid(sealer));
  void * sealed = cheri_seal(buf, sealer);
  printf("[main] sealed: %#p, valid: %d\n", sealed, cheri_is_valid(sealed));

  // we can unseal the capability with the original sealer
  char * unsealed = (char *)cheri_unseal(sealed, sealer);
  printf("[after] unsealed: %#p, %s\n", unsealed, unsealed);

  // pass the sealed capability to an untrusted function
  untrusted_3rd_party_func(sealed);
}

Note that the example code above won’t be able to compile with the MUSL libc library because MUSL libc does not have support for sysctl. Therefore, we have to either compile it natively on CheriBSD or with the GCC toolchain.

If we build and run the program, we will see the following output:

# compile and run on a Morello system
$ clang-morello -march=morello+c64 -mabi=purecap \
    -Xclang -morello-vararg=new \
    -O0 -g seal.c -o seal
$ ./seal
[main] buf: 0x40838000 [rwRW,0x40838000-0x40838020], valid: 1
[main] sealer: 0x4 [,0x4-0x2000], valid: 1
[main] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1
[after] unsealed: 0x40838000 [rwRW,0x40838000-0x40838020], Shh! This is a secret!
[untrusted func] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1

Now if we try read the sealed capability in the untrusted function, we’ll receive a SIGPROT fault:

void untrusted_3rd_party_func(void * data) {
  printf("[untrusted func] sealed: %#p, valid: %d\n", data, cheri_is_valid(data));
  printf("[untrusted func] read as char *: %s\n", (char *)data);
}
$ ./seal
[main] buf: 0x40838000 [rwRW,0x40838000-0x40838020], valid: 1
[main] sealer: 0x4 [,0x4-0x2000], valid: 1
[main] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1
[after] unsealed: 0x40838000 [rwRW,0x40838000-0x40838020], Shh! This is a secret!
[untrusted func] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1
In-address space security exception (core dumped)

However, since we were using the OS canonical in the above example, which is accessible by everyone, the attacker can also get a copy of that and try to unseal the capabilities we pass to these untrusted functions. For example:

void untrusted_3rd_party_func(void * data) {
  printf("[untrusted func] sealed: %#p, valid: %d\n", data, cheri_is_valid(data));

  void * root_sealer;
  size_t sealcap_size = sizeof(root_sealer);
  sysctlbyname("security.cheri.sealcap", &root_sealer, &sealcap_size, NULL, 0);
  data = cheri_unseal(data, root_sealer);
  printf("[untrusted func] read as char *: %s\n", (char *)data);
}
$ ./seal
[main] buf: 0x40838000 [rwRW,0x40838000-0x40838020], valid: 1
[main] sealer: 0x4 [,0x4-0x2000], valid: 1
[main] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1
[after] unsealed: 0x40838000 [rwRW,0x40838000-0x40838020], Shh! This is a secret!
[untrusted func] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1
[untrusted func] read as char *: Shh! This is a secret!

To address this potential security issue, we can create our own sealer, and seal sensetive capabilities with it instead of the OS canonical one. The reason for doing so is that, in CHERI, a sealed capability can only be unseal with its original sealer.

In order to create our own sealer, we can derive it from the the OS canonical one. But before that, we can take a look at output for the OS canonical one, which serves as the userspace root sealer.

$ ./seal
...
[main] sealer: 0x4 [,0x4-0x2000], valid: 1
...

As shown above, the address range of the OS canonical sealer is [0x4, 0x2000), and its current offset is 0x0. Therefore, in this example, we can derive our own sealer by change the offset of the root sealer:

void * get_derived_sealer() {
  static void * sealer = NULL;
  if (!sealer) {
    void * root_sealer = get_system_sealer();
    size_t offset = arc4random() % cheri_length_get(root_sealer);
    sealer = cheri_offset_set(root_sealer, offset);
  }
  return sealer;
}

And now we can seal the secret with the derived sealer in the main function:

int main() {
  char * buf = get_secret();
  printf("[main] buf: %#p, valid: %d\n", buf, cheri_is_valid(buf));

  // we can seal the capability to prevent tampering using `cheri_seal`
  void * sealer = get_derived_sealer();
  printf("[main] sealer: %#p, valid: %d\n", sealer, cheri_is_valid(sealer));
  void * sealed = cheri_seal(buf, sealer);
  printf("[main] sealed: %#p, valid: %d\n", sealed, cheri_is_valid(sealed));

  // we can unseal the capability with the original sealer
  char * unsealed = (char *)cheri_unseal(sealed, sealer);
  printf("[after] unsealed: %#p, %s\n", unsealed, unsealed);

  // pass the sealed capability to an untrusted function
  untrusted_3rd_party_func(sealed);
}

void untrusted_3rd_party_func(void * data) {
  printf("[untrusted func] sealed: %#p, valid: %d\n", data, cheri_is_valid(data));

  void * root_sealer;
  size_t sealcap_size = sizeof(root_sealer);
  sysctlbyname("security.cheri.sealcap", &root_sealer, &sealcap_size, NULL, 0);
  data = cheri_unseal(data, root_sealer);
  printf("[untrusted func] read as char *: %s\n", (char *)data);
}

If we build and run the program now, the untrusted function won’t be able to use the default root sealer to unseal the capability we passed to it, and result in a SIGPROT fault:

$ clang-morello -march=morello+c64 -mabi=purecap \
    -Xclang -morello-vararg=new \
    seal.c -o seal
$ ./seal
[main] buf: 0x40838000 [rwRW,0x40838000-0x40838020], valid: 1
[main] sealer: 0x170c [,0x4-0x2000], valid: 1
[main] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1
[after] unsealed: 0x40838000 [rwRW,0x40838000-0x40838020], Shh! This is a secret!
[untrusted func] sealed: 0x40838000 [rwRW,0x40838000-0x40838020] (sealed), valid: 1
In-address space security exception (core dumped)