Skip to content

transmute-industries/cose

Repository files navigation

cose

CI

Usage

🔥 This package is not stable or suitable for production use 🚧

npm install '@transmute/cose'
const cose = require("@transmute/cose");
import * as cose from "@transmute/cose";

Examples

COSE Receipts & Signature Transparency

import crypto from "crypto";
import sqlite from "better-sqlite3";
import * as cose from "@transmute/cose";

const create_software_producer = async ({
  website,
  product,
}: {
  website: string;
  product: string;
}) => {
  const privateKeyJwk = await cose.crypto.key.generate<
    "ES256",
    "application/jwk+json"
  >({
    type: "application/jwk+json",
    algorithm: "ES256",
  });
  const publicKeyJwk = cose.public_from_private({
    key: privateKeyJwk,
    type: "application/jwk+json",
  });
  const signer = cose.hash.signer({
    remote: cose.crypto.signer({
      privateKeyJwk,
    }),
  });
  const verifier = cose.detached.verifier({
    resolver: {
      resolve: async () => {
        return publicKeyJwk;
      },
    },
  });

  return { website, product, signer, verifier, public_key: publicKeyJwk };
};

const create_sqlite_log = (database: string) => {
  const db = new sqlite(database);

  db.prepare(
    `
  CREATE TABLE IF NOT EXISTS tiles 
  (id TEXT PRIMARY KEY, data BLOB);
  
  `
  ).run();

  db.prepare(
    `
  CREATE TABLE IF NOT EXISTS kv 
  (key text unique, value text);
  `
  ).run();

  const hash_size = 32;
  const tile_height = 2;

  const log = new cose.TileLog({
    tile_height,
    hash_size,
    read_tree_size: () => {
      const rows = db
        .prepare(
          `
        SELECT * FROM kv
        WHERE key = 'tree_size'
                `
        )
        .all();
      const [row] = rows as { key: string; value: string }[];
      try {
        return parseInt(row.value, 10);
      } catch (e) {
        // console.error(e)
        return 0;
      }
    },
    update_tree_size: (new_tree_size: number) => {
      try {
        db.prepare(
          `
      INSERT OR REPLACE INTO kv (key, value)
      VALUES( 'tree_size',	'${new_tree_size}');
              `
        ).run();
      } catch (e) {
        // console.error(e)
        // ignore errors
      }
    },

    read_tree_root: function () {
      const rows = db
        .prepare(
          `
        SELECT * FROM kv
        WHERE key = 'tree_root'
                `
        )
        .all();
      const [row] = rows as { key: string; value: string }[];
      try {
        return new Uint8Array(Buffer.from(row.value, "hex"));
      } catch (e) {
        return null;
      }
    },
    update_tree_root: (new_tree_root: Uint8Array): void => {
      try {
        db.prepare(
          `
      INSERT OR REPLACE INTO kv (key, value)
      VALUES( 'tree_root',	'${Buffer.from(new_tree_root).toString("hex")}');
              `
        ).run();
      } catch (e) {
        // ignore errors
      }
    },

    read_tile: (tile: string): Uint8Array => {
      const [base_tile] = tile.split(".");
      // look for completed tiles first
      for (let i = 4; i > 0; i--) {
        const tile_path = base_tile + "." + i;
        const rows = db
          .prepare(
            `
            SELECT * FROM tiles
            WHERE id = '${tile_path}'
                    `
          )
          .all();
        if (rows.length) {
          const [row] = rows as { id: string; data: Uint8Array }[];
          return row.data;
        }
      }
      return new Uint8Array(32);
    },
    update_tiles: function (
      tile_path: string,
      start: number,
      end: number,
      stored_hash: Uint8Array
    ) {
      if (end - start !== 32) {
        // this hash was an intermediate of the tile
        // so it will never be persisted
        return null;
      }
      let tile_data = this.read_tile(tile_path);
      if (tile_data.length < end) {
        const expanded_tile_data = new Uint8Array(tile_data.length + 32);
        expanded_tile_data.set(tile_data);
        tile_data = expanded_tile_data;
      }
      tile_data.set(stored_hash, start);
      try {
        db.prepare(
          `
      INSERT INTO tiles (id, data)
      VALUES( '${tile_path}',	x'${Buffer.from(tile_data).toString("hex")}');
              `
        ).run();
      } catch (e) {
        // ignore errors
      }
      return tile_data;
    },
    hash_function: (data: Uint8Array) => {
      return new Uint8Array(crypto.createHash("sha256").update(data).digest());
    },
  });

  return { db, log };
};

