How to validate a certificate from JS/TS

@NathanosDev I am developing a test suite in JS/TS using the PocketIC to test a ICRC3 Ledger developed in Motoko.

In Motoko I use ‘CertTree’ package to certify my last block A:

gcs = CertTree.newStore()
...
let ct = CertTree.Ops(gcs);
ct.put([Text.encodeUtf8("A")], A);
ignore ct.setCertifiedData();

When I want to get the last certificate:

let aux = ct.reveal([Text.encodeUtf8("A")]);
let witness = ct.encodeWitness(aux);
return {
    certificate=CertifiedData.getCertificate()
    winess=witness;
};

So now from my js/ts test I can I receive an object with 2 fields (certificate and witness). How can I check in js/ts the consistency of this certificate?

1 Like

You can check the library and example project documented here: response-verification/packages/certificate-verification-js at main · dfinity/response-verification · GitHub

Feel free to let me know if anything is unclear.

1 Like

When trying to import verifyCertification from '@dfinity/certificate-verification from my js/ts test file, I get the following error. I’m using version 2.4 or superior of “@dfinity/certificate-verification”. any idea what could this error be?:


> test:cert
> jest -c ./jest.config.ts -t 'Cert'

Determining test suites to run...2024-07-23T12:54:39.724292Z  INFO pocket_ic_server: The PocketIC server is listening on port 42009
 FAIL  ./cert.spec.ts
  ● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:

    /home/ildefons/neutrinite/rechain_icrc3/test/node_modules/@dfinity/certificate-verification/dist/certificate-verification.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { Certificate as u, Cbor as d, reconstruct as m, compare as h } from "@dfinity/agent";
                                                                                      ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      20 | } from "./build/cert.idl.js";
      21 | import { HttpAgent, compare, lookup_path } from '@dfinity/agent';
    > 22 | import { verifyCertification } from '@dfinity/certificate-verification';
         | ^
      23 | //import { backend, canisterId } from './build';
      24 |
      25 | // ILDE import {ICRCLedgerService, ICRCLedger} from "./icrc_ledger/ledgerCanister";

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1505:14)
      at Object.<anonymous> (cert.spec.ts:22:1)

Ah sorry, the wrong file is linked in the package.json. I will fix it in the next release. In the meantime, you can follow this recommended step from your logs:

To have some of your “node_modules” files transformed, you can specify a custom “transformIgnorePatterns” in your config.

I am not able to connect your suggestion with an actual fix. Could you please help me to modify my jest.config.ts file so I am able to do import { verifyCertification } from '@dfinity/certificate-verification'; ?

The docs are here: Configuring Jest · Jest

Something like this should work, but it’s not tested:

import type {Config} from 'jest';

const config: Config = {
  // your other config...
  transformIgnorePatterns: ['/node_modules/(?!(@dfinity/certificate-verification)/)'],
};

export default config;
1 Like

It worked, we can now import it. Now I have a new problem when creating the verifyCertification object. Below I have copied the code to create the verifyCertification object, the error and the content of the necessary variables. It seems an encoding problem but we cannot figure out:

verifyCertification object creation code:

 const nnsSubnet = pic.getNnsSubnet();
 if (!nnsSubnet) {
       throw new Error('NNS subnet not found');
 }
const rootKey = await pic.getPubKey(nnsSubnet.id);
let data_cert: []|[DataCertificate] = await can.icrc3_get_tip_certificate();// : async ?Trechain.DataCertificate 
if (data_cert != null) {
      let ddddd: undefined|DataCertificate = data_cert[0];
      if (typeof ddddd != "undefined") {
        const certificate = ddddd.certificate;
        const witness = ddddd.hash_tree;
        const tree = await verifyCertification({
          canisterId: Principal.fromText(canCanisterId.toString()),
          encodedCertificate: new Uint8Array(certificate).buffer,
          encodedTree: new Uint8Array(witness).buffer,
          rootKey: rootKey,//pubKey,//agent.rootKey,
          maxCertificateTimeOffsetMs: 50000,
        });

Error:

 unexpected end of buffer

      118 |             console.log("inputs:", inputs)
      119 |         
    > 120 |         const tree = await verifyCertification({
          |                      ^
      121 |           canisterId: Principal.fromText(canCanisterId.toString()),
      122 |           encodedCertificate: new Uint8Array(certificate),
      123 |           encodedTree: new Uint8Array(witness),

      at eob (node_modules/@dfinity/candid/src/utils/leb128.ts:12:9)
      at safeReadUint8 (node_modules/@dfinity/candid/src/utils/leb128.ts:34:5)
      at lebDecode (node_modules/@dfinity/candid/src/utils/leb128.ts:80:12)
      at p (node_modules/@dfinity/certificate-verification/dist/certificate-verification.js:28:14)
      at E (node_modules/@dfinity/certificate-verification/dist/certificate-verification.js:25:10)
      at Object.<anonymous> (cert.spec.ts:120:22)

Content of variables used to create verifyCertification object:

      inputs: {
        canisterId: Principal {
          _arr: Uint8Array(10) [
            255, 255, 255, 255, 255,
            208,   0,   0,   1,   1
          ],
          _isPrincipal: true
        },
        encodedCertificate: Uint8Array(794) [
          217, 217, 247, 163, 100, 116, 114, 101, 101, 131,   1, 131,
            1, 131,   1, 131,   2,  72,  99,  97, 110, 105, 115, 116,
          101, 114, 131,   2,  74, 255, 255, 255, 255, 255, 208,   0,
            0,   1,   1, 131,   1, 131,   1, 131,   2,  78,  99, 101,
          114, 116, 105, 102, 105, 101, 100,  95, 100,  97, 116,  97,
          130,   3,  88,  32, 131, 224, 242, 181,  74, 123, 162, 150,
          129,  46, 207, 222,  77,   5, 126,  76, 171, 119, 133,  88,
          203, 114, 250,  36,  92, 227,  50, 166, 207,  20, 176,  31,
          130,   4,  88,  32,
          ... 694 more items
        ],
        encodedTree: Uint8Array(82) [
          217, 217, 247, 131,   1, 131,   2,  79, 108,  97, 115, 116,
           95,  98, 108, 111,  99, 107,  95, 104,  97, 115, 104, 130,
            3,  88,  32, 224, 251, 149, 223, 148,   8, 157, 207, 254,
           39,  44,  50, 148, 155,  66, 210, 219, 241, 251,  28,   9,
          232,  98, 236, 184, 140, 126,  33,  63, 173,  98, 104, 131,
            2,  80, 108,  97, 115, 116,  95,  98, 108, 111,  99, 107,
           95, 105, 110, 100, 101, 120, 130,   3,  65,   1
        ],
        rootKey: Uint8Array(133) [
           48, 129, 130,  48,  29,   6,  13,  43,   6,   1,   4,   1,
          130, 220, 124,   5,   3,   1,   2,   1,   6,  12,  43,   6,
            1,   4,   1, 130, 220, 124,   5,   3,   2,   1,   3,  97,
            0, 173, 246,  86,  56, 165,  48,  86, 178,  34,  44, 145,
          187,  36,  87, 176,  39,  75, 202, 149,  25, 138,  90, 203,
          218, 223, 231, 253, 114,  23, 143,   6, 155, 222, 168, 217,
          158, 148, 121, 216,   8, 122,  38, 134, 252, 129, 191,  60,
           75,  17, 254,  39,  85, 112, 212, 129, 241, 105, 143, 121,
          212, 104, 175, 224,
          ... 33 more items
        ],
        maxCertificateTimeOffsetMs: 500000000
      }

Can you grab the certificate and witness as hex encoded strings?

If I do as suggested:

function i2hex(i:number ) {
        return ('0' + i.toString(16)).slice(-2);
};

const cert_hex = Array.from(new Uint8Array(certificate)).map(i2hex).join('');
const cert_wit = Array.from(new Uint8Array(witness)).map(i2hex).join('');

I get a syntax error:
Type ‘string’ is not assignable to type ‘ArrayBuffer’.ts(2322)

in the paramter encodedCertificate:

const tree = await verifyCertification({
          canisterId: Principal.fromText(canCanisterId.toString()),
          encodedCertificate: cert_hex,//new Uint8Array(certificate),   <------------
          encodedTree: new Uint8Array(witness),
          rootKey:  new Uint8Array(rootKey),//pubKey,//agent.rootKey,
          maxCertificateTimeOffsetMs: 50000,
        });

Sorry, I meant just grab them in the hex format and paste them here so I can take a look at them.

certificate: d9d9f7a3647472656583018301830183024863616e697374657283024affffffffffd0000001018301830183024e6365727469666965645f646174618203582083e0f2b54a7ba296812ecfde4d057e4cab778558cb72fa245ce332a6cf14b01f82045820eeebdca966b95b97cecdf624e005758a11b5d031b574818565faaa89fc5a258b82045820fa4371885963f8bd6bee84ce6bb3ff38bcae307cd5876da21d2abcac0462074b820458208e51ac0acf852b7bc7d643a6a59d3afdff7ef6b801570ffaac01fbeed25c5bf38204582003c5ec76668668d0f32246f59e363ecce0055f26ad28191ad52371e18acdd5b6830182045820afd90d75a8f505d991a1b255cee1efe3697d0135d8143eda99042f6768f9d59683024474696d65820349c0c8abd2ffd4b8f217697369676e6174757265583096bccbfc5fa4db235a5a91580f0cd9f19e34bf8bc8e6aa59e141e084ba042a1bcd6fcce6b95c620e896ddec9e571629e6a64656c65676174696f6ea2697375626e65745f69644a0100000000000000fc016b6365727469666963617465590185d9d9f7a26474726565830182045820c1407872709828bf5290e8187c9b3577b8fd7f8f68c0e052020fae104b2ec93383018302467375626e6574830182045820cbeb7d480006425aa659ce58cf80d5fec8f4d6f04b3ee16ad4244824126a82b083024a0100000000000000fc01830183024f63616e69737465725f72616e6765738203581bd9d9f781824affffffffffd0000001014affffffffffdfffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100adf65638a53056b2222c91bb2457b0274bca95198a5acbdadfe7fd72178f069bdea8d99e9479d8087a2686fc81bf3c4b11fe275570d481f1698f79d468afe0e57acc1e298f8b69798da7a891bbec197093ec5f475909923d48bfed6843dbed1f83024474696d65820349c0c8abd2ffd4b8f217697369676e6174757265583098e7eadc6aea8ebc2d70ed10fcfefc57d8e8aab231eead00e83e99df630b2c168d89b2752e3e75ff14187b853468529c

witness: d9d9f7830183024f6c6173745f626c6f636b5f6861736882035820e0fb95df94089dcffe272c32949b42d2dbf1fb1c09e862ecb88c7e213fad62688302506c6173745f626c6f636b5f696e64657882034101

They look fine. What version of agent-js are you using?

In package.json I specified "@dfinity/agent": "^1.0.0"
How can I check the specific version that is installed?

The specific version will be available in package-lock.json, or the equivalent file for whatever package manager you’re using. But anything above 1.0.0 should be fine. I’m not sure what else could be going wrong, I’ll have to debug this. Not sure when I will get time for that, but hopefully sometime this week.

I have checked, I am using version “1.4.0”

@NathanosDev thinking about it, could it be that verifyCertification cannot be tested locally in my laptop and I need to generate certificates from a canister running in a real sub-net?

As long as you use the correct keys, it does not matter if it’s a subnet running locally or on mainnet. In your case, you seem to be correctly using the public key of the NNS subnet.

If that was the problem, the error would look differently. The error you’re seeing looks to be related to the format of something, but I’m not sure what.

P.S. It’s likely not the cause of issue, but you can replace Principal.fromText(canCanisterId.toString()) with canCanisterId since it’s already the correct type.

I tried but I get the same unexpected end of buffer error

@ildefons and @NathanosDev Did this ever get resolved? I have my own use case to verify these certs. My format is a bit custom but I’m hoping I can coble it together.

So a couple things I figured out after poking around:

  1. In case it wasn’t obvious, if you have no NNS subnet then your canister will write a certificate with it’s subnet pub key as the rootKey. (root_key for searchers).

  2. Also the pic.getPubKey just returned an array and the verification library needs a byte buffer or it fails due to checking byteLength instead of length.

let nnspubkey = new Uint8Array(await pic.getPubKey(subnet.id));
  1. I had hell getting the npm package to work with my jest and typescript set up so I just rawdogged the ts files from the verification project into mine and then futzed around until I exported validateTree which seemed to get me what I wanted.

And now my test passes. :rocket: