I am wondering what possibilities a user has to verify the frontend is served from an asset canister, for example if that frontend is a web wallet application.
I know the standard asset canister has a form of certification which in my understanding is there to protect against a malicious replica manipulating the assets when serving them. Then there are signed queries which protect against malicious boundary nodes. My question here is what protections are there against the controller of the asset canister changing the assets, even only temporarily. For backend canisters we have the module hash and any changes to it are preserved in the history and publicly accessible. What is there for the assets in an asset canister?
What would be nice it to have a way that the user can
reproduce the build of the assets from a public repo and get to a hash of all the assets
ask the asset canister what hash it is currently serving
verify the wasm module hash of the asset canister
Then the user would know that the asset canister currently serves a specific version of an open-source web app. Is this currently possible, are there best practices to achieve this and are there pre-existing tools?
Of course it is possible to black-hole the asset canister and then it will serve always the same assets and it might be possible to download them once and verify them. That approach is possible but then the app can’t be upgraded and I would be interested in a better approach.
Not all that much right now. The source of truth about existing assets is the certified data that the canister sets. It can be requested from the outside at any time, and it hashes all the possible certified responses, so it should be a solid source of truth. But of course we don’t have a convenient way to get that root hash right now… What do you think would be a nice way to do it? Have a get_recent_hashes function that returns the last n (maybe 20) root hashes plus their timestamps?
Do you mean the current Rust implementation of the asset canister already does that? I mean calculate a hash of its assets and set its certified data blob to that hash? Or do you mean we can do that?
Almost. It does a similar thing and sets the certified data, but it doesn’t really expose a user-friendly way to query that hash.
The asset canister serves certified assets according to the HTTP Gateway spec. As the spec demands the root hash is not a hash of all the assets, but a hash of a tree that includes all valid(=certified) responses, so basically a superset of a hash of all assets. I’m saying superset because for v2 certification this means that even headers are included in the hash tree.
And the root hash is calculated inside the canister? Or is it pre-calculated by dfx and uploaded along with the assets?
If calculated in the canister then it would serve my purpose because then it is a trustworthy fingerprint of the assets and can serve as a “version hash” for the web app. Assuming the module hash of the asset canister checks out against a known list.
It is computed inside the canister (entry point for your auditing desires is here ). And we’re (sort-of accidentally) starting to build a list of known asset canister hashes here.
Can I also have your opinion on how you would best like this information to be exposed? I estimate that it wouldn’t be hard at all to make the current hash easily accessible and to expose a history of changes
We can get it with curl from the gateway interface, or? By “no convenient way” do you mean we can’t get it directly from the canister, for example from another canister?
Re. curl. Can you remind we what URL I have to use? I can’t seem to be able to put it together from the spec.
AFAIK you have to use the state tree for that. From the above-linked section on certified data:
The certificate is a blob as described in Certification that contains the values at path /canister/<canister_id>/certified_data and at path /time of The system state tree.
This is actually really tough…especially from inside the IC as the certs aren’t returned in update calls…since queries are upgraded to updates when calling canister to canister you can’t even get the cert…you can manually return the data. For ICRC3 I’m having to add a get_tip function that returns this so I can do a manual test:
public type Tip = {
// Signature of the root of the hash_tree
last_block_index : Blob;
last_block_hash : Blob;
// CBOR encoded hash_tree
hash_tree : Blob;
};
public func get_tip() : Tip {
debug if(debug_channel.certificate) D.print("in get tip certificate");
switch(environment){
case(null){};
case(?env){
debug if(debug_channel.certificate) D.print("have env");
switch(env.get_certificate_store){
case(null){};
case(?gcs){
debug if(debug_channel.certificate) D.print("have gcs");
let ct = CertTree.Ops(gcs());
let blockWitness = ct.reveal([Text.encodeUtf8("last_block_index")]);
let hashWitness = ct.reveal([Text.encodeUtf8("last_block_hash")]);
let merge = MTree.merge(blockWitness,hashWitness);
let witness = ct.encodeWitness(merge);
return {
last_block_hash = switch(state.latest_hash){
case(null) D.trap("No root");
case(?val) val;
};
last_block_index = encodeBigEndian(state.lastIndex);
hash_tree = witness;
};
};
};
};
};
D.trap("no environment");
};
But there is no way for me to get the current(or last) cert.
The canister certified data root hash is available to the canister during a non-replicated query execution using the ic0.data_certificate_copy system api. It is not available through an external read_state request (unlike other paths in the state-tree which are). Asset canisters use this system-api to get the certificate with the certified data during an http_request call and returns the certificate with the certified_data path as a response header. In certification-v1 the header key is IC-Certificate and the header value is a
format!(“certificate=:{}:, tree=:{}:”, base64(system-api-certificate-cbor-encoded), base64(cbor-encoded-tree)).
Not sure what it is in certification-v2.
So for an asset canister you can get the system-api-certificate by querying the http_request method for an asset. To get the certified-data root hash from the system-api-certificate you can read the path canister/<canister-id>/certified_data in the certificate.
The service-worker in js also implements reading out the certified data root hash from the http_request responses. I have not looked at the code for it though.
For an option with better types, you can also use the ic-agent rust library’s lookup_path method on the system-state-tree contained in the certificate.