const create_transparency_service = async ({
  website,
  database,
}: {
  website: string;
  database: string;
}) => {
  const privateKeyJwk = await cose.crypto.key.generate<
    "ES256",
    "application/jwk+json"
  >({
    type: "application/jwk+json",
    algorithm: "ES256",
  });
  const publicKeyJwk = cose.public_from_private({
    key: privateKeyJwk,
    type: "application/jwk+json",
  });
  const signer = cose.detached.signer({
    remote: cose.crypto.signer({
      privateKeyJwk,
    }),
  });
  const verifier = cose.detached.verifier({
    resolver: {
      resolve: async () => {
        return publicKeyJwk;
      },
    },
  });
  const { log, db } = create_sqlite_log(database);
  const register_signed_statement = async (signed_statement: Uint8Array) => {
    // registration policy goes here...
    // for this test, we accept everything
    const record = await cose.prepare_for_inclusion(signed_statement);
    log.write_record(record);
    const root = log.root();
    const index = log.size();
    const decoded = cose.cbor.decode(signed_statement);
    const signed_statement_header = cose.cbor.decode(decoded.value[0]);
    const signed_statement_claims = signed_statement_header.get(
      cose.header.cwt_claims
    );
    const inclusion_proof = log.inclusion_proof(index, index - 1);
    return signer.sign({
      protectedHeader: cose.ProtectedHeader([
        [cose.header.kid, publicKeyJwk.kid],
        [cose.header.alg, cose.algorithm.es256],
        [
          cose.draft_headers.verifiable_data_structure,
          cose.verifiable_data_structures.rfc9162_sha256,
        ],
        [
          cose.header.cwt_claims,
          cose.CWTClaims([
            [cose.cwt_claims.iss, website], // issuer notary
            // receipt subject is statement subject.
            // ... could be receipts have different subject id
            [
              cose.cwt_claims.sub,
              signed_statement_claims.get(cose.cwt_claims.sub),
            ],
          ]),
        ],
      ]),
      unprotectedHeader: cose.UnprotectedHeader([
        [
          cose.draft_headers.verifiable_data_proofs,
          cose.VerifiableDataStructureProofs([
            [cose.rfc9162_sha256_proof_types.inclusion, [inclusion_proof]],
          ]),
        ],
      ]),
      payload: root,
    });
  };
  return {
    website,
    db,
    signer,
    verifier,
    log,
    register_signed_statement,
    public_key: publicKeyJwk,
  };
};

const software_producer = await create_software_producer({
  website: "https://green.example",
  product: "https://green.example/[email protected]",
});

const blue_notary = await create_transparency_service({
  website: "https://blue.example",
  database: "./tests/draft-ietf-scitt-architecture/blue.transparency.db",
});

const orange_notary = await create_transparency_service({
  website: "https://orange.example",
  database: "./tests/draft-ietf-scitt-architecture/orange.transparency.db",
});

const statement = Buffer.from("large file that never moves over a network");

const signed_statement = await software_producer.signer.sign({
  protectedHeader: cose.ProtectedHeader([
    [cose.header.kid, software_producer.public_key.kid],
    [cose.header.alg, cose.algorithm.es256],
    [cose.draft_headers.payload_hash_algorithm, cose.algorithm.sha_256],
    [cose.draft_headers.payload_preimage_content_type, "application/spdx+json"],
    [cose.draft_headers.payload_location, "https://cloud.example/sbom/42"],
    [
      cose.header.cwt_claims,
      cose.CWTClaims([
        [cose.cwt_claims.iss, software_producer.website],
        [cose.cwt_claims.sub, software_producer.product],
      ]),
    ],
  ]),
  payload: statement,
});

