@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/alpine/apkindexreader.d.ts +87 -0
- package/dist_ts/alpine/apkindexreader.js +193 -0
- package/dist_ts/arch/indexreader.d.ts +35 -0
- package/dist_ts/arch/indexreader.js +45 -0
- package/dist_ts/arch/reader.js +1 -1
- package/dist_ts/debian/indexbuilder.js +8 -23
- package/dist_ts/debian/indexreader.d.ts +107 -0
- package/dist_ts/debian/indexreader.js +274 -0
- package/dist_ts/debian/reader.js +1 -1
- package/dist_ts/debian/rules.d.ts +18 -0
- package/dist_ts/debian/rules.js +81 -2
- package/dist_ts/index.d.ts +14 -4
- package/dist_ts/index.js +9 -3
- package/dist_ts/readers/decompress.d.ts +1 -2
- package/dist_ts/readers/decompress.js +1 -1
- package/dist_ts/readers/document.d.ts +36 -0
- package/dist_ts/readers/document.js +110 -0
- package/dist_ts/rpm/repodata.d.ts +3 -2
- package/dist_ts/rpm/repodata.js +5 -4
- package/dist_ts/rpm/repodatareader.d.ts +173 -0
- package/dist_ts/rpm/repodatareader.js +414 -0
- package/dist_ts/rpm/xmlreader.d.ts +70 -0
- package/dist_ts/rpm/xmlreader.js +576 -0
- package/dist_ts/shared/errors.d.ts +12 -4
- package/dist_ts/shared/errors.js +1 -1
- package/dist_ts/shared/limits.d.ts +8 -1
- package/dist_ts/shared/limits.js +5 -2
- package/dist_ts/shared/types.d.ts +2 -0
- package/dist_ts/signing/verifiers.d.ts +43 -0
- package/dist_ts/signing/verifiers.js +81 -2
- package/package.json +1 -1
- package/readme.md +53 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/alpine/apkindexreader.ts +265 -0
- package/ts/arch/indexreader.ts +66 -0
- package/ts/arch/reader.ts +2 -1
- package/ts/debian/indexbuilder.ts +13 -23
- package/ts/debian/indexreader.ts +372 -0
- package/ts/debian/reader.ts +2 -1
- package/ts/debian/rules.ts +83 -1
- package/ts/index.ts +40 -2
- package/ts/readers/decompress.ts +1 -3
- package/ts/readers/document.ts +136 -0
- package/ts/rpm/repodata.ts +4 -3
- package/ts/rpm/repodatareader.ts +582 -0
- package/ts/rpm/xmlreader.ts +680 -0
- package/ts/shared/errors.ts +13 -4
- package/ts/shared/limits.ts +9 -1
- package/ts/shared/types.ts +3 -0
- package/ts/signing/verifiers.ts +113 -1
package/ts/shared/errors.ts
CHANGED
|
@@ -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 {
|
package/ts/shared/limits.ts
CHANGED
|
@@ -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>;
|
package/ts/shared/types.ts
CHANGED
|
@@ -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';
|
package/ts/signing/verifiers.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|