@push.rocks/smartlinuxrepo 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/alpine/apkindexreader.d.ts +87 -0
  3. package/dist_ts/alpine/apkindexreader.js +193 -0
  4. package/dist_ts/arch/indexreader.d.ts +35 -0
  5. package/dist_ts/arch/indexreader.js +45 -0
  6. package/dist_ts/arch/reader.js +1 -1
  7. package/dist_ts/debian/indexbuilder.js +8 -23
  8. package/dist_ts/debian/indexreader.d.ts +107 -0
  9. package/dist_ts/debian/indexreader.js +274 -0
  10. package/dist_ts/debian/reader.js +1 -1
  11. package/dist_ts/debian/rules.d.ts +18 -0
  12. package/dist_ts/debian/rules.js +81 -2
  13. package/dist_ts/index.d.ts +14 -4
  14. package/dist_ts/index.js +9 -3
  15. package/dist_ts/readers/decompress.d.ts +1 -2
  16. package/dist_ts/readers/decompress.js +1 -1
  17. package/dist_ts/readers/document.d.ts +36 -0
  18. package/dist_ts/readers/document.js +110 -0
  19. package/dist_ts/rpm/repodata.d.ts +3 -2
  20. package/dist_ts/rpm/repodata.js +5 -4
  21. package/dist_ts/rpm/repodatareader.d.ts +173 -0
  22. package/dist_ts/rpm/repodatareader.js +414 -0
  23. package/dist_ts/rpm/xmlreader.d.ts +70 -0
  24. package/dist_ts/rpm/xmlreader.js +576 -0
  25. package/dist_ts/shared/errors.d.ts +12 -4
  26. package/dist_ts/shared/errors.js +1 -1
  27. package/dist_ts/shared/limits.d.ts +8 -1
  28. package/dist_ts/shared/limits.js +5 -2
  29. package/dist_ts/shared/types.d.ts +2 -0
  30. package/dist_ts/signing/verifiers.d.ts +43 -0
  31. package/dist_ts/signing/verifiers.js +81 -2
  32. package/package.json +1 -1
  33. package/readme.md +53 -2
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/alpine/apkindexreader.ts +265 -0
  36. package/ts/arch/indexreader.ts +66 -0
  37. package/ts/arch/reader.ts +2 -1
  38. package/ts/debian/indexbuilder.ts +13 -23
  39. package/ts/debian/indexreader.ts +372 -0
  40. package/ts/debian/reader.ts +2 -1
  41. package/ts/debian/rules.ts +83 -1
  42. package/ts/index.ts +40 -2
  43. package/ts/readers/decompress.ts +1 -3
  44. package/ts/readers/document.ts +136 -0
  45. package/ts/rpm/repodata.ts +4 -3
  46. package/ts/rpm/repodatareader.ts +582 -0
  47. package/ts/rpm/xmlreader.ts +680 -0
  48. package/ts/shared/errors.ts +13 -4
  49. package/ts/shared/limits.ts +9 -1
  50. package/ts/shared/types.ts +3 -0
  51. package/ts/signing/verifiers.ts +113 -1
@@ -6,7 +6,9 @@ import type { ILinuxPackageLimits } from './limits.js';
6
6
  * - `unsupported-compression`: a compression the format does not allow.
7
7
  * - `malformed-archive`: broken container structure (ar, tar, gzip, xz, zstd framing or data).
8
8
  * - `malformed-header`: a broken package header (RPM lead and headers).
9
- * - `missing-field` / `invalid-field`: package metadata that is absent or not acceptable.
9
+ * - `missing-field` / `invalid-field`: package metadata that is absent or not acceptable; for the index
10
+ * readers, a unit without its key (`Filename`, `pkgid`, `id`, a `<data>` part, the `APKINDEX` member)
11
+ * or a Release checksum line that fails its grammar.
10
12
  * - `unsupported-package-format`: input that is not the expected package format, or uses a form of it
11
13
  * the codec does not read (the data of a sparse tar entry).
12
14
  * - `truncated`: only a clean early end of input.