const blue_receipt = await blue_notary.register_signed_statement(
  signed_statement
);
const transparent_statement = await cose.add_receipt(
  signed_statement,
  blue_receipt
);
const orange_receipt = await orange_notary.register_signed_statement(
  transparent_statement
);
const signed_statement_with_multiple_receipts = await cose.add_receipt(
  transparent_statement,
  orange_receipt
);
const statement_hash = new Uint8Array(
  await (await cose.crypto.subtle()).digest("SHA-256", statement)
);
const verification = await verify_transparent_statement(
  statement_hash,
  signed_statement_with_multiple_receipts,
  {
    tree_hasher: blue_notary.log.tree_hasher, // both logs use same tree algorithm
    resolver: {
      resolve: async (token: Buffer) => {
        const decoded = cose.cbor.decode(token);
        const header = cose.cbor.decode(decoded.value[0]);
        const kid = header.get(cose.header.kid);
        switch (kid) {
          case software_producer.public_key.kid: {
            return software_producer.public_key;
          }
          case blue_notary.public_key.kid: {
            return blue_notary.public_key;
          }
          case orange_notary.public_key.kid: {
            return orange_notary.public_key;
          }
          default: {
            throw new Error("Unknown key: " + kid);
          }
        }
      },
    },
  }
);

Example of a transparent signed statement with multiple receipts in extended diagnostic notation:

Transparent Statement

/ cose-sign1 / 18([
  / protected   / <<{
    / key / 4 : "vCl7UcS0ZZY99VpRthDc-0iUjLdfLtnmFqLJ2-Tt8N4",
    / algorithm / 1 : -7,  # ES256
    / hash  / -6800 : -16, # SHA-256
    / content  / -6802 : "application/spdx+json",
    / location / -6801 : "https://cloud.example/sbom/42",
    / claims / 15 : {
      / issuer  / 1 : "https://green.example",
      / subject / 2 : "https://green.example/[email protected]",
    },
  }>>,
  / unprotected / {
    / receipts / 394 : {
      <</ cose-sign1 / 18([
        / protected   / <<{
          / key / 4 : "mxA4KiOkQFZ-dkLebSo3mLOEPR7rN8XtxkJe45xuyJk",
          / algorithm / 1 : -7,  # ES256
          / notary    / 395 : 1, # RFC9162 SHA-256
          / claims / 15 : {
            / issuer  / 1 : "https://blue.example",
            / subject / 2 : "https://green.example/[email protected]",
          },
        }>>,
        / unprotected / {
          / proofs / 396 : {
            / inclusion / -1 : [
              <<[
                / size / 9, / leaf / 8,
                / inclusion path /
                h'7558a95f...e02e35d6'
              ]>>
            ],
          },
        },
        / payload     / null,
        / signature   / h'02d227ed...ccd3774f'
      ])>>,
      <</ cose-sign1 / 18([
        / protected   / <<{
          / key / 4 : "ajOkeBTJou_wPrlExLMw7L9OTCD5ZIOBYc-O6LESe9c",
          / algorithm / 1 : -7,  # ES256
          / notary    / 395 : 1, # RFC9162 SHA-256
          / claims / 15 : {
            / issuer  / 1 : "https://orange.example",
            / subject / 2 : "https://green.example/[email protected]",
          },
        }>>,
        / unprotected / {
          / proofs / 396 : {
            / inclusion / -1 : [
              <<[
                / size / 6, / leaf / 5,
                / inclusion path /
                h'9352f974...4ffa7ce0',
                h'54806f32...f007ea06'
              ]>>
            ],
          },
        },
        / payload     / null,
        / signature   / h'36581f38...a5581960'
      ])>>
    },
  },
  / payload     / h'0167c57c...deeed6d4',
  / signature   / h'2544f2ed...5840893b'
])

COSE RFCs

COSE Drafts

SCITT Drafts

Develop

npm i
npm t
npm run lint
npm run build