Skip to main content

3.3 Certified data

Intermediate
Tutorial

In a previous module, 2.2: Advanced canister calls, you briefly learned about certified variables. In this guide, you'll dive deeper into certified data and take a look at an example that displays how to use certified data.

Recall that certified variables are used as a way for queries to return an authenticated response that can be validated and trusted, since query calls do not go through consensus. This is useful for workflows that require additional layers of authentication, such as applications that handle financial data or important records. For users of the application, they can trust that the information they're interacting with and receiving from the application is trustworthy and secure.

Certified variables are set within an update call to a canister, since an update call changes the canister's state and goes through consensus. Once a certified variable is set, the certification can be read by a query call that returns the certified variable.

Certified variables are part of a broader concept of certified data. Certified data utilizes chain-key cryptography at the canister level to generate a digital signature that can be validated using a permanent, public key that belongs to ICP, whose private key counterpart is distributed across many different nodes on the network. A valid signature can only be generated when the majority of the nodes that store the components of the private key cooperate in a cryptographic protocol.

How data is certified

Certified data can be used to certify all the assets of a dapp's frontend, such as the HTML, CSS, Javascript, image, or video files. When a canister issues a response along with its certificate, an HTTP gateway can be used to verify that certificate before passing the response off to the client.

In each round of message execution on ICP, the message routing layer generates a new state tree that contains that round's information. The tree contains a root hash that is then used by the nodes in the subnet to create a certificate. This state tree contains several pieces of information, including the certified variables of each canister. An example of what a state tree looks like can be found below:

*root*
└── canisters
    ├── <canister id>
              ├── metadata
              ├── module_hash
              ├── controllers
              └── certified_data
                          └── <blob data>
    ├── <canister id>
       ...

The certified data of a canister is limited to being 32 bytes long; if a canister needs to certify more than 32 bytes of information, the canister must hash the data before certifying it.

Data certificates

A certificate for each piece of certified data consists of the root hash of the state tree and a Merkle proof that validates that the certified data belongs to a tree that hashes the root hash.

How canisters certify data

To certify data, a canister uses the following workflow:

  1. First, a canister must construct a hash tree that maps the paths of HTTPS resources to SHA-256 hashes. An example of this tree is:
*root*
└── http_assets
    ├── index.html -> SHA256(body)
    ├── ...
    └── /css/styles.css -> SHA256(body)
  1. Then, the canister computes the root hash of the tree and calls ic0.certified_data_set with the bytes of the hash as the argument.

  2. Lastly, a #IC-Certificate header is added to each certified HTTP response.

Certified data API methods

A canister can interact with and change its certified data by calling the following API methods:

How developers can certify data

Developers can certify their canister's data and assets in two ways:

  • Code can be written that explicitly manages and certifies all assets. In this workflow, the developer must construct a tree containing all of the assets, Merkelize the tree, then compute its root hash.     - To verify the root hash, developers can use libraries within Motoko and Rust, or use the ic-certified-assets library.

  • Alternatively, developers can create a dedicated asset canister. For new projects created with dfx, the 'asset' canister is the 'frontend' canister. Any canister can be configured to be an asset canister by setting the canister's type as 'asset' in the project's dfx.json file. An asset canister's boilerplate code manages and certifies all assets in the background.

Certified data Motoko module

The Motoko base library includes a module called CertifiedData that contains the following system API methods for certified data:

let set : (data : Blob) -> ()
let getCertificate : () -> ?Blob

Generating an HTTP response

On ICP, there is a built-in http_request query method. This method uses information related to an HTTP request as the input and outputs an HTTP response; specifically, the output includes the request's status, headers, and body. If a canister needs to serve HTTP requests, this method should be implemented.

When a canister would like to return a certified asset to a request, the response body will contain the asset and a response header with the name IC-Certificate and a value equal to the asset's certificate.

More information can be found in the HTTP gateway protocol specification.

Example

To demonstrate the concepts you've learned thus far, consider the /index.html asset of the Internet Identity canister (canister ID rdmx6-jaaaa-aaaaa-aaadq-cai). The SHA-256 hash of this asset at the time it was retrieved for this tutorial was 478afb8206ca0b566a7f138e623accd169fa822602d2f6d717fb67d1045f4f0d. The response contained the following header:

IC-Certificate: certificate=:2dn3omR0cmVlgwGDAYMBgwJIY2FuaXN0ZXKDAYMBggRYIIudikoDwH1gRK637olblUhMUX3HlE0Dihj8MTACxGzHgwGCBFggyCRYc8M/ugt8G7C8RPYayn+l4sdBj8gvFotzJELnQ32DAYIEWCA1/+UHZ9SF67w4ssjOi+Jv3ch7WQNzezGmhtuvB+RDpYMCSgAAAAAAAAAHAQGDAYMBgwJOY2VydGlmaWVkX2RhdGGCA1ggWUt10wjWinx0aAWyrNEi/0R7VeuhalDMjGDErzIbZzqCBFgg/VtZRZdYyK/sr3KF2jWeS1rblF+4ajwfDv2ZbCGpaTiCBFggSoI5JS0pCusHP4nh6h780ebr961E0lVnFkFwzF5pZaeCBFggcKidPEGiPoFMPYfEyNGsDRYWmry1iGX0HNUEoKhIATeCBFggR0zdKUZOMcm5EHNl5Tee3XWqbq1gArwUGzZ2FH4rWtmCBFggTkwJcNrh0eJ9FutJcn6th9eCbM2KXnloxed0acxmQNeDAYIEWCA6SNH8IT1JMHEDEE99csK1kw7bqHh7kGMfNDs6popfCoMCRHRpbWWCA0nFnbXrts/65xZpc2lnbmF0dXJlWDCkXN2tcvH5b+xFCzfkuJMqrZDcplfW8vDziJwzx08WOPI4rh2TIGYZ3R6dgQTF0CA=:, tree=:2dn3gwGDAktodHRwX2Fzc2V0c4MBggRYIDgtAGcz5VvevwiEwwZB9zpkt17C9LE6o/O37bEwQUawgwGDAksvaW5kZXguaHRtbIIDWCBHivuCBsoLVmp/E45iOszRafqCJgLS9tcX+2fRBF9PDYIEWCCx2L8SfJwOydBkUxjc8tKXDVUeoiw8qEYI+8b+HRWIWYIEWCAqZ+3yoFSA9s+jbLFbtcVz+wi0HF9x51Kx38qPcBhiDA==:

Using this header, the following data can be extracted:

ROOT HASH: 0b2d843df534ac8ed2331fe2782deb71d23a08d9b4019a8fa695ec7fde93de36
TREE HASH: 594b75d308d68a7c746805b2acd122ff447b55eba16a50cc8c60c4af321b673a
SIGNATURE: a45cddad72f1f96fec450b37e4b8932aad90dca657d6f2f0f3889c33c74f1638f238ae1d93206619dd1e9d8104c5d020
CERTIFICATE TIME: 2022-02-02T08:23:24.851277509+00:00
CERTIFICATE TREE:
HashTree {
    root: Fork(
        Fork(
            Fork(
                Label("canister", Fork(
                    Fork(
                        Pruned(8b9d8a4a03c07d6044aeb7ee895b95484c517dc7944d038a18fc313002c46cc7),
                        Fork(
                            Pruned(c8245873c33fba0b7c1bb0bc44f61aca7fa5e2c7418fc82f168b732442e7437d),
                            Fork(
                                Pruned(35ffe50767d485ebbc38b2c8ce8be26fddc87b5903737b31a686dbaf07e443a5),
                                Label(0x00000000000000070101, Fork(
                                    Fork(
                                        Label("certified_data", Leaf(0x594b75d308d68a7c746805b2acd122ff447b55eba16a50cc8c60c4af321b673a)),
                                        Pruned(fd5b59459758c8afecaf7285da359e4b5adb945fb86a3c1f0efd996c21a96938),
                                    ),
                                    Pruned(4a8239252d290aeb073f89e1ea1efcd1e6ebf7ad44d25567164170cc5e6965a7),
                                )),
                            ),
                        ),
                    ),
                    Pruned(70a89d3c41a23e814c3d87c4c8d1ac0d16169abcb58865f41cd504a0a8480137),
                )),
                Pruned(474cdd29464e31c9b9107365e5379edd75aa6ead6002bc141b3676147e2b5ad9),
            ),
            Pruned(4e4c0970dae1d1e27d16eb49727ead87d7826ccd8a5e7968c5e77469cc6640d7),
        ),
        Fork(
            Pruned(3a48d1fc213d49307103104f7d72c2b5930edba8787b90631f343b3aa68a5f0a),
            Label("time", Leaf(0xc59db5ebb6cffae716)),
        ),
    ),
}
TREE:
HashTree {
    root: Fork(
        Label("http_assets", Fork(
            Pruned(382d006733e55bdebf0884c30641f73a64b75ec2f4b13aa3f3b7edb1304146b0),
            Fork(
                Label("/index.html", Leaf(0x478afb8206ca0b566a7f138e623accd169fa822602d2f6d717fb67d1045f4f0d)),
                Pruned(b1d8bf127c9c0ec9d0645318dcf2d2970d551ea22c3ca84608fbc6fe1d158859),
            ),
        )),
        Pruned(2a67edf2a05480f6cfa36cb15bb5c573fb08b41c5f71e752b1dfca8f7018620c),
    ),
}

Using certified variables

To demonstrate how to use certified data, open the ICP Ninja certified data example. In this example, the certification of an HTTP response uses a simple interface to store both the response and the certification through a single call.

Take a look at the source code for this project's canister in the backend/app.mo file to see how this code works. Let's take a look at a few snippets of it. For example, the function to submit the HTTP request uses the following code:

backend/app.mo
public query func http_request(req : HttpRequest) : async HttpResponse {
    let cached = cache.get(req.url);

    switch cached {
      case (?body) {
        // Print the body of the response
        let message = Text.decodeUtf8(body);
        switch message {
          case (null) {};
          case (?m) {
            Debug.print(m);
          };
        };
        let response : HttpResponse = {
          status_code : Nat16 = 200;
          headers = [("content-type", "text/html"), cache.certificationHeader(req.url)];
          body = body;
          streaming_strategy = null;
          upgrade = null;
        };

        return response;
      };
      case null {
        Debug.print("Request was not found in cache. Upgrading to update request.\n");
        return {
          status_code = 404;
          headers = [];
          body = Blob.fromArray([]);
          streaming_strategy = null;
          upgrade = ?true;
        };
      };
    };
  };

In this function, if the HTTP query request returns a status code of 200, this indicates a successful request, and it returns the certificationHeader stored in the cache.

If the HTTP query request returns a status code of 404, the request has failed, and the request is upgraded to an update request. Then, the http_request_update function is called, which uses the following code:

backend/app.mo
public func http_request_update(req : HttpRequest) : async HttpResponse {
    let url = req.url;

    Debug.print("Storing request in cache.");
    let time = Time.now();
    let message = "<pre>Request has been stored in cache: \n" # "URL is: " # url # "\n" # "Method is " # req.method # "\n" # "Body is: " # debug_show req.body # "\n" # "Timestamp is: \n" # debug_show Time.now() # "\n" # "</pre>";

    if (req.url == "/" or req.url == "/index.html") {
      let page = main_page();
      let response : HttpResponse = {
        status_code : Nat16 = 200;
        headers = [("content-type", "text/html")];
        body = page;
        streaming_strategy = null;
        upgrade = null;
      };

      let put = cache.put(req.url, page, null);
      return response;
    } else {
      let page = page_template(message);

      let response : HttpResponse = {
        status_code : Nat16 = 200;
        headers = [("content-type", "text/html")];
        body = page;
        streaming_strategy = null;
        upgrade = null;
      };

      let put = cache.put(req.url, page, null);

      // update index
      let indexBody = main_page();
      cache.put("/", indexBody, null);

      return response;
    };
  };

In this function, a request that was upgraded to an update request is stored in the cache with a certification, so that next time it is queried, it can be returned with the certificate and a status code of 200, rather than the status code of 404.

Interacting with certified variables

To make a request to the canister, open the canister's Candid UI URL. Then, click "Call" under the http_request_query method. It should return an error 404 response since there aren't any requests stored in the cache to begin with. It will automatically upgrade the request to an update and store it in the cache.

Then, if you make this call again, the request will be returned automatically without the request having to be upgraded to an update request. In the response, you will see the certificate of the data:

(record {body=vec {60; 104; 116; 109; 108; 62; 60; 104; 101; 97; 100; 62; 60; 109; 101; 116; 97; 32; 110; 97; 109; 101; 61; 39; 118; 105; 101; 119; 112; 111; 114; 116; 39; 32; 99; 111; 110; 116; 101; 110; 116; 61; 39; 119; 105; 100; 116; 104; 61; 100; 101; 118; 105; 99; 101; 45; 119; 105; 100; 116; 104; 44; 32; 105; 110; 105; 116; 105; 97; 108; 45; 115; 99; 97; 108; 101; 61; 49; 39; 62; 60; 108; 105; 110; 107; 32; 114; 101; 108; 61; 39; 115; 116; 121; 108; 101; 115; 104; 101; 101; 116; 39; 32; 104; 114; 101; 102; 61; 39; 104; 116; 116; 112; 115; 58; 47; 47; 117; 110; 112; 107; 103; 46; 99; 111; 109; 47; 99; 104; 111; 116; 97; 64; 108; 97; 116; 101; 115; 116; 39; 62; 60; 116; 105; 116; 108; 101; 62; 73; 67; 32; 99; 101; 114; 116; 105; 102; 105; 101; 100; 32; 97; 115; 115; 101; 116; 115; 32; 100; 101; 109; 111; 60; 47; 116; 105; 116; 108; 101; 62; 60; 47; 104; 101; 97; 100; 62; 60; 98; 111; 100; 121; 62; 60; 100; 105; 118; 32; 99; 108; 97; 115; 115; 61; 39; 99; 111; 110; 116; 97; 105; 110; 101; 114; 39; 32; 114; 111; 108; 101; 61; 39; 100; 111; 99; 117; 109; 101; 110; 116; 39; 62; 60; 112; 114; 101; 62; 82; 101; 113; 117; 101; 115; 116; 32; 104; 97; 115; 32; 98; 101; 101; 110; 32; 115; 116; 111; 114; 101; 100; 32; 105; 110; 32; 99; 97; 99; 104; 101; 58; 32; 10; 85; 82; 76; 32; 105; 115; 58; 32; 10; 77; 101; 116; 104; 111; 100; 32; 105; 115; 32; 10; 66; 111; 100; 121; 32; 105; 115; 58; 32; 34; 34; 10; 84; 105; 109; 101; 115; 116; 97; 109; 112; 32; 105; 115; 58; 32; 10; 43; 49; 95; 55; 52; 49; 95; 50; 57; 52; 95; 55; 56; 56; 95; 57; 49; 56; 95; 53; 52; 54; 95; 57; 56; 57; 10; 60; 47; 112; 114; 101; 62; 60; 47; 100; 105; 118; 62; 60; 47; 98; 111; 100; 121; 62; 60; 47; 104; 116; 109; 108; 62}; headers=vec {record {"content-type"; "text/html"}; record {"ic-certificate"; "certificate=:2dn3o2R0cmVlgwGDAYMBggRYIH95Xq8hH7PaMh0ikUKbrUOoaaHM/XYf9aNfGjYjIqH0gwJIY2FuaXN0ZXKDAYIEWCB2sZMeGXOE+sGIEqYPOjvHmvS0ZyyW/r+TKI+CUdS09oMBgwGCBFggiHJvSkBbY+OwEk/iHzUvCOYkq+97yREBgaB2htjE1k6DAYMBggRYIDg5hKENC+HoMONJv0IZxuj92XtUSg0pGd/3LXkwP8zAgwGCBFgg5bj4KSDe8x80+UolD6X5DotRCz53gqbVhF0kBd2M8aKDAYMBgwGCBFggXuYzJcmCOvNk+3Hbf8ZJBZdRlLKPRtE1gmzcqnhixLGDAYMBgwGDAkoAAAAAADALFQEBgwGDAYMCTmNlcnRpZmllZF9kYXRhggNYIGjrwAYOKbzNf9Pik8Qaenhw5wuQUNFCKwvJH6NJezAJggRYIOZYPTpY2YGxf5mB+0Z68Ho8mLJRVJfjKGeWfSzWEQiHggRYIEq7TgH+HYOGTkvtgHAmJAS/4k0XAGZMIZFf5c7mKzV8ggRYICYcCnLK7xDuqmbX0KIr+azmZPdgC6fZjFtARxtA18FfggRYIFhCz9UfqnXVOPiwG8fZZEOrJ4Amsf2XVEzc1THR+SWyggRYIBm496bmCQNlpPzWLzlqiH7cQ3fcDHz3Tx4P8McDZ9koggRYIFbi8tw2jYm2dMbcaCoavadkH0lt0VlA5iG44iIn73i2ggRYIDbj/uXRv4TLhV0M4GmxSL6OiDzmjJN9Yty/P55QtiPnggRYIOtR2xKby4powcPLgvlLX45BaTAUm+9J94KshUEi09rwggRYICwKe1gt6Id86afBH0EtnzprPDldHi47h8otcDrDmtsMggRYIHGoa2yaefljnUiqx8a6NOL4o2mOqWEj76+OHiJCLAT0gwGCBFggR/y+V96q0OalbVk+N42oHKM2jLWXjXO+Q/DHmoTm+6eDAkR0aW1lggNJrLipnIzOlJUYaXNpZ25hdHVyZVgwkG5ctbCG3cmeoj5ioKqw/qlWAjyLBHnvGwUUiL3ICQ+HtQ+sCfy6Nv05d+GRhS5famRlbGVnYXRpb26iaXN1Ym5ldF9pZFgdw7lupH4EyJd8twPxWq9aqq9z1ZbHvhmkOnGNiwJrY2VydGlmaWNhdGVZAjHZ2feiZHRyZWWDAYIEWCA4JvPJWsHXI4KJgS6EbnAKyKspzKLAcZFGip+O7yPlYIMBgwGCBFggeX1CyOT1kassDqRyAecS5HNgE1ZccnJOC4ju22ffjyeDAkZzdWJuZXSDAYIEWCCktZ0dMTiiE/sQngk82YrksC34jYWP/OrgasBa+bx4YYMBgwGDAYMCWB3DuW6kfgTIl3y3A/Far1qqr3PVlse+GaQ6cY2LAoMBgwJPY2FuaXN0ZXJfcmFuZ2VzggNYG9nZ94GCSgAAAAAAMAAAAQFKAAAAAAA///8BAYMCSnB1YmxpY19rZXmCA1iFMIGCMB0GDSsGAQQBgtx8BQMBAgEGDCsGAQQBgtx8BQMCAQNhAIe8xygapWIEVNViHaV1Av0KyfEAMc7MnfZxWZTDCsdvISeI0j26iqBcjsEJCQVhzgfFznrvnySO7NHQqXvKsil3+5hW9L+1ztH8VpMyC3tu9f4oxpaIuSqNrmHK52LrR4IEWCB8UnE056bG6ld7GSNoueLdWnD48HXVlqNrhljoaiYi3IIEWCAUWeDIR/nzfKfbxG5FxlOHoYB9tLZqqiBjkjUM5w6wq4IEWCDqQ7o4kiwKqNimRX1aK6PPrL3fJpZyEi8c/3iApOXyroMCRHRpbWWCA0n6zeTMkdL2lBhpc2lnbmF0dXJlWDCRWL0ALcZQOo832TH6p4WDIZePec3qYhzuaDd7WgnEk5aqSWGsCOSBXTN6lK+PP/w=:, tree=:2dn3gwJLaHR0cF9hc3NldHODAYMCQIIDWCAulU6i05Lor+BUVf5P0/xZSpFnvj46oIGTxnZAViIA5YIEWCDQDBXcUOzZR6JU+GCVkisJqqQ+Un3IrWIezpqlwMjUuA==:"}}; upgrade=null; streaming_strategy=null; status_code=200})

That'll wrap up this module! Want to learn more about certified variables? Check out the resources below.

Resources

Canisters using HTTP asset certification:

ICP AstronautNeed help?

Did you get stuck somewhere in this tutorial, or do you feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out: