UNPKG

@dfinity/agent

Version:

JavaScript and TypeScript library to interact with the Internet Computer

583 lines 24.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.LookupLabelStatus = exports.LookupSubtreeStatus = exports.LookupPathStatus = exports.Certificate = exports.NodeType = void 0; exports.hashTreeToString = hashTreeToString; exports.lookupResultToBuffer = lookupResultToBuffer; exports.reconstruct = reconstruct; exports.domain_sep = domain_sep; exports.lookup_path = lookup_path; exports.lookup_subtree = lookup_subtree; exports.flatten_forks = flatten_forks; exports.find_label = find_label; exports.check_canister_ranges = check_canister_ranges; const cbor = __importStar(require("./cbor.js")); const errors_ts_1 = require("./errors.js"); const principal_1 = require("@dfinity/principal"); const bls = __importStar(require("./utils/bls.js")); const leb_ts_1 = require("./utils/leb.js"); const utils_1 = require("@noble/hashes/utils"); const buffer_ts_1 = require("./utils/buffer.js"); const sha2_1 = require("@noble/hashes/sha2"); const MINUTES_TO_MSEC = 60 * 1000; const HOURS_TO_MINUTES = 60; const DAYS_TO_HOURS = 24; const DAYS_TO_MINUTES = DAYS_TO_HOURS * HOURS_TO_MINUTES; const DEFAULT_CERTIFICATE_MAX_AGE_IN_MINUTES = 5; const DEFAULT_CERTIFICATE_MAX_MINUTES_IN_FUTURE = 5; // For now, we don't want to set a strict timeout on the certificate delegation freshness, // so we set the max age really far in the past. const DEFAULT_CERTIFICATE_DELEGATION_MAX_AGE_IN_MINUTES = 30 * DAYS_TO_MINUTES; var NodeType; (function (NodeType) { NodeType[NodeType["Empty"] = 0] = "Empty"; NodeType[NodeType["Fork"] = 1] = "Fork"; NodeType[NodeType["Labeled"] = 2] = "Labeled"; NodeType[NodeType["Leaf"] = 3] = "Leaf"; NodeType[NodeType["Pruned"] = 4] = "Pruned"; })(NodeType || (exports.NodeType = NodeType = {})); /** * Make a human readable string out of a hash tree. * @param tree The hash tree to convert to a string */ function hashTreeToString(tree) { const indent = (s) => s .split('\n') .map(x => ' ' + x) .join('\n'); function labelToString(label) { const decoder = new TextDecoder(undefined, { fatal: true }); try { return JSON.stringify(decoder.decode(label)); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return `data(...${label.byteLength} bytes)`; } } switch (tree[0]) { case NodeType.Empty: return '()'; case NodeType.Fork: { if (tree[1] instanceof Array && tree[2] instanceof Uint8Array) { const left = hashTreeToString(tree[1]); const right = hashTreeToString(tree[2]); return `sub(\n left:\n${indent(left)}\n---\n right:\n${indent(right)}\n)`; } else { throw errors_ts_1.UnknownError.fromCode(new errors_ts_1.HashTreeDecodeErrorCode('Invalid tree structure for fork')); } } case NodeType.Labeled: { if (tree[1] instanceof Uint8Array && tree[2] instanceof Uint8Array) { const label = labelToString(tree[1]); const sub = hashTreeToString(tree[2]); return `label(\n label:\n${indent(label)}\n sub:\n${indent(sub)}\n)`; } else { throw errors_ts_1.UnknownError.fromCode(new errors_ts_1.HashTreeDecodeErrorCode('Invalid tree structure for labeled')); } } case NodeType.Leaf: { if (!tree[1]) { throw errors_ts_1.UnknownError.fromCode(new errors_ts_1.HashTreeDecodeErrorCode('Invalid tree structure for leaf')); } else if (Array.isArray(tree[1])) { return JSON.stringify(tree[1]); } return `leaf(...${tree[1].byteLength} bytes)`; } case NodeType.Pruned: { if (!tree[1]) { throw errors_ts_1.UnknownError.fromCode(new errors_ts_1.HashTreeDecodeErrorCode('Invalid tree structure for pruned')); } else if (Array.isArray(tree[1])) { return JSON.stringify(tree[1]); } return `pruned(${(0, utils_1.bytesToHex)(new Uint8Array(tree[1]))}`; } default: { return `unknown(${JSON.stringify(tree[0])})`; } } } function isBufferGreaterThan(a, b) { for (let i = 0; i < a.length; i++) { if (a[i] > b[i]) { return true; } } return false; } class Certificate { #disableTimeVerification = false; #agent = undefined; /** * Create a new instance of a certificate, automatically verifying it. * @param {CreateCertificateOptions} options {@link CreateCertificateOptions} * @throws if the verification of the certificate fails */ static async create(options) { const cert = Certificate.createUnverified(options); await cert.verify(); return cert; } static createUnverified(options) { return new Certificate(options.certificate, options.rootKey, options.canisterId, options.blsVerify ?? bls.blsVerify, options.maxAgeInMinutes, options.disableTimeVerification, options.agent); } constructor(certificate, _rootKey, _canisterId, _blsVerify, _maxAgeInMinutes = DEFAULT_CERTIFICATE_MAX_AGE_IN_MINUTES, disableTimeVerification = false, agent) { this._rootKey = _rootKey; this._canisterId = _canisterId; this._blsVerify = _blsVerify; this._maxAgeInMinutes = _maxAgeInMinutes; this.#disableTimeVerification = disableTimeVerification; this.cert = cbor.decode(certificate); if (agent && 'getTimeDiffMsecs' in agent && 'hasSyncedTime' in agent && 'syncTime' in agent) { this.#agent = agent; } } /** * Lookup a path in the certificate tree, using {@link lookup_path}. * @param path The path to lookup. * @returns The result of the lookup. */ lookup_path(path) { return lookup_path(path, this.cert.tree); } /** * Lookup a subtree in the certificate tree, using {@link lookup_subtree}. * @param path The path to lookup. * @returns The result of the lookup. */ lookup_subtree(path) { return lookup_subtree(path, this.cert.tree); } async verify() { const rootHash = await reconstruct(this.cert.tree); const derKey = await this._checkDelegationAndGetKey(this.cert.delegation); const sig = this.cert.signature; const key = extractDER(derKey); const msg = (0, utils_1.concatBytes)(domain_sep('ic-state-root'), rootHash); const lookupTime = lookupResultToBuffer(this.lookup_path(['time'])); if (!lookupTime) { // Should never happen - time is always present in IC certificates throw errors_ts_1.ProtocolError.fromCode(new errors_ts_1.CertificateVerificationErrorCode('Certificate does not contain a time')); } // Certificate time verification checks if (!this.#disableTimeVerification) { const timeDiffMsecs = this.#agent?.getTimeDiffMsecs() ?? 0; const maxAgeInMsec = this._maxAgeInMinutes * MINUTES_TO_MSEC; const now = new Date(); const adjustedNow = now.getTime() + timeDiffMsecs; const earliestCertificateTime = adjustedNow - maxAgeInMsec; const latestCertificateTime = adjustedNow + DEFAULT_CERTIFICATE_MAX_MINUTES_IN_FUTURE * MINUTES_TO_MSEC; const certTime = (0, leb_ts_1.decodeTime)(lookupTime); const isCertificateTimePast = certTime.getTime() < earliestCertificateTime; const isCertificateTimeFuture = certTime.getTime() > latestCertificateTime; if ((isCertificateTimePast || isCertificateTimeFuture) && this.#agent && !this.#agent.hasSyncedTime()) { await this.#agent.syncTime(this._canisterId); return await this.verify(); } if (isCertificateTimePast) { throw errors_ts_1.TrustError.fromCode(new errors_ts_1.CertificateTimeErrorCode(this._maxAgeInMinutes, certTime, now, timeDiffMsecs, 'past')); } else if (isCertificateTimeFuture) { if (this.#agent?.hasSyncedTime()) { // This case should never happen, and it signals a bug in either the replica or the local system. throw errors_ts_1.UnknownError.fromCode(new errors_ts_1.UnexpectedErrorCode('System time has been synced with the IC network, but certificate is still too far in the future.')); } throw errors_ts_1.TrustError.fromCode(new errors_ts_1.CertificateTimeErrorCode(5, certTime, now, timeDiffMsecs, 'future')); } } try { const sigVer = await this._blsVerify(key, sig, msg); if (!sigVer) { throw errors_ts_1.TrustError.fromCode(new errors_ts_1.CertificateVerificationErrorCode('Invalid signature')); } } catch (err) { throw errors_ts_1.TrustError.fromCode(new errors_ts_1.CertificateVerificationErrorCode('Signature verification failed', err)); } } async _checkDelegationAndGetKey(d) { if (!d) { return this._rootKey; } const cert = Certificate.createUnverified({ certificate: d.certificate, rootKey: this._rootKey, canisterId: this._canisterId, blsVerify: this._blsVerify, disableTimeVerification: this.#disableTimeVerification, maxAgeInMinutes: DEFAULT_CERTIFICATE_DELEGATION_MAX_AGE_IN_MINUTES, agent: this.#agent, }); if (cert.cert.delegation) { throw errors_ts_1.ProtocolError.fromCode(new errors_ts_1.CertificateHasTooManyDelegationsErrorCode()); } await cert.verify(); const subnetIdBytes = d.subnet_id; const subnetId = principal_1.Principal.fromUint8Array(subnetIdBytes); const canisterInRange = check_canister_ranges({ canisterId: this._canisterId, subnetId, tree: cert.cert.tree, }); if (!canisterInRange) { throw errors_ts_1.TrustError.fromCode(new errors_ts_1.CertificateNotAuthorizedErrorCode(this._canisterId, subnetId)); } const publicKeyLookup = lookupResultToBuffer(cert.lookup_path(['subnet', subnetIdBytes, 'public_key'])); if (!publicKeyLookup) { throw errors_ts_1.TrustError.fromCode(new errors_ts_1.MissingLookupValueErrorCode(`Could not find subnet key for subnet ID ${subnetId.toText()}`)); } return publicKeyLookup; } } exports.Certificate = Certificate; const DER_PREFIX = (0, utils_1.hexToBytes)('308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100'); const KEY_LENGTH = 96; function extractDER(buf) { const expectedLength = DER_PREFIX.byteLength + KEY_LENGTH; if (buf.byteLength !== expectedLength) { throw errors_ts_1.ProtocolError.fromCode(new errors_ts_1.DerKeyLengthMismatchErrorCode(expectedLength, buf.byteLength)); } const prefix = buf.slice(0, DER_PREFIX.byteLength); if (!(0, buffer_ts_1.uint8Equals)(prefix, DER_PREFIX)) { throw errors_ts_1.ProtocolError.fromCode(new errors_ts_1.DerPrefixMismatchErrorCode(DER_PREFIX, prefix)); } return buf.slice(DER_PREFIX.byteLength); } /** * Utility function to constrain the type of a lookup result * @param result the result of a lookup * @returns {Uint8Array | undefined} the value if the lookup was found, `undefined` otherwise */ function lookupResultToBuffer(result) { if (result.status !== LookupPathStatus.Found) { return undefined; } if (result.value instanceof Uint8Array) { return result.value; } return undefined; } /** * @param t The hash tree to reconstruct */ async function reconstruct(t) { switch (t[0]) { case NodeType.Empty: return (0, sha2_1.sha256)(domain_sep('ic-hashtree-empty')); case NodeType.Pruned: return t[1]; case NodeType.Leaf: return (0, sha2_1.sha256)((0, utils_1.concatBytes)(domain_sep('ic-hashtree-leaf'), t[1])); case NodeType.Labeled: return (0, sha2_1.sha256)((0, utils_1.concatBytes)(domain_sep('ic-hashtree-labeled'), t[1], await reconstruct(t[2]))); case NodeType.Fork: return (0, sha2_1.sha256)((0, utils_1.concatBytes)(domain_sep('ic-hashtree-fork'), await reconstruct(t[1]), await reconstruct(t[2]))); default: throw errors_ts_1.UNREACHABLE_ERROR; } } /** * Creates a domain separator for hashing by encoding the input string * with its length as a prefix. * @param s - The input string to encode. * @returns A Uint8Array containing the encoded domain separator. */ function domain_sep(s) { const len = new Uint8Array([s.length]); const str = new TextEncoder().encode(s); return (0, utils_1.concatBytes)(len, str); } function pathToLabel(path) { return (typeof path[0] === 'string' ? (0, utils_1.utf8ToBytes)(path[0]) : path[0]); } var LookupPathStatus; (function (LookupPathStatus) { LookupPathStatus["Unknown"] = "Unknown"; LookupPathStatus["Absent"] = "Absent"; LookupPathStatus["Found"] = "Found"; LookupPathStatus["Error"] = "Error"; })(LookupPathStatus || (exports.LookupPathStatus = LookupPathStatus = {})); var LookupSubtreeStatus; (function (LookupSubtreeStatus) { LookupSubtreeStatus["Absent"] = "Absent"; LookupSubtreeStatus["Unknown"] = "Unknown"; LookupSubtreeStatus["Found"] = "Found"; })(LookupSubtreeStatus || (exports.LookupSubtreeStatus = LookupSubtreeStatus = {})); var LookupLabelStatus; (function (LookupLabelStatus) { LookupLabelStatus["Absent"] = "Absent"; LookupLabelStatus["Unknown"] = "Unknown"; LookupLabelStatus["Found"] = "Found"; LookupLabelStatus["Less"] = "Less"; LookupLabelStatus["Greater"] = "Greater"; })(LookupLabelStatus || (exports.LookupLabelStatus = LookupLabelStatus = {})); /** * Lookup a path in a tree. If the path is a subtree, use {@link lookup_subtree} instead. * @param path the path to look up * @param tree the tree to search * @returns {LookupResult} the result of the lookup */ function lookup_path(path, tree) { if (path.length === 0) { switch (tree[0]) { case NodeType.Empty: { return { status: LookupPathStatus.Absent, }; } case NodeType.Leaf: { if (!tree[1]) { throw errors_ts_1.UnknownError.fromCode(new errors_ts_1.HashTreeDecodeErrorCode('Invalid tree structure for leaf')); } if (tree[1] instanceof Uint8Array) { return { status: LookupPathStatus.Found, value: tree[1].slice(tree[1].byteOffset, tree[1].byteLength + tree[1].byteOffset), }; } throw errors_ts_1.UNREACHABLE_ERROR; } case NodeType.Pruned: { return { status: LookupPathStatus.Unknown, }; } case NodeType.Labeled: case NodeType.Fork: { return { status: LookupPathStatus.Error, }; } default: { throw errors_ts_1.UNREACHABLE_ERROR; } } } const label = pathToLabel(path); const lookupResult = find_label(label, tree); switch (lookupResult.status) { case LookupLabelStatus.Found: { return lookup_path(path.slice(1), lookupResult.value); } case LookupLabelStatus.Absent: case LookupLabelStatus.Greater: case LookupLabelStatus.Less: { return { status: LookupPathStatus.Absent, }; } case LookupLabelStatus.Unknown: { return { status: LookupPathStatus.Unknown, }; } default: { throw errors_ts_1.UNREACHABLE_ERROR; } } } /** * Lookup a subtree in a tree. * @param path the path to look up * @param tree the tree to search * @returns {SubtreeLookupResult} the result of the lookup */ function lookup_subtree(path, tree) { if (path.length === 0) { return { status: LookupSubtreeStatus.Found, value: tree, }; } const label = pathToLabel(path); const lookupResult = find_label(label, tree); switch (lookupResult.status) { case LookupLabelStatus.Found: { return lookup_subtree(path.slice(1), lookupResult.value); } case LookupLabelStatus.Unknown: { return { status: LookupSubtreeStatus.Unknown, }; } case LookupLabelStatus.Absent: case LookupLabelStatus.Greater: case LookupLabelStatus.Less: { return { status: LookupSubtreeStatus.Absent, }; } default: { throw errors_ts_1.UNREACHABLE_ERROR; } } } /** * If the tree is a fork, flatten it into an array of trees * @param {HashTree} t the tree to flatten * @returns {HashTree[]} the flattened tree */ function flatten_forks(t) { switch (t[0]) { case NodeType.Empty: return []; case NodeType.Fork: return flatten_forks(t[1]).concat(flatten_forks(t[2])); default: return [t]; } } /** * Find a label in a tree * @param label the label to find * @param tree the tree to search * @returns {LabelLookupResult} the result of the label lookup */ function find_label(label, tree) { switch (tree[0]) { // if we have a labelled node, compare the node's label to the one we are // looking for case NodeType.Labeled: // if the label we're searching for is greater than this node's label, // we need to keep searching if (isBufferGreaterThan(label, tree[1])) { return { status: LookupLabelStatus.Greater, }; } // if the label we're searching for is equal this node's label, we can // stop searching and return the found node if ((0, buffer_ts_1.uint8Equals)(label, tree[1])) { return { status: LookupLabelStatus.Found, value: tree[2], }; } // if the label we're searching for is not greater than or equal to this // node's label, then it's less than this node's label, and we can stop // searching because we've looked too far return { status: LookupLabelStatus.Less, }; // if we have a fork node, we need to search both sides, starting with the left case NodeType.Fork: { // search in the left node const leftLookupResult = find_label(label, tree[1]); switch (leftLookupResult.status) { // if the label we're searching for is greater than the left node lookup, // we need to search the right node case LookupLabelStatus.Greater: { const rightLookupResult = find_label(label, tree[2]); // if the label we're searching for is less than the right node lookup, // then we can stop searching and say that the label is provably Absent if (rightLookupResult.status === LookupLabelStatus.Less) { return { status: LookupLabelStatus.Absent, }; } // if the label we're searching for is less than or equal to the right // node lookup, then we let the caller handle it return rightLookupResult; } // if the left node returns an uncertain result, we need to search the // right node case LookupLabelStatus.Unknown: { const rightLookupResult = find_label(label, tree[2]); // if the label we're searching for is less than the right node lookup, // then we also need to return an uncertain result if (rightLookupResult.status === LookupLabelStatus.Less) { return { status: LookupLabelStatus.Unknown, }; } // if the label we're searching for is less than or equal to the right // node lookup, then we let the caller handle it return rightLookupResult; } // if the label we're searching for is not greater than the left node // lookup, or the result is not uncertain, we stop searching and return // whatever the result of the left node lookup was, which can be either // Found or Absent default: { return leftLookupResult; } } } // if we encounter a Pruned node, we can't know for certain if the label // we're searching for is present or not case NodeType.Pruned: return { status: LookupLabelStatus.Unknown, }; // if the current node is Empty, or a Leaf, we can stop searching because // we know for sure that the label we're searching for is not present default: return { status: LookupLabelStatus.Absent, }; } } /** * Check if a canister ID falls within the canister ranges of a given subnet * @param params the parameters with which to check the canister ranges * @param params.canisterId the canister ID to check * @param params.subnetId the subnet ID from which to check the canister ranges * @param params.tree the hash tree in which to lookup the subnet's canister ranges * @returns {boolean} `true` if the canister is in the range, `false` otherwise */ function check_canister_ranges(params) { const { canisterId, subnetId, tree } = params; const rangeLookup = lookup_path(['subnet', subnetId.toUint8Array(), 'canister_ranges'], tree); if (rangeLookup.status !== LookupPathStatus.Found) { throw errors_ts_1.ProtocolError.fromCode(new errors_ts_1.LookupErrorCode(`Could not find canister ranges for subnet ${subnetId.toText()}`, rangeLookup.status)); } if (!(rangeLookup.value instanceof Uint8Array)) { throw errors_ts_1.ProtocolError.fromCode(new errors_ts_1.MalformedLookupFoundValueErrorCode(`Could not find canister ranges for subnet ${subnetId.toText()}`)); } const ranges_arr = cbor.decode(rangeLookup.value); const ranges = ranges_arr.map(v => [ principal_1.Principal.fromUint8Array(v[0]), principal_1.Principal.fromUint8Array(v[1]), ]); const canisterInRange = ranges.some(r => r[0].ltEq(canisterId) && r[1].gtEq(canisterId)); return canisterInRange; } //# sourceMappingURL=certificate.js.map