Skip to content

A library for certifying assets served via HTTP, ensuring the security of query calls on the Internet Computer.

License

Notifications You must be signed in to change notification settings

NatLabs/certified-assets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

5955002 · Jan 27, 2025

History

18 Commits
Jan 27, 2025
Dec 10, 2024
Jan 27, 2025
Jan 27, 2025
Jan 27, 2025
Dec 10, 2024
Dec 26, 2023
Jan 2, 2024
Dec 10, 2024
Dec 10, 2024
Jan 27, 2025
Jun 18, 2024
Jan 27, 2025

Repository files navigation

Certified Assets

A library designed to certify assets served via HTTP on the Internet Computer. This library only stores the certificates, not the assets themselves. It implements the Response Verification Standard and works by certifying data and their endpoints during update calls. Once certified, the certificates are returned as headers in an HTTP response, ensuring the security and integrity of the data.

Note that this library does not store the assets themselves, only the certificates. Either use the ic-assets library to store the assets or define your own storage mechanism.

Motivation

Getting Started

Installation

  1. Install mops
  2. Run the following command in your project directory:
mops install certified-assets

Usage

To begin using Certified Assets, import the module into your project:

  • class version
import CertifiedAssets "mo:certified-assets";
  • stable version
import CertifiedAssets "mo:certified-assets/Stable";

Create a new instance

  • Creates a persistent instance that remains stable through canister upgrades
    stable let cert_store = CertifiedAssets.init_stable_store();
    let certs = CertifiedAssets.CertifiedAssets(?cert_store);

Certify an Asset

Define an Endpoint with the URL where the asset will be hosted, the data for certification, and optionally, details about the HTTP request and response.

An Endpoint is a combination of the url path to the asset, and the http request and response details that are associated with the served asset.

    let endpoint = CertifiedAssets.Endpoint("/hello.txt", ?"Hello, World!");
    certs.certify(endpoint);

The above method creates a new sha256 hash of the data, if you already have the hash, you can pass it in via the hash() method to avoid recomputing it.

    let endpoint = CertifiedAssets.Endpoint("/hello.txt", null)
        .hash(hello_world_sha256_hash);
    certs.certify(endpoint);

Certification V2 allows for the inclusion of additional optional information in the future response's certificate.

These additional parameters include:

  • Flags:
    • no_certification(): if called, none of the data will be certified
    • no_request_certification(): if called, only the response will be certified. Set by default, as request certification is not supported
  • Request methods:
    • method(): the request method, defaults to 'GET' if not set
    • query_param(): the query parameters
    • request_headers(): the request headers
  • Response methods:
    • status(): the response status code, defaults to 200 if not set
    • response_headers(): the response headers

When certifying assets, it's crucial to consider not just the content but also the context in which it's served, including HTTP headers, status code and query parameters.

    let html_page = "<h2 style=\"color: red;\">Hello, World!</h2>";

    let endpoint = CertifiedAssets.Endpoint("/hello.html", html_page);
        .no_request_certification()
        .status(200)
        .response_header("Content-Type", "text/html");

    certs.certify(endpoint);

Update Certified Assets

A unique hash is generated for each endpoint, so any change to the data will require re-certification. To re-certify an asset, you need to remove() the old one and certify() the new one.

    let old_endpoint = CertifiedAssets.Endpoint("/hello.txt", ?"Hello, World!");
    let new_endpoint = CertifiedAssets.Endpoint("/hello.txt", ?"Hello, World! Updated");

    certs.remove(endpoint);
    certs.certify(endpoint);

Serving A Certified Asset

Serving a certified asset it is as easy as adding the two headers (IC-Certificate and IC-CertificateExpression) with the certificates in your HTTP response. Using get_certificate() allows you to retrieve those two headers for the given endpoint. While get_certified_response() returns an updated version of your response with the header certificates added to them. These two functions will search the internal store for the certificates and return it if the search was successful. If not, the function will return an error. To prevent an error ensure that the Http Request and response match the details in the endpoint originally used to certify the asset.

    import Debug "mo:base/Debug";
    import Text "mo:base/Text";
    import CertifiedAssets "mo:certified-assets";

    public func http_request(req : CertifiedAssets.HttpRequest) : CertifiedAssets.HttpResponse {
        assert req.url == "/hello.html";

        let res : CertifiedAssets.HttpResponse = {
            status_code = 200;
            headers = [("Content-Type", "text/html")];
            body = Text.encodeUtf8(html_page);
            streaming_strategy = null;
            upgrade = null;
        };

        let result = certs.get_certified_response(req, res, null);

        let #ok(certified_response) = result else return Debug.trap(debug_show result);

        return certified_response;
    };

Once again, you can include the hash of the data when retrieving the certified response to avoid recomputing it.

    let result = certs.get_certified_response(req, res, ?sha256_of_html_page);

Fallback index.html files

Asset certification V2 allows you to fallback to a previously certified fallback endpoint if the requested file does not exist. Fallbacks only work if there is a certified fallback endpoint that is a prefix of the requested file path. For example, if the requested file is /path/to/file.txt, valid fallback paths include /path/to/, /path/, and /.

  • Certify a fallback endpoint
    let fallback = CertifiedAssets
        .Endpoint("/path/to/", ?"Default Fallback Page")
        .status(200)
        .is_fallback(true);

    certs.certify(endpoint);
  • Request a missing file and fallback to the index.html file
    public query func http_request(req: CertifiedAssets.HttpRequest) : CertifiedAssets.HttpResponse {
        // suppose this request was sent to your canister
        assert req == {
            method = "GET";
            url = "/path/to/unknown/file.txt";
            headers = [];
        };

        // search your file store for the requested file
        // if the file is not found, create a response with the index.html file

        let ?fallback_path = certs.get_fallback_path(req.url); // returns "/path/to/"

        let res : CertifiedAssets.HttpResponse = {
            status_code = 200;
            headers = [];
            body = Text.encodeUtf8("Default Fallback Page");
            streaming_strategy = null;
            upgrade = null;
        };

        let #ok(certified_headers) = certs.get_fallback_response(req, fallback_path, res, null);

        return certified_response;

    };

Testing

Unit tests

  • Install mops
  • Run the following command in your project directory:
mops test

Replica Tests

  • Install zx with npm install -g zx
  • Install dfinity's idl2json package
  • Run the following commands:
    dfx start --background
    zx -i ./z-scripts/canister-tests.mjs

Limitations

  • This implementation does not support request certification.

Credits & References

About

A library for certifying assets served via HTTP, ensuring the security of query calls on the Internet Computer.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published