Skip to main content

Signing a Manifest

To ensure the authenticity of an add-on, the manifest can be signed by its creator. Signing the manifest allows EMET Surf and other clients to verify that the manifest has not been tampered with and was indeed created by the add-on developer.

This guide provides a Node.js 18+ example for generating a private key, signing a JSON manifest, and saving both the private key and signed manifest.

Important: The private key must be kept secret. Anyone with access to the private key can impersonate the add-on creator. The signature should be generated only in a secure environment, and only the signed manifest is shared publicly.


Example (sign-manifest.js)

// sign-manifest.js - tool for signing manifest file
import fs from 'fs';
import { createPrivateKey, encodePrivateKey, signJsonObject } from './crypto.js';

function main() {
// Create a new private key
const privateKeyBytes = createPrivateKey();
const encodedPrivateKey = encodePrivateKey(privateKeyBytes);

// Read input manifest
const manifestRaw = fs.readFileSync('./manifest_unsigned.json', 'utf-8');
const manifest = JSON.parse(manifestRaw);

// Sign manifest
const signedManifest = signJsonObject(manifest, encodedPrivateKey);

// Save outputs
const privateKeyPath = './privkey.key';
const signedManifestPath = './manifest_signed.json';

fs.writeFileSync(privateKeyPath, encodedPrivateKey);
fs.writeFileSync(signedManifestPath, JSON.stringify(signedManifest, null, 2));

console.log(`Private key saved to ${privateKeyPath}`);
console.log(`Signed manifest saved to ${signedManifestPath}`);
}


main();

Toolset (crypto.js)

// crypto.js - Toolset for signing JSON manifests (Node.js 18+)
import crypto from "crypto";
import { base58Encode, base58Decode } from "./base58.js";
import { secp256k1 } from "ethereum-cryptography/secp256k1";
import { keccak256 } from "ethereum-cryptography/keccak";
import { bytesToHex, utf8ToBytes } from "ethereum-cryptography/utils";
import { sha256 } from "ethereum-cryptography/sha256";

const KEY_PREFIX = "E";

// Polyfill getRandomValues() if needed
const getRandomValues = crypto.webcrypto?.getRandomValues ?? ((arr) => {
arr.set(crypto.randomBytes(arr.length));
return arr;
});

/* ---------- Basic Hash Utils ---------- */
export function sha256Bytes(bytes) {
return sha256(bytes);
}

export function doubleSha256Bytes(bytes) {
return sha256Bytes(sha256Bytes(bytes));
}

export function bigIntTo32Bytes(bn) {
const bytes = new Uint8Array(32);
for (let i = 31; i >= 0; i--) {
bytes[i] = Number(bn & 0xffn);
bn >>= 8n;
}
return bytes;
}

/* ---------- Key Encoding / Decoding ---------- */
export function createPrivateKey() {
const privateKey = new Uint8Array(32);
getRandomValues(privateKey);
return privateKey;
}

export function encodePrivateKey(keyBytes) {
if (!(keyBytes instanceof Uint8Array) || keyBytes.length !== 32) {
throw new Error("Private key must be 32 bytes");
}

const checksum = doubleSha256Bytes(keyBytes).slice(0, 4);
const payload = new Uint8Array([...keyBytes, ...checksum]);
const encoded = base58Encode(payload);

return KEY_PREFIX + encoded;
}

export function decodePrivateKey(input) {
if (!input.startsWith(KEY_PREFIX)) throw new Error("Invalid prefix");
const body = input.slice(1);
const decoded = base58Decode(body);
if (decoded.length !== 36) throw new Error("Invalid key length");

const key = decoded.slice(0, 32);
const checksum = decoded.slice(32);
const expected = doubleSha256Bytes(key).slice(0, 4);

for (let i = 0; i < 4; i++) {
if (checksum[i] !== expected[i]) throw new Error("Checksum mismatch");
}

return key;
}

