Certified Data

The certihi app at https://fqbzl-iqaaa-aaaae-aaanq-cai.ic0.app/ is certified. That is, distinct nodes have signed the content it serves.

More accurately, they have signed the hash of the root of a Merkle tree. To aid verification, our app must supply a Merkle proof along with the certificate constructed by the distinct nodes. It does so via a header named IC-Certificate:

$ dfx canister call --query certihi http_request
(
  record {
    1_092_319_906 = blob "Hi, all!";
    1_661_489_734 = vec {
      record {
        "IC-Certificate";
        "certificate=:2dn3omR0cmVlgwGDAYMBgwJIY2FuaXN0ZXKDAYMBggRYIORXF2kSCClQXWAznVjWDzB+8a+LGxo9BRxsbbAFIjw7gwJKAAAAAAAAAAEBAYMBgwGDAk5jZXJ0aWZpZWRfZGF0YYIDWCDe+optmu9vCjJzdps0OvlAPUkxbQmVwhyDpyCcxyXg6IIEWCAOlSWK24IbhSMZLAeSNp0tGDcaF8UaDR4QKQQPa350j4IEWCBzY29zk9oDvVJuCWm2wZatalkfy5Me93fB3UEHHd9ftIIEWCDt/okOidtL2ZO4BrQFwLClloNGm1b+5MXYjJ6mfK/7wYIEWCDge1cw0o691IAgDi0bJ4kuxqttjQDjoOufBJ3kIqwI+YIEWCCBKLU61AuhbVMu9fqjjMPL2S4/7R32AMjYGjAWvCzO94MBggRYIJQ/ibcwesOX8GpXL9y5E5JM3t+B8w1buBmMUTSvoBVwgwJEdGltZYIDSZP+s+eEuvnFFmlzaWduYXR1cmVYMJY6422rLRGzLocYnHcq5PVWubAzTgM9WcQJ4IM0OY8FiPXCrwt27PQ7e8Vud6I6yA==:, tree=:2dn3gwJLaHR0cF9hc3NldHODAkEvggNYII8I8ISx2GY+w7ZMsU4plibjki1mVv98d/nNPfjCftAv:";
      };
    };
    3_475_804_314 = 200;
  },
)

The certificate and tree fields are Base64 encodings of CBOR data, which can be explored by pasting into cbor.me. The certificate field is easy to handle: just encode the output of ic0.data_certificate_copy. However, we must construct the tree field ourselves.

We strive to make our example self-contained. We depend on a SHA-256 routine and nothing else.

Our example is small enough that writing CBOR and Candid encodings by hand is tolerable, except for the LEB128 encoding of naturals which we perform via leb128(). This leaves Base64, for which we write a barebones encoder.

#include "sha256.h"  // https://github.com/983/SHA-256

#define WASM_IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n)));
#define WASM_EXPORT(n) asm(n) __attribute__((visibility("default")))

typedef unsigned u;
void reply_data_append(void *, u) WASM_IMPORT("ic0", "msg_reply_data_append");
void reply(void) WASM_IMPORT("ic0", "msg_reply");
u cert_size(void) WASM_IMPORT("ic0", "data_certificate_size");
void cert_copy(void *, u, u) WASM_IMPORT("ic0", "data_certificate_copy");
void cert_set(void *, u) WASM_IMPORT("ic0", "certified_data_set");

u b64_length(u n) {return (n+2)/3*4;}
char ch64(u n) {return n<26?n+'A':n<52?n+'a'-26:"0123456789+/"[n-52];}
void b64_append(u n, char(*get)(u)) {
  for (u i = 0; i < n; i += 3) {
    unsigned char buf[3] = {0,0,0};
    u lim = n - i > 3 ? 3 : n - i;
    for (u j = 0; j < lim; j++) buf[j] = get(i + j);
    u x = (buf[0] << 16) | (buf[1] << 8) | buf[2];
    for (u j = 0; j < 4; j++) {
      buf[0] = i + j > n ? '=' : ch64((x >> (6*(3 - j))) & 63);
      reply_data_append(buf, 1);
    }
  }
}

void leb128(u n) {
  char buf[1];
  if (n < 128) {
    *buf = n;
    reply_data_append(buf, 1);
    return;
  }
  *buf = 128 | n&127;
  reply_data_append(buf, 1);
  leb128(n >> 7);
}

// CBOR encoding of the hash-tree:
//  Labeled "http_assets" $ Labeled "/" $ Leaf $ sha256 "Hi, all!"
char rawtree[] = "\xd9\xd9\xf7"  // CBOR Self-Describe.
  "\x83\x02"  // Labeled.
    "\x4B" "http_assets"  // bytes(11).
    "\x83\x02"  // Labeled.
      "\x41" "/"  // bytes(1).
      "\x82\x03"  // Leaf.
        "\x58\x20"  // bytes(32).
  // Will be overwritten with SHA-256("Hi, all!"):
  "0123456789abcdef0123456789abcdef";

#define APPEND(str) do{char msg[]=str; reply_data_append(msg, sizeof(msg) - 1);}while(0)

char return_cert(u i) {char buf[1]; cert_copy(buf, i, 1); return *buf;}
char return_rawtree(u i) {return rawtree[i];}

void go() WASM_EXPORT("canister_query http_request");
void go() {
  // Overwrite leaf contents.
  sha256_bytes("Hi, all!", 8, rawtree + sizeof(rawtree) - 1 - 32);
  APPEND("DIDL\x04"  // 4 Types.
    "\x6c\x03"  // Type #0: Record, 3 fields.
      "\xa2\xf5\xed\x88\x04\x01"  // "body", type #1.
      "\xc6\xa4\xa1\x98\x06\x02"  // "headers", type #2.
      "\x9a\xa1\xb2\xf9\x0c\x7a"  // "status_code", Nat16.
    "\x6d\x7b"  // Type #1: Vec Nat8.
    "\x6d\x03"  // Type #2: Vec of type #3.
    "\x6c\x02"  // Type #3: Record, 2 fields,
    "\x00\x71\x01\x71");  // Field 0, text, field 1, text.
  APPEND(
    "\x01\x00"  // 1 argument of type #0.
    "\x08Hi, all!"  // "body": 8 bytes, "Hi, all!"
    "\x01\x0eIC-Certificate"  // "headers": Vec of size 1, text field 0.
  );
  u n = cert_size();
  leb128(13 + b64_length(n) + 9 + b64_length(sizeof(rawtree) - 1) + 1);
  APPEND("certificate=:");  // Length 13.
  b64_append(n, return_cert);
  APPEND(":, tree=:");  // Length 9.
  b64_append(sizeof(rawtree) - 1, return_rawtree);
  APPEND(":");  // Length 1.
  APPEND("\xc8\x00");  // "status_code": 200.
  reply();
}

// Set certified data to `reconstruct(tree)`.
// See https://sdk.dfinity.org/docs/interface-spec/index.html
#define SHA256_APPEND(sha, str) do{char msg[]=str; sha256_append(sha, msg, sizeof(msg) - 1);}while(0)
void certify() WASM_EXPORT("canister_update certify");
void certify() {
  char hash[32];
  sha256_bytes("Hi, all!", 8, hash);
  struct sha256 sha[1];
  sha256_init(sha);
  SHA256_APPEND(sha, "\x10ic-hashtree-leaf");
  sha256_append(sha, hash, 32);
  sha256_finalize_bytes(sha, hash);
  sha256_init(sha);
  SHA256_APPEND(sha, "\x13ic-hashtree-labeled/");
  sha256_append(sha, hash, 32);
  sha256_finalize_bytes(sha, hash);
  sha256_init(sha);
  SHA256_APPEND(sha, "\x13ic-hashtree-labeledhttp_assets");
  sha256_append(sha, hash, 32);
  sha256_finalize_bytes(sha, hash);
  cert_set(hash, 32);
  // Reply with hash we computed.
  APPEND("DIDL\x01\x6d\x7b\x01\x00\x20");
  reply_data_append(hash, 32);
  reply();
}

To build, first copy sha256.c and sha256.h from this public-domain implementation of SHA-256, then run:

#!/usr/bin/env bash
set -e
wcc="clang --target=wasm32 -c -O3"
wld="wasm-ld-11 --no-entry --export-dynamic --allow-undefined"
$wcc certihi.c sha256.c
$wld certihi.o sha256.o -o certihi.wasm
echo > certihi.did
echo '{"canisters":{"certihi":{"type":"custom","candid":"certihi.did","wasm":"certihi.wasm","build":""}}}' > dfx.json
echo Deploy with:
echo "  $ dfx deploy --network ic"

After deployment, call the certify update method to compute the certificate data. Canister updates leave certified data alone, so we need only call certify after changes that affect HTTP assets.