@@ -17,14 +19,20 @@ import type { ILinuxPackageLimits } from './limits.js';
17
19
  * - `malformed-signature`: an OpenPGP signature or cleartext-signed message the verifier refuses before
18
20
  * any cryptography: armor, encoding, framing, packet count, version, signature type, a hash outside
19
21
  * SHA-256/384/512, a `Hash:` header that does not name the signature's hash, a missing creation time,
20
- * a missing or wildcard issuer, or an unknown critical subpacket or notation.
22
+ * a missing or wildcard issuer, or an unknown critical subpacket or notation; and an Alpine index
23
+ * whose signature member the Alpine verifier refuses (no `.SIGN.` entry, more than one, a type other
24
+ * than `RSA`, an entry that is not a regular `.SIGN.<type>.<keyName>` file).
21
25
  * - `wrong-key`: the signature's issuer is neither the verifier's primary key nor one of its subkeys, or
22
- * its issuer fingerprint disagrees (`detail.value` carries the issuer in hex).
26
+ * its issuer fingerprint disagrees (`detail.value` carries the issuer in hex); for the Alpine verifier,
27
+ * the `.SIGN.RSA.<keyName>` entry names another key (`detail.value` carries that key name).
23
28
  * - `unusable-key`: the issuing key or subkey cannot sign at the signature's creation time or at the
24
29
  * verification date: expired, revoked, no sign flag, a bad binding or back-signature, created after
25
30
  * the signature, or below the algorithm floor.
26
31
  * - `bad-signature`: the cryptographic check failed, the signature was created after the verification
27
32
  * date, or it has expired at that date.
33
+ * - `malformed-index`: an index document an index reader refuses (1.2.0): line framing, block or
34
+ * stanza structure, XML well-formedness, a DOCTYPE, an unexpected root or child element, an
35
+ * ambiguous duplicate key line or element, or a tar member type an index does not hold.
28
36
  *
29
37
  * New codes arrive in minor releases: a consumer that switches over `code` needs a default branch.
30
38
  *
@@ -48,7 +56,8 @@ export type TLinuxRepoErrorCode =
48
56
  | 'malformed-signature'
49
57
  | 'wrong-key'
50
58
  | 'unusable-key'
51
- | 'bad-signature';
59
+ | 'bad-signature'
60
+ | 'malformed-index';
52
61
 
53
62
  /** Names the field, structure or limit an error is about. */
54
63
  export interface ILinuxRepoErrorDetail {
@@ -29,6 +29,11 @@ export interface ILinuxPackageLimits {
29
29
  * `maxMetadataBytes`, not here.
30
30
  */
31
31
  maxHeaderBytes: number;
32
+ /**
33
+ * One decoded index document (decompressed output, or the plain input), in
34
+ * bytes. Only the index readers read it (1.2.0).
35
+ */
36
+ maxIndexBytes: number;
32
37
  }
33
38
 
34
39
  const KiB = 1024;
@@ -44,7 +49,9 @@ const GiB = 1024 * MiB;
44
49
  * - `maxEntries` and `maxFileListBytes` are Gitea's Arch file-list caps
45
50
  * (Alpine's 1024 `.PKGINFO` values fold into `maxEntries`);
46
51
  * - `maxHeaderBytes` covers a full PAX + GNU long name + GNU long link chain at
47
- * Go `archive/tar`'s 1 MiB per special record, and Gitea's 64 KiB line limit.
52
+ * Go `archive/tar`'s 1 MiB per special record, and Gitea's 64 KiB line limit;
53
+ * - `maxIndexBytes` bounds the memory of one index reader call (the decoded
54
+ * document is buffered once); 256 MiB holds a full Debian `main` `Packages`.
48
55
  */
49
56
  export const DEFAULT_LINUX_PACKAGE_LIMITS: Readonly<ILinuxPackageLimits> = Object.freeze({
50
57
  maxPackageBytes: 8 * GiB,
@@ -54,6 +61,7 @@ export const DEFAULT_LINUX_PACKAGE_LIMITS: Readonly<ILinuxPackageLimits> = Objec
54
61
  maxEntries: 100_000,
55
62
  maxFileListBytes: 16 * MiB,
56
63
  maxHeaderBytes: 4 * MiB,
64
+ maxIndexBytes: 256 * MiB,
57
65
  });
58
66
 
59
67
  const LIMIT_KEYS = Object.keys(DEFAULT_LINUX_PACKAGE_LIMITS) as Array<keyof ILinuxPackageLimits>;
@@ -249,3 +249,6 @@ export interface ILinuxIndexFile {
249
249
  /** Other paths serving the same bytes (Debian `by-hash/<ALGO>/<hex>` for every listed hash). */
250
250
  aliases: string[];
251
251
  }
252
+
253
+ /** The stream compressions the readers decode; the caller names it from the file name. */
254
+ export type TLinuxCompression = 'none' | 'gzip' | 'xz' | 'zstd';
@@ -1,6 +1,8 @@
1
1
  import * as plugins from '../plugins.js';
2
+ import { splitAlpineIndexSignature } from '../alpine/apkindexreader.js';
2
3
  import { LinuxRepoError } from '../shared/errors.js';
3
- import { assertBytes, assertString, assertTimestamp } from '../shared/validation.js';
4
+ import { resolveLinuxPackageLimits, type ILinuxPackageLimits } from '../shared/limits.js';
5
+ import { assertBytes, assertSegmentName, assertString, assertTimestamp } from '../shared/validation.js';
4
6
  import {
5
7
  dearmor,
6
8
  decodeArmorBytes,
@@ -355,3 +357,113 @@ async function verifySignature(inputArg: {
355
357
  hashAlgorithm,
356
358
  };
357
359
  }
360
+
361
+ /** An Alpine index signature that verified. */
362
+ export interface IAlpineSignatureVerification {
363
+ /** The key name of the `.SIGN.RSA.<keyName>` entry: the verifier's. */
364
+ readonly keyName: string;
365
+ readonly hashAlgorithm: 'sha1';
366
+ }
367
+
368
+ /**
369
+ * Verifies Alpine index signatures against one RSA public key. Fails closed
370
+ * with a `LinuxRepoError` and never returns a verdict of failure:
371
+ * `malformed-signature`, `wrong-key` or `bad-signature` (bad arguments are
372
+ * `invalid-input`; the index's own framing keeps its reader codes).
373
+ */
374
+ export interface IAlpineVerifier {
375
+ /** The key name apk finds the public key under (`/etc/apk/keys/<keyName>`). */
376
+ readonly keyName: string;
377
+ /**
378
+ * Verifies the one `.SIGN.RSA.<keyName>` entry of an `APKINDEX.tar.gz` over
379
+ * its signed bytes (everything after the signature member): RSASSA-PKCS1-v1_5
380
+ * over SHA-1, as apk-tools 2 and Gitea sign. The split is
381
+ * `readAlpineIndex`'s; no signature member, more than one `.SIGN.` entry or
382
+ * a type other than `RSA` is `malformed-signature`, an entry naming another
383
+ * key `wrong-key` (`detail.value` the entry's key name), and a signature
384
+ * that does not verify `bad-signature`. The format carries no time.
385
+ */
386
+ verifyIndexSignature(inputArg: { index: Uint8Array; limits?: Partial<ILinuxPackageLimits> }): Promise<IAlpineSignatureVerification>;
387
+ }
388
+
389
+ /** The OpenPGP verifier's RSA floor (`VERIFY_CONFIG.minRSABits`), for the Alpine key too. */
390
+ const ALPINE_MIN_RSA_BITS = 2047;
391
+ const ALPINE_PUBLIC_KEY_PEM = /^-----BEGIN PUBLIC KEY-----\n(?:[A-Za-z0-9+/=]+\n)+-----END PUBLIC KEY-----\n?$/;
392
+
393
+ /**
394
+ * Builds an Alpine verifier from one PKIX `PUBLIC KEY` PEM block, the form
395
+ * Gitea serves (`GET /alpine/key`, `util.GenerateKeyPair`) and Alpine installs
396
+ * in `/etc/apk/keys`, and the key name apk finds it under. The PEM must be
397
+ * exactly one block with nothing around it but one final LF, and an RSA key
398
+ * of at least 2047 bits (the OpenPGP verifier's floor); anything else is
399
+ * `invalid-key` (`publicKeyPem`). A `keyName` that is empty or holds `/` or a
400
+ * control character is `invalid-input`.
401
+ */
402
+ export function createAlpineVerifier(inputArg: { publicKeyPem: string; keyName: string }): IAlpineVerifier {
403
+ if (inputArg === null || typeof inputArg !== 'object') {
404
+ throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
405
+ }
406
+ assertString(inputArg.publicKeyPem, 'publicKeyPem');
407
+ assertSegmentName(inputArg.keyName, 'keyName');
408
+ if (!ALPINE_PUBLIC_KEY_PEM.test(inputArg.publicKeyPem)) {
409
+ throw new LinuxRepoError('invalid-key', 'the Alpine public key must be exactly one PKIX "PUBLIC KEY" PEM block', {
410
+ field: 'publicKeyPem',
411
+ });
412
+ }
413
+ let key: plugins.crypto.KeyObject;
414
+ try {
415
+ key = plugins.crypto.createPublicKey({ key: inputArg.publicKeyPem, format: 'pem' });
416
+ } catch (error) {
417
+ throw new LinuxRepoError('invalid-key', 'the Alpine public key cannot be read', { field: 'publicKeyPem' }, {
418
+ cause: error,
419
+ });
420
+ }
421
+ const modulusLength = key.asymmetricKeyDetails?.modulusLength ?? 0;
422
+ if (key.asymmetricKeyType !== 'rsa' || modulusLength < ALPINE_MIN_RSA_BITS) {
423
+ throw new LinuxRepoError('invalid-key', `the Alpine public key must be an RSA key of at least ${ALPINE_MIN_RSA_BITS} bits`, {
424
+ field: 'publicKeyPem',
425
+ });
426
+ }
427
+ const keyName = inputArg.keyName;
428
+ return {
429
+ keyName,
430
+ verifyIndexSignature: async (input) => {
431
+ assertInputObject(input);
432
+ assertBytes(input.index, 'index');
433
+ const limits = resolveLinuxPackageLimits(input.limits);
434
+ const split = await splitAlpineIndexSignature(input.index, limits);
435
+ const malformed = (messageArg: string) => new LinuxRepoError('malformed-signature', messageArg, { field: 'index' });
436
+ if (split.signatures.length === 0) {
437
+ throw malformed('the index is not signed');
438
+ }
439
+ if (split.signatures.length > 1) {
440
+ throw malformed(`the index holds ${split.signatures.length} .SIGN. entries, expected one`);
441
+ }
442
+ const entry = split.signatures[0];
443
+ if (entry.type !== 'RSA') {
444
+ throw malformed(`the index signature type is ${JSON.stringify(entry.type)}, expected "RSA"`);
445
+ }
446
+ if (entry.keyName !== keyName) {
447
+ throw new LinuxRepoError('wrong-key', 'the index was signed under another key name', {
448
+ field: 'index',
449
+ value: entry.keyName,
450
+ });
451
+ }
452
+ let verified: boolean;
453
+ try {
454
+ verified = plugins.crypto.verify(
455
+ 'sha1',
456
+ input.index.subarray(split.signedOffset),
457
+ { key, padding: plugins.crypto.constants.RSA_PKCS1_PADDING },
458
+ entry.signature,
459
+ );
460
+ } catch (error) {
461
+ throw new LinuxRepoError('bad-signature', 'the index signature does not verify', { field: 'index' }, { cause: error });
462
+ }
463
+ if (!verified) {
464
+ throw new LinuxRepoError('bad-signature', 'the index signature does not verify', { field: 'index' });
465
+ }
466
+ return { keyName, hashAlgorithm: 'sha1' };
467
+ },
468
+ };
469
+ }