role.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.SuccinctRoles = exports.DelegatedRole = exports.Role = exports.TOP_LEVEL_ROLE_NAMES = void 0;
  7. const crypto_1 = __importDefault(require("crypto"));
  8. const minimatch_1 = require("minimatch");
  9. const util_1 = __importDefault(require("util"));
  10. const error_1 = require("./error");
  11. const utils_1 = require("./utils");
  12. exports.TOP_LEVEL_ROLE_NAMES = [
  13. 'root',
  14. 'targets',
  15. 'snapshot',
  16. 'timestamp',
  17. ];
  18. /**
  19. * Container that defines which keys are required to sign roles metadata.
  20. *
  21. * Role defines how many keys are required to successfully sign the roles
  22. * metadata, and which keys are accepted.
  23. */
  24. class Role {
  25. constructor(options) {
  26. const { keyIDs, threshold, unrecognizedFields } = options;
  27. if (hasDuplicates(keyIDs)) {
  28. throw new error_1.ValueError('duplicate key IDs found');
  29. }
  30. if (threshold < 1) {
  31. throw new error_1.ValueError('threshold must be at least 1');
  32. }
  33. this.keyIDs = keyIDs;
  34. this.threshold = threshold;
  35. this.unrecognizedFields = unrecognizedFields || {};
  36. }
  37. equals(other) {
  38. if (!(other instanceof Role)) {
  39. return false;
  40. }
  41. return (this.threshold === other.threshold &&
  42. util_1.default.isDeepStrictEqual(this.keyIDs, other.keyIDs) &&
  43. util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
  44. }
  45. toJSON() {
  46. return {
  47. keyids: this.keyIDs,
  48. threshold: this.threshold,
  49. ...this.unrecognizedFields,
  50. };
  51. }
  52. static fromJSON(data) {
  53. const { keyids, threshold, ...rest } = data;
  54. if (!utils_1.guard.isStringArray(keyids)) {
  55. throw new TypeError('keyids must be an array');
  56. }
  57. if (typeof threshold !== 'number') {
  58. throw new TypeError('threshold must be a number');
  59. }
  60. return new Role({
  61. keyIDs: keyids,
  62. threshold,
  63. unrecognizedFields: rest,
  64. });
  65. }
  66. }
  67. exports.Role = Role;
  68. function hasDuplicates(array) {
  69. return new Set(array).size !== array.length;
  70. }
  71. /**
  72. * A container with information about a delegated role.
  73. *
  74. * A delegation can happen in two ways:
  75. * - ``paths`` is set: delegates targets matching any path pattern in ``paths``
  76. * - ``pathHashPrefixes`` is set: delegates targets whose target path hash
  77. * starts with any of the prefixes in ``pathHashPrefixes``
  78. *
  79. * ``paths`` and ``pathHashPrefixes`` are mutually exclusive: both cannot be
  80. * set, at least one of them must be set.
  81. */
  82. class DelegatedRole extends Role {
  83. constructor(opts) {
  84. super(opts);
  85. const { name, terminating, paths, pathHashPrefixes } = opts;
  86. this.name = name;
  87. this.terminating = terminating;
  88. if (opts.paths && opts.pathHashPrefixes) {
  89. throw new error_1.ValueError('paths and pathHashPrefixes are mutually exclusive');
  90. }
  91. this.paths = paths;
  92. this.pathHashPrefixes = pathHashPrefixes;
  93. }
  94. equals(other) {
  95. if (!(other instanceof DelegatedRole)) {
  96. return false;
  97. }
  98. return (super.equals(other) &&
  99. this.name === other.name &&
  100. this.terminating === other.terminating &&
  101. util_1.default.isDeepStrictEqual(this.paths, other.paths) &&
  102. util_1.default.isDeepStrictEqual(this.pathHashPrefixes, other.pathHashPrefixes));
  103. }
  104. isDelegatedPath(targetFilepath) {
  105. if (this.paths) {
  106. return this.paths.some((pathPattern) => isTargetInPathPattern(targetFilepath, pathPattern));
  107. }
  108. if (this.pathHashPrefixes) {
  109. const hasher = crypto_1.default.createHash('sha256');
  110. const pathHash = hasher.update(targetFilepath).digest('hex');
  111. return this.pathHashPrefixes.some((pathHashPrefix) => pathHash.startsWith(pathHashPrefix));
  112. }
  113. return false;
  114. }
  115. toJSON() {
  116. const json = {
  117. ...super.toJSON(),
  118. name: this.name,
  119. terminating: this.terminating,
  120. };
  121. if (this.paths) {
  122. json.paths = this.paths;
  123. }
  124. if (this.pathHashPrefixes) {
  125. json.path_hash_prefixes = this.pathHashPrefixes;
  126. }
  127. return json;
  128. }
  129. static fromJSON(data) {
  130. const { keyids, threshold, name, terminating, paths, path_hash_prefixes, ...rest } = data;
  131. if (!utils_1.guard.isStringArray(keyids)) {
  132. throw new TypeError('keyids must be an array of strings');
  133. }
  134. if (typeof threshold !== 'number') {
  135. throw new TypeError('threshold must be a number');
  136. }
  137. if (typeof name !== 'string') {
  138. throw new TypeError('name must be a string');
  139. }
  140. if (typeof terminating !== 'boolean') {
  141. throw new TypeError('terminating must be a boolean');
  142. }
  143. if (utils_1.guard.isDefined(paths) && !utils_1.guard.isStringArray(paths)) {
  144. throw new TypeError('paths must be an array of strings');
  145. }
  146. if (utils_1.guard.isDefined(path_hash_prefixes) &&
  147. !utils_1.guard.isStringArray(path_hash_prefixes)) {
  148. throw new TypeError('path_hash_prefixes must be an array of strings');
  149. }
  150. return new DelegatedRole({
  151. keyIDs: keyids,
  152. threshold,
  153. name,
  154. terminating,
  155. paths,
  156. pathHashPrefixes: path_hash_prefixes,
  157. unrecognizedFields: rest,
  158. });
  159. }
  160. }
  161. exports.DelegatedRole = DelegatedRole;
  162. // JS version of Ruby's Array#zip
  163. const zip = (a, b) => a.map((k, i) => [k, b[i]]);
  164. function isTargetInPathPattern(target, pattern) {
  165. const targetParts = target.split('/');
  166. const patternParts = pattern.split('/');
  167. if (patternParts.length != targetParts.length) {
  168. return false;
  169. }
  170. return zip(targetParts, patternParts).every(([targetPart, patternPart]) => (0, minimatch_1.minimatch)(targetPart, patternPart));
  171. }
  172. /**
  173. * Succinctly defines a hash bin delegation graph.
  174. *
  175. * A ``SuccinctRoles`` object describes a delegation graph that covers all
  176. * targets, distributing them uniformly over the delegated roles (i.e. bins)
  177. * in the graph.
  178. *
  179. * The total number of bins is 2 to the power of the passed ``bit_length``.
  180. *
  181. * Bin names are the concatenation of the passed ``name_prefix`` and a
  182. * zero-padded hex representation of the bin index separated by a hyphen.
  183. *
  184. * The passed ``keyids`` and ``threshold`` is used for each bin, and each bin
  185. * is 'terminating'.
  186. *
  187. * For details: https://github.com/theupdateframework/taps/blob/master/tap15.md
  188. */
  189. class SuccinctRoles extends Role {
  190. constructor(opts) {
  191. super(opts);
  192. const { bitLength, namePrefix } = opts;
  193. if (bitLength <= 0 || bitLength > 32) {
  194. throw new error_1.ValueError('bitLength must be between 1 and 32');
  195. }
  196. this.bitLength = bitLength;
  197. this.namePrefix = namePrefix;
  198. // Calculate the suffix_len value based on the total number of bins in
  199. // hex. If bit_length = 10 then number_of_bins = 1024 or bin names will
  200. // have a suffix between "000" and "3ff" in hex and suffix_len will be 3
  201. // meaning the third bin will have a suffix of "003".
  202. this.numberOfBins = Math.pow(2, bitLength);
  203. // suffix_len is calculated based on "number_of_bins - 1" as the name
  204. // of the last bin contains the number "number_of_bins -1" as a suffix.
  205. this.suffixLen = (this.numberOfBins - 1).toString(16).length;
  206. }
  207. equals(other) {
  208. if (!(other instanceof SuccinctRoles)) {
  209. return false;
  210. }
  211. return (super.equals(other) &&
  212. this.bitLength === other.bitLength &&
  213. this.namePrefix === other.namePrefix);
  214. }
  215. /***
  216. * Calculates the name of the delegated role responsible for 'target_filepath'.
  217. *
  218. * The target at path ''target_filepath' is assigned to a bin by casting
  219. * the left-most 'bit_length' of bits of the file path hash digest to
  220. * int, using it as bin index between 0 and '2**bit_length - 1'.
  221. *
  222. * Args:
  223. * target_filepath: URL path to a target file, relative to a base
  224. * targets URL.
  225. */
  226. getRoleForTarget(targetFilepath) {
  227. const hasher = crypto_1.default.createHash('sha256');
  228. const hasherBuffer = hasher.update(targetFilepath).digest();
  229. // can't ever need more than 4 bytes (32 bits).
  230. const hashBytes = hasherBuffer.subarray(0, 4);
  231. // Right shift hash bytes, so that we only have the leftmost
  232. // bit_length bits that we care about.
  233. const shiftValue = 32 - this.bitLength;
  234. const binNumber = hashBytes.readUInt32BE() >>> shiftValue;
  235. // Add zero padding if necessary and cast to hex the suffix.
  236. const suffix = binNumber.toString(16).padStart(this.suffixLen, '0');
  237. return `${this.namePrefix}-${suffix}`;
  238. }
  239. *getRoles() {
  240. for (let i = 0; i < this.numberOfBins; i++) {
  241. const suffix = i.toString(16).padStart(this.suffixLen, '0');
  242. yield `${this.namePrefix}-${suffix}`;
  243. }
  244. }
  245. /***
  246. * Determines whether the given ``role_name`` is in one of
  247. * the delegated roles that ``SuccinctRoles`` represents.
  248. *
  249. * Args:
  250. * role_name: The name of the role to check against.
  251. */
  252. isDelegatedRole(roleName) {
  253. const desiredPrefix = this.namePrefix + '-';
  254. if (!roleName.startsWith(desiredPrefix)) {
  255. return false;
  256. }
  257. const suffix = roleName.slice(desiredPrefix.length, roleName.length);
  258. if (suffix.length != this.suffixLen) {
  259. return false;
  260. }
  261. // make sure the suffix is a hex string
  262. if (!suffix.match(/^[0-9a-fA-F]+$/)) {
  263. return false;
  264. }
  265. const num = parseInt(suffix, 16);
  266. return 0 <= num && num < this.numberOfBins;
  267. }
  268. toJSON() {
  269. const json = {
  270. ...super.toJSON(),
  271. bit_length: this.bitLength,
  272. name_prefix: this.namePrefix,
  273. };
  274. return json;
  275. }
  276. static fromJSON(data) {
  277. const { keyids, threshold, bit_length, name_prefix, ...rest } = data;
  278. if (!utils_1.guard.isStringArray(keyids)) {
  279. throw new TypeError('keyids must be an array of strings');
  280. }
  281. if (typeof threshold !== 'number') {
  282. throw new TypeError('threshold must be a number');
  283. }
  284. if (typeof bit_length !== 'number') {
  285. throw new TypeError('bit_length must be a number');
  286. }
  287. if (typeof name_prefix !== 'string') {
  288. throw new TypeError('name_prefix must be a string');
  289. }
  290. return new SuccinctRoles({
  291. keyIDs: keyids,
  292. threshold,
  293. bitLength: bit_length,
  294. namePrefix: name_prefix,
  295. unrecognizedFields: rest,
  296. });
  297. }
  298. }
  299. exports.SuccinctRoles = SuccinctRoles;