/* ---------- Address / Hashing ---------- */
export function getKeyAddress(privateKey) {
const publicKey = secp256k1.getPublicKey(privateKey, false);
const address = "0x" + bytesToHex(keccak256(publicKey.slice(1)).slice(-20));
return address;
}

export function messageHash(message, usePrefix = false) {
const msgBytes = utf8ToBytes(message);
if (usePrefix) {
const prefix = utf8ToBytes(`\x19Ethereum Signed Message:\n${msgBytes.length}`);
return keccak256(new Uint8Array([...prefix, ...msgBytes]));
}
return keccak256(msgBytes);
}

/* ---------- Signing ---------- */
export function signString(message, encodedPrivateKey, usePrefix = false) {
const privateKey = decodePrivateKey(encodedPrivateKey);
const msgHash = messageHash(message, usePrefix);
const sigObj = secp256k1.sign(msgHash, privateKey);

const rBytes = bigIntTo32Bytes(sigObj.r);
const sBytes = bigIntTo32Bytes(sigObj.s);
const v = sigObj.recovery;
const signatureBytes = new Uint8Array([...rBytes, ...sBytes, v]);

return bytesToHex(signatureBytes);
}

/* ---------- JSON Signing ---------- */
export function canonicalJSONStringify(obj) {
return JSON.stringify(obj, Object.keys(obj).sort());
}

export function signJsonObject(jsonObj, encodedPrivateKey, usePrefix = false) {
const unsigned = { ...jsonObj };
delete unsigned.signature;
delete unsigned.ownerAddress;

const canonical = canonicalJSONStringify(unsigned);
const signature = signString(canonical, encodedPrivateKey, usePrefix);
const ownerAddress = getKeyAddress(decodePrivateKey(encodedPrivateKey));

return {
...unsigned,
ownerAddress,
signature,
};
}

Base58 Utility (base58.js)

// base58.js

// Bitcoin Base58 alphabet
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const BASE = ALPHABET.length;

// Map char -> value for decode
const ALPHABET_MAP = {};
for (let i = 0; i < ALPHABET.length; i++) {
ALPHABET_MAP[ALPHABET[i]] = i;
}

/**
* Encode Uint8Array into Base58 string
*/
export function base58Encode(buffer) {
if (!(buffer instanceof Uint8Array)) {
throw new TypeError("Expected Uint8Array");
}

// Count leading zeros
let zeros = 0;
while (zeros < buffer.length && buffer[zeros] === 0) {
zeros++;
}

// Convert base256 -> base58
let digits = [0];
for (let i = 0; i < buffer.length; i++) {
let carry = buffer[i];
for (let j = 0; j < digits.length; j++) {
carry += digits[j] << 8;
digits[j] = carry % BASE;
carry = (carry / BASE) | 0;
}
while (carry > 0) {
digits.push(carry % BASE);
carry = (carry / BASE) | 0;
}
}

// Add leading zeros
for (let k = 0; k < zeros; k++) {
digits.push(0);
}

// Convert digits to string
return digits.reverse().map(d => ALPHABET[d]).join("");
}

/**
* Decode Base58 string into Uint8Array
*/
export function base58Decode(str) {
if (typeof str !== "string") {
throw new TypeError("Expected string");
}

if (str.length === 0) return new Uint8Array(0);

let zeros = 0;
while (zeros < str.length && str[zeros] === "1") {
zeros++;
}

let bytes = [0];
for (let i = 0; i < str.length; i++) {
let value = ALPHABET_MAP[str[i]];
if (value === undefined) throw new Error("Invalid Base58 character");

let carry = value;
for (let j = 0; j < bytes.length; j++) {
carry += bytes[j] * BASE;
bytes[j] = carry & 0xff;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}

// Add leading zeros
for (let k = 0; k < zeros; k++) {
bytes.push(0);
}

return new Uint8Array(bytes.reverse());
}