@push.rocks/smartlinuxrepo 1.0.0 → 1.1.1

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.
@@ -0,0 +1,263 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { LinuxRepoError } from '../shared/errors.js';
3
+ import { assertBytes, assertString, assertTimestamp } from '../shared/validation.js';
4
+ import { dearmor, decodeArmorBytes, readPacketTags, splitCleartextSignedMessage, } from './openpgpframing.js';
5
+ const HASH_ALGORITHMS = new Map([
6
+ [plugins.openpgp.enums.hash.sha256, 'sha256'],
7
+ [plugins.openpgp.enums.hash.sha384, 'sha384'],
8
+ [plugins.openpgp.enums.hash.sha512, 'sha512'],
9
+ ]);
10
+ /** Subpacket types openpgp.js reads in the unhashed area (issuer, embedded signature, issuer fingerprint). */
11
+ const UNHASHED_SUBPACKETS_READ = new Set([16, 32, 33]);
12
+ /**
13
+ * The openpgp.js settings verification depends on, pinned instead of taken
14
+ * from the process-wide `openpgp.config`. The values are openpgp.js 6.3.2's
15
+ * defaults except `ignoreUnsupportedPackets`.
16
+ */
17
+ const VERIFY_CONFIG = {
18
+ ...plugins.openpgp.config,
19
+ ignoreUnsupportedPackets: false,
20
+ ignoreMalformedPackets: false,
21
+ enableParsingV5Entities: false,
22
+ additionalAllowedPackets: [],
23
+ allowMissingKeyFlags: false,
24
+ allowInsecureVerificationWithReformattedKeys: false,
25
+ minRSABits: 2047,
26
+ knownNotations: [],
27
+ rejectHashAlgorithms: new Set([plugins.openpgp.enums.hash.md5, plugins.openpgp.enums.hash.ripemd]),
28
+ rejectMessageHashAlgorithms: new Set([
29
+ plugins.openpgp.enums.hash.md5,
30
+ plugins.openpgp.enums.hash.ripemd,
31
+ plugins.openpgp.enums.hash.sha1,
32
+ ]),
33
+ rejectPublicKeyAlgorithms: new Set([plugins.openpgp.enums.publicKey.elgamal, plugins.openpgp.enums.publicKey.dsa]),
34
+ rejectCurves: new Set([plugins.openpgp.enums.curve.secp256k1]),
35
+ };
36
+ /**
37
+ * Builds a verifier from one armored OpenPGP public key: exactly one
38
+ * transferable v4 public key (the primary key with its user IDs, subkeys and
39
+ * self-signatures, all v4) in exactly one `PUBLIC KEY BLOCK`. A private key
40
+ * block, several blocks or keys, a v6 key, text around the block, an armor
41
+ * CRC-24 that does not match or any other garbage is `invalid-key`
42
+ * (`detail.field` `publicKeyArmored`). Only the structure is checked here;
43
+ * whether a key can sign depends on time and is checked at verification.
44
+ */
45
+ export async function createOpenPgpVerifier(inputArg) {
46
+ if (inputArg === null || typeof inputArg !== 'object') {
47
+ throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
48
+ }
49
+ assertString(inputArg.publicKeyArmored, 'publicKeyArmored');
50
+ const keyBytes = dearmor(inputArg.publicKeyArmored, 'PUBLIC KEY BLOCK', { code: 'invalid-key', field: 'publicKeyArmored' });
51
+ let keys;
52
+ try {
53
+ keys = await plugins.openpgp.readKeys({ binaryKeys: keyBytes, config: VERIFY_CONFIG });
54
+ }
55
+ catch (error) {
56
+ throw new LinuxRepoError('invalid-key', 'the OpenPGP public key cannot be read', { field: 'publicKeyArmored' }, {
57
+ cause: error,
58
+ });
59
+ }
60
+ if (keys.length !== 1) {
61
+ throw new LinuxRepoError('invalid-key', `expected exactly one OpenPGP public key, got ${keys.length}`, {
62
+ field: 'publicKeyArmored',
63
+ });
64
+ }
65
+ const key = keys[0];
66
+ if (key.isPrivate()) {
67
+ throw new LinuxRepoError('invalid-key', 'a private key is not accepted as the verification key', {
68
+ field: 'publicKeyArmored',
69
+ });
70
+ }
71
+ if (key.keyPacket.version !== 4 || key.subkeys.some((subkey) => subkey.keyPacket.version !== 4)) {
72
+ throw new LinuxRepoError('invalid-key', 'only OpenPGP v4 keys and subkeys are accepted', { field: 'publicKeyArmored' });
73
+ }
74
+ const publicKey = key;
75
+ return {
76
+ fingerprint: publicKey.getFingerprint().toUpperCase(),
77
+ verifyDetachedSignature: async (input) => {
78
+ assertInputObject(input);
79
+ assertBytes(input.data, 'data');
80
+ assertBytes(input.signature, 'signature');
81
+ assertTimestamp(input.date, 'date');
82
+ return verifySignature({
83
+ key: publicKey,
84
+ packets: input.signature,
85
+ content: { binary: input.data },
86
+ signatureType: plugins.openpgp.enums.signature.binary,
87
+ date: input.date,
88
+ field: 'signature',
89
+ headerHashAlgorithms: [],
90
+ });
91
+ },
92
+ verifyArmoredDetachedSignature: async (input) => {
93
+ assertInputObject(input);
94
+ assertBytes(input.data, 'data');
95
+ assertBytes(input.signatureArmored, 'signatureArmored');
96
+ assertTimestamp(input.date, 'date');
97
+ const failure = { code: 'malformed-signature', field: 'signatureArmored' };
98
+ return verifySignature({
99
+ key: publicKey,
100
+ packets: dearmor(decodeArmorBytes(input.signatureArmored, failure), 'SIGNATURE', failure),
101
+ content: { binary: input.data },
102
+ signatureType: plugins.openpgp.enums.signature.binary,
103
+ date: input.date,
104
+ field: 'signatureArmored',
105
+ headerHashAlgorithms: [],
106
+ });
107
+ },
108
+ verifyCleartextMessage: async (input) => {
109
+ assertInputObject(input);
110
+ assertBytes(input.message, 'message');
111
+ assertTimestamp(input.date, 'date');
112
+ const parts = splitCleartextSignedMessage(input.message, { code: 'malformed-signature', field: 'message' });
113
+ const verification = await verifySignature({
114
+ key: publicKey,
115
+ packets: parts.signature,
116
+ // a text signature hashes the text with CRLF line endings; the split refused trailing whitespace,
117
+ // so this equals the cleartext digest of RFC 9580 section 7.1
118
+ content: { text: parts.signedText },
119
+ signatureType: plugins.openpgp.enums.signature.text,
120
+ date: input.date,
121
+ field: 'message',
122
+ headerHashAlgorithms: parts.headerHashAlgorithms,
123
+ });
124
+ return { ...verification, signedText: parts.signedText };
125
+ },
126
+ };
127
+ }
128
+ function hexByte(valueArg) {
129
+ return valueArg === null ? 'none' : `0x${valueArg.toString(16).padStart(2, '0')}`;
130
+ }
131
+ function assertInputObject(inputArg) {
132
+ if (inputArg === null || typeof inputArg !== 'object') {
133
+ throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
134
+ }
135
+ }
136
+ /**
137
+ * The verification steps, each with its own error code: structure
138
+ * (`malformed-signature`), issuer (`wrong-key`), the signature's own time
139
+ * rules (`bad-signature`), the issuing key's ability to sign at creation and
140
+ * at `date` (`unusable-key`), then the cryptographic check (`bad-signature`).
141
+ */
142
+ async function verifySignature(inputArg) {
143
+ const { key, date, field } = inputArg;
144
+ const malformed = (messageArg, causeArg) => new LinuxRepoError('malformed-signature', messageArg, { field }, causeArg === undefined ? undefined : { cause: causeArg });
145
+ // 1. structure, before any cryptography
146
+ const tags = readPacketTags(inputArg.packets, { code: 'malformed-signature', field });
147
+ if (tags.length !== 1 || tags[0] !== plugins.openpgp.enums.packet.signature) {
148
+ throw malformed(`expected exactly one signature packet, got packet tags [${tags.join(', ')}]`);
149
+ }
150
+ let signature;
151
+ try {
152
+ signature = await plugins.openpgp.readSignature({ binarySignature: inputArg.packets, config: VERIFY_CONFIG });
153
+ }
154
+ catch (error) {
155
+ throw malformed('the signature packet cannot be read', error);
156
+ }
157
+ const packet = signature.packets[0];
158
+ if (signature.packets.length !== 1 || !(packet instanceof plugins.openpgp.SignaturePacket)) {
159
+ throw malformed('expected exactly one signature packet');
160
+ }
161
+ if (packet.version !== 4) {
162
+ throw malformed(`only v4 signatures are accepted, got v${packet.version}`);
163
+ }
164
+ if (packet.signatureType !== inputArg.signatureType) {
165
+ throw malformed(`expected signature type ${hexByte(inputArg.signatureType)}, got ${hexByte(packet.signatureType)}`);
166
+ }
167
+ const hashId = packet.hashAlgorithm;
168
+ const hashAlgorithm = hashId === null ? undefined : HASH_ALGORITHMS.get(hashId);
169
+ if (hashId === null || hashAlgorithm === undefined) {
170
+ throw malformed(`hash algorithm ${hashId} is not SHA-256, SHA-384 or SHA-512`);
171
+ }
172
+ if (inputArg.headerHashAlgorithms.length > 0 && !inputArg.headerHashAlgorithms.includes(hashId)) {
173
+ throw malformed('the Hash: header does not name the signature hash algorithm');
174
+ }
175
+ if (packet.created === null) {
176
+ throw malformed('the signature has no creation time');
177
+ }
178
+ const issuerKeyId = packet.issuerKeyID.toHex();
179
+ if (!/^[0-9a-f]{16}$/.test(issuerKeyId) || /^0+$/.test(issuerKeyId)) {
180
+ throw malformed('the signature names no issuer, or the wildcard issuer');
181
+ }
182
+ if (packet.unknownSubpackets.some((subpacket) => subpacket.critical)) {
183
+ throw malformed('the signature has an unknown critical subpacket');
184
+ }
185
+ if (packet.unhashedSubpackets.some((subpacket) => subpacket.critical && !UNHASHED_SUBPACKETS_READ.has(subpacket.type))) {
186
+ throw malformed('the signature has a critical unhashed subpacket the verifier does not read');
187
+ }
188
+ if (packet.rawNotations.some((notation) => notation.critical)) {
189
+ throw malformed('the signature has a critical notation');
190
+ }
191
+ // 2. issuer: the primary key or one of its subkeys, by key ID and, when present, by fingerprint
192
+ const issuerFingerprint = packet.issuerFingerprint === null ? null : Buffer.from(packet.issuerFingerprint).toString('hex');
193
+ const issuers = [key, ...key.subkeys].filter((candidate) => candidate.getKeyID().equals(packet.issuerKeyID) && (issuerFingerprint === null || candidate.getFingerprint() === issuerFingerprint));
194
+ if (issuers.length === 0) {
195
+ throw new LinuxRepoError('wrong-key', 'the signature was not issued by this key or one of its subkeys', {
196
+ field,
197
+ value: (issuerFingerprint ?? issuerKeyId).toUpperCase(),
198
+ });
199
+ }
200
+ // 3. the signature's own time rules
201
+ if (packet.isExpired(date)) {
202
+ const reason = packet.created.getTime() > date.getTime() ? 'was created after the verification date' : 'has expired at the verification date';
203
+ throw new LinuxRepoError('bad-signature', `the signature ${reason}`, { field });
204
+ }
205
+ // 4. the issuing key or subkey must be able to sign at the creation time and at `date`
206
+ const signingKeyAt = async (whenArg) => {
207
+ let signingKey;
208
+ try {
209
+ signingKey = await key.getSigningKey(packet.issuerKeyID, whenArg, undefined, VERIFY_CONFIG);
210
+ }
211
+ catch (error) {
212
+ throw new LinuxRepoError('unusable-key', `key ${issuerKeyId.toUpperCase()} cannot sign at ${whenArg.toISOString()}`, { field: 'publicKeyArmored', value: issuers[0].getFingerprint().toUpperCase() }, { cause: error });
213
+ }
214
+ if (!issuers.includes(signingKey)) {
215
+ throw new LinuxRepoError('unusable-key', `key ${issuerKeyId.toUpperCase()} cannot sign at ${whenArg.toISOString()}`, {
216
+ field: 'publicKeyArmored',
217
+ value: issuers[0].getFingerprint().toUpperCase(),
218
+ });
219
+ }
220
+ return signingKey;
221
+ };
222
+ const atCreation = await signingKeyAt(packet.created);
223
+ const atDate = await signingKeyAt(date);
224
+ if (atCreation !== atDate) {
225
+ throw new LinuxRepoError('unusable-key', `key ${issuerKeyId.toUpperCase()} is not the same signing key at creation and at the verification date`, {
226
+ field: 'publicKeyArmored',
227
+ value: issuers[0].getFingerprint().toUpperCase(),
228
+ });
229
+ }
230
+ // 5. cryptography
231
+ try {
232
+ const content = inputArg.content;
233
+ const { signatures } = 'text' in content
234
+ ? await plugins.openpgp.verify({
235
+ message: await plugins.openpgp.createMessage({ text: content.text }),
236
+ signature,
237
+ verificationKeys: key,
238
+ date,
239
+ config: VERIFY_CONFIG,
240
+ })
241
+ : await plugins.openpgp.verify({
242
+ message: await plugins.openpgp.createMessage({ binary: content.binary }),
243
+ signature,
244
+ verificationKeys: key,
245
+ date,
246
+ format: 'binary',
247
+ config: VERIFY_CONFIG,
248
+ });
249
+ if (signatures.length !== 1) {
250
+ throw new Error(`openpgp.js evaluated ${signatures.length} signatures, expected 1`);
251
+ }
252
+ await signatures[0].verified;
253
+ }
254
+ catch (error) {
255
+ throw new LinuxRepoError('bad-signature', 'the signature does not verify', { field }, { cause: error });
256
+ }
257
+ return {
258
+ signingKeyFingerprint: atDate.getFingerprint().toUpperCase(),
259
+ created: new Date(packet.created.getTime()),
260
+ hashAlgorithm,
261
+ };
262
+ }
263
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyaWZpZXJzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvc2lnbmluZy92ZXJpZmllcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUM7QUFDekMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQ3JELE9BQU8sRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFFLGVBQWUsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3JGLE9BQU8sRUFDTCxPQUFPLEVBQ1AsZ0JBQWdCLEVBQ2hCLGNBQWMsRUFDZCwyQkFBMkIsR0FFNUIsTUFBTSxxQkFBcUIsQ0FBQztBQWdFN0IsTUFBTSxlQUFlLEdBQStDLElBQUksR0FBRyxDQUFDO0lBQzFFLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxRQUFRLENBQUM7SUFDN0MsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLFFBQVEsQ0FBQztJQUM3QyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsUUFBUSxDQUFDO0NBQzlDLENBQUMsQ0FBQztBQUVILDhHQUE4RztBQUM5RyxNQUFNLHdCQUF3QixHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBRXZEOzs7O0dBSUc7QUFDSCxNQUFNLGFBQWEsR0FBMkI7SUFDNUMsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLE1BQU07SUFDekIsd0JBQXdCLEVBQUUsS0FBSztJQUMvQixzQkFBc0IsRUFBRSxLQUFLO0lBQzdCLHVCQUF1QixFQUFFLEtBQUs7SUFDOUIsd0JBQXdCLEVBQUUsRUFBRTtJQUM1QixvQkFBb0IsRUFBRSxLQUFLO0lBQzNCLDRDQUE0QyxF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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartlinuxrepo",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "private": false,
5
5
  "description": "A pure codec for Linux package repositories (Alpine apk, Debian apt, RPM dnf, Arch pacman): read packages into records, build index file sets and sign them.",
6
6
  "main": "dist_ts/index.js",
package/readme.md CHANGED
@@ -16,7 +16,7 @@ Node.js `^24.20.0 || >=26.6.0`: the releases whose `node:zlib` reports a truncat
16
16
 
17
17
  ## Status
18
18
 
19
- All four formats read, index and sign: `readAlpinePackage`, `readDebianPackage`, `readRpmPackage` and `readArchPackage` read packages into records; `buildAlpineIndex`, `buildDebianIndex`, `buildRpmIndex` and `buildArchIndex` build signed index file sets; `signRpmPackage` and `signArchPackage` sign packages at upload. Each format is checked against Gitea 28.2.0's own output and installed from with apk, apt, dnf and pacman. The built-in OpenPGP signer does not stream: openpgp.js 6.3.2 holds a detached-signed document in memory (see Signing).
19
+ All four formats read, index and sign: `readAlpinePackage`, `readDebianPackage`, `readRpmPackage` and `readArchPackage` read packages into records; `buildAlpineIndex`, `buildDebianIndex`, `buildRpmIndex` and `buildArchIndex` build signed index file sets; `signRpmPackage` and `signArchPackage` sign packages at upload. Each format is checked against Gitea 28.2.0's own output and installed from with apk, apt, dnf and pacman. `createOpenPgpVerifier` verifies OpenPGP detached and cleartext signatures (see Verification). The built-in OpenPGP signer does not stream: openpgp.js 6.3.2 holds a detached-signed document in memory (see Signing).
20
20
 
21
21
  ## Records
22
22
 
@@ -36,7 +36,7 @@ Readers take `Partial<ILinuxPackageLimits>` per call over `DEFAULT_LINUX_PACKAGE
36
36
  | `maxFileListBytes` | 16 MiB | total Arch file-name bytes |
37
37
  | `maxHeaderBytes` | 4 MiB | one tar header chain or metadata line |
38
38
 
39
- Every failure is a `LinuxRepoError` with a stable `code` (`limit-exceeded`, `unsupported-compression`, `malformed-archive`, `malformed-header`, `missing-field`, `invalid-field`, `unsupported-package-format`, `truncated`, `digest-mismatch`, `invalid-input`, `invalid-key`, `signing-failed`) and a `detail` naming the field or limit. Errors from your own source or signer, and failures inside node:crypto or openpgp.js during key generation, are passed through unchanged.
39
+ Every failure is a `LinuxRepoError` with a stable `code` (`limit-exceeded`, `unsupported-compression`, `malformed-archive`, `malformed-header`, `missing-field`, `invalid-field`, `unsupported-package-format`, `truncated`, `digest-mismatch`, `invalid-input`, `invalid-key`, `signing-failed`, and from the verifier `malformed-signature`, `wrong-key`, `unusable-key`, `bad-signature`) and a `detail` naming the field or limit. Errors from your own source or signer, and failures inside node:crypto or openpgp.js during key generation, are passed through unchanged. New codes arrive in minor releases (1.1.0 added the four verifier codes), each raised only by a new function: a `switch` over `code` needs a default branch.
40
40
 
41
41
  ## Signing
42
42
 
@@ -65,6 +65,37 @@ const pgpSigner = await createOpenPgpSigner({ privateKeyArmored: pgpKeys.private
65
65
 
66
66
  `isIndexableArchitecture(architecture)` tells whether an architecture can name an index path; builders refuse entries that fail it.
67
67
 
68
+ ## Verification
69
+
70
+ ```ts
71
+ import { createOpenPgpVerifier } from '@push.rocks/smartlinuxrepo';
72
+
73
+ const verifier = await createOpenPgpVerifier({ publicKeyArmored }); // invalid-key unless one v4 public key
74
+ const now = new Date();
75
+ const signature = await pgpSigner.signBinary((async function* () { yield data; })(), now); // a raw packet, as an Arch .sig
76
+ await verifier.verifyDetachedSignature({ data, signature, date: now });
77
+ await verifier.verifyArmoredDetachedSignature({ data: release, signatureArmored: releaseGpg, date: now }); // Release.gpg, repomd.xml.asc
78
+ const { signedText } = await verifier.verifyCleartextMessage({ message: inRelease, date: now }); // InRelease
79
+ ```
80
+
81
+ `createOpenPgpVerifier` reads exactly one transferable OpenPGP v4 public key (primary key, user IDs, subkeys, self-signatures) in exactly one `PUBLIC KEY BLOCK`. A private key, several blocks or keys, a v6 key, text around the block or an armor CRC-24 that does not match is `invalid-key`. Armor headers (`Comment:`, `Version:`) are ignored; a malformed header line is refused. Whether the key can sign is checked at verification, where the time is known.
82
+
83
+ The three methods take the served bytes as `Uint8Array` and a required `date`; the codec reads no clock. `verifyDetachedSignature` takes a raw signature packet (what `signBinary` returns, an Arch `.sig`), `verifyArmoredDetachedSignature` an ASCII-armored one, and nothing is sniffed. A signature is valid when it is exactly one v4 signature packet (type 0x00 detached, 0x01 cleartext) hashed with SHA-256, SHA-384 or SHA-512, issued by the primary key or one of its subkeys, created at or before `date` and not expired at `date`, made by a key that can sign both at the signature's creation time and at `date` (not expired, not revoked, sign flag, valid binding, openpgp.js's algorithm floor), and when it verifies. A result names the key or subkey that verified (`signingKeyFingerprint`, 40 uppercase hex), the creation time and the hash algorithm. Every failure is a `LinuxRepoError`, never a boolean:
84
+
85
+ | Code | Meaning |
86
+ | --- | --- |
87
+ | `malformed-signature` | armor, encoding or framing, not exactly one v4 signature packet, the wrong signature type, a hash outside SHA-256/384/512, a `Hash:` header without the signature's hash, no creation time, no or a wildcard issuer, an unknown critical subpacket or notation |
88
+ | `wrong-key` | issued by neither the primary key nor a subkey, or the issuer fingerprint disagrees (`detail.value`: the issuer in hex) |
89
+ | `unusable-key` | the issuing key or subkey cannot sign at the creation time or at `date`: expired, revoked, no sign flag, created after the signature, below the algorithm floor (`detail.value`: the fingerprint of the key or subkey that cannot sign) |
90
+ | `bad-signature` | the cryptographic check failed, or the signature was created after `date` or has expired at `date` |
91
+ | `invalid-input` | an argument that is not a `Uint8Array`, or an invalid `date` |
92
+
93
+ A cleartext message is strict UTF-8 with LF line endings, as apt splits it: the `-----BEGIN PGP SIGNED MESSAGE-----` line first, optional `Hash:` headers and no others, the dash-escaped text, one signature block, then at most one line break. Text before or after, a CR, a line ending in a space or tab, or a line starting with `-` but not `- ` is `malformed-signature`. `signedText` is the text as signed: dash-escaping removed, without the line break before the signature block. For the codec's and GnuPG's `InRelease` that is `Release` without its final line break; Gitea 28.2.0's `InRelease` (go-crypto) signs that line break too, so its `signedText` is the whole `Release`.
94
+
95
+ Two refusals depart from RFC 9580: an armor CRC-24 that does not match (§6.1; gpgv refuses it, sqv accepts it) and a `Hash:` header that does not name the signature's hash (§6.2.2.3).
96
+
97
+ The verdicts match gpgv as apt reads it (`EXPKEYSIG` and `REVKEYSIG` are refusals). sqv, apt 3's verifier, checks the key only at the signature's creation time: it accepts a signature whose subkey expired, or whose key was soft-revoked, after signing, which the codec refuses at a later `date`.
98
+
68
99
  Every builder returns the complete file set for its input as `ILinuxIndexFile[]` (`path`, `bytes`, `sha256`, `mediaType`, `stage`, `aliases`); a previously written file that is not in the set is stale. Write every `content` file before any `pointer` file: the pointer files are the signed documents that name the content files.
69
100
 
70
101
  ## Alpine
@@ -107,9 +138,9 @@ const files = await buildDebianIndex({
107
138
  });
108
139
  ```
109
140
 
110
- `readDebianPackage` reads the `control.tar` member (plain, `.gz`, `.xz` or `.zst`) and hashes the rest of the file without decompressing it. The record keeps Gitea's derived values exactly, and its `fileName` is `<name>_<version>_<architecture>.deb`.
141
+ `readDebianPackage` reads the `control.tar` member (plain, `.gz`, `.xz` or `.zst`) and hashes the rest of the file without decompressing it. The record keeps Gitea's derived values exactly, and its `fileName` is `<name>_<version>_<architecture>.deb`. Two departures from Gitea's control checks keep every record indexable. The version: a version that holds `/` or `?` is refused with `invalid-field` (`detail.field` `Version`), a departure from Gitea's version pattern, which admits both in the revision (its `+-~` class is the range 0x2B-0x7E); no pool file name can hold them, because `/` splits the path and apt sends `?` unescaped, so the server would read the rest as a query. The architecture: Gitea accepts any non-empty Architecture; the reader refuses one that is `.` or `..` or holds `/`, `#`, `?`, whitespace or a control character (`invalid-field`, `detail.field` `Architecture`), because the file name embeds it. `buildDebianIndex` applies the same rules to `record.architecture` and `record.fileName` (`invalid-input`), so every record the reader returns can be indexed.
111
142
 
112
- `buildDebianIndex` returns the complete set for one distribution. For every component and architecture with packages it writes `dists/<distribution>/<component>/binary-<architecture>/Packages`, `Packages.gz` and `Packages.xz` (`content` files). Packages of architecture `all` are listed only under `binary-all`, as in Gitea. Then it writes `dists/<distribution>/Release`, its armored detached signature `Release.gpg` and the cleartext-signed `InRelease` (`pointer` files). `distribution` and `component` must match `^[a-zA-Z0-9][a-zA-Z0-9.~+_-]*$`; `origin` and `label` are each one non-empty line without surrounding whitespace. Your route serves:
143
+ `buildDebianIndex` returns the complete set for one distribution. For every component and architecture with packages it writes `dists/<distribution>/<component>/binary-<architecture>/Packages`, `Packages.gz` and `Packages.xz` (`content` files). Packages of architecture `all` are listed only under `binary-all`, as in Gitea. Then it writes `dists/<distribution>/Release`, its armored detached signature `Release.gpg` and the cleartext-signed `InRelease` (`pointer` files). `distribution` and `component` must match `^[a-zA-Z0-9][a-zA-Z0-9.~+_-]*$` (`isDebianSymbol`); `origin` and `label` are each one non-empty line without surrounding whitespace (`isDebianReleaseValue`). Both predicates are the builder's own rules, for checking configuration before a build. Entries are `IDebianIndexEntry` objects (`{ component, record }`). Your route serves:
113
144
 
114
145
  - every file at its `path` and at each of its `aliases`, `by-hash/MD5Sum`, `SHA1`, `SHA256` and `SHA512/<hex>` next to the `Packages` file. `Release` sets `Acquire-By-Hash: yes`, and apt 2.6 and 3.x fetch `Packages.xz` through `by-hash/SHA512/<hex>`;
115
146
  - each package file at `pool/<distribution>/<component>/<fileName>`, the `Filename:` its stanza names;
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartlinuxrepo',
6
- version: '1.0.0',
6
+ version: '1.1.1',
7
7
  description: 'A pure codec for Linux package repositories (Alpine apk, Debian apt, RPM dnf, Arch pacman): read packages into records, build index file sets and sign them.'
8
8
  }
@@ -1,7 +1,9 @@
1
+ import { isIndexableArchitecture } from '../shared/architecture.js';
1
2
  import { LinuxRepoError, limitExceeded } from '../shared/errors.js';
2
3
  import { isValidUrl } from '../shared/url.js';
3
4
  import type { IDebianPackageMetadata } from '../shared/types.js';
4
5
  import { fromByteString, goTrimSpace, toByteString } from './gostring.js';
6
+ import { hasPoolFileNameRefusedCharacter } from './rules.js';
5
7
  import { parseMailAddress } from './mailaddress.js';
6
8
 
7
9
  /**
@@ -11,8 +13,6 @@ import { parseMailAddress } from './mailaddress.js';
11
13
  */
12
14
  const NAME_PATTERN = /^[a-z0-9][a-z0-9+-.]+$/;
13
15
  const VERSION_PATTERN = /^(?:(0|[1-9][0-9]*):)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?$/;
14
- /** Gitea's distribution and component rule (`metadata.go:67`, applied at upload). */
15
- export const DEBIAN_SYMBOL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.~+_-]*$/;
16
16
 
17
17
  export interface IDebianControl {
18
18
  name: string;
@@ -31,7 +31,13 @@ export interface IDebianControl {
31
31
  * verbatim, and the Depends text split on `,` (`['']` when there is none).
32
32
  *
33
33
  * Gitea's scanner refuses lines over 64 KiB; the codec accepts lines up to
34
- * `maxLineBytes` (`maxHeaderBytes`), which is at least that.
34
+ * `maxLineBytes` (`maxHeaderBytes`), which is at least that. A version that
35
+ * matches Gitea's pattern but holds `/` or `?` (both inside the revision's
36
+ * range) is refused with `invalid-field` (`Version`): no pool file name can
37
+ * hold it (`hasPoolFileNameRefusedCharacter`), where Gitea accepts the upload
38
+ * and then cannot serve it. An Architecture that fails `isIndexableArchitecture`
39
+ * or `hasPoolFileNameRefusedCharacter` (decoded) is refused with
40
+ * `invalid-field` (`Architecture`), where Gitea accepts any non-empty value.
35
41
  */
36
42
  export function parseDebianControlFile(bytesArg: Uint8Array, maxLineBytesArg: number): IDebianControl {
37
43
  const text = toByteString(bytesArg);
@@ -123,7 +129,10 @@ export function parseDebianControlFile(bytesArg: Uint8Array, maxLineBytesArg: nu
123
129
  if (version === '') {
124
130
  throw new LinuxRepoError('missing-field', 'the control file has no Version field', { field: 'Version' });
125
131
  }
126
- if (!VERSION_PATTERN.test(version)) {
132
+ // a departure from Gitea's version pattern: the revision range admits
133
+ // `/` and `?`, which the pool file name cannot hold, so the version is
134
+ // refused here rather than by buildDebianIndex
135
+ if (!VERSION_PATTERN.test(version) || hasPoolFileNameRefusedCharacter(version)) {
127
136
  throw new LinuxRepoError('invalid-field', 'the package version is invalid', {
128
137
  field: 'Version',
129
138
  value: fromByteString(version),
@@ -134,11 +143,22 @@ export function parseDebianControlFile(bytesArg: Uint8Array, maxLineBytesArg: nu
134
143
  field: 'Architecture',
135
144
  });
136
145
  }
146
+ // a departure from Gitea, which accepts any non-empty Architecture: the
147
+ // file name embeds it, so it must pass the builder's rules; checked on the
148
+ // decoded text the record carries (on the byte string, `\s` and `\p{Cc}`
149
+ // would match the UTF-8 continuation bytes 0x85 and 0xA0)
150
+ const decodedArchitecture = fromByteString(architecture);
151
+ if (!isIndexableArchitecture(decodedArchitecture) || hasPoolFileNameRefusedCharacter(decodedArchitecture)) {
152
+ throw new LinuxRepoError('invalid-field', 'the package architecture cannot name an index path or pool file', {
153
+ field: 'Architecture',
154
+ value: decodedArchitecture,
155
+ });
156
+ }
137
157
 
138
158
  return {
139
159
  name,
140
160
  version,
141
- architecture: fromByteString(architecture),
161
+ architecture: decodedArchitecture,
142
162
  metadata: {
143
163
  control: fromByteString(goTrimSpace(control)),
144
164
  maintainer: fromByteString(maintainer),
@@ -2,11 +2,11 @@ import * as plugins from '../plugins.js';
2
2
  import { isIndexableArchitecture } from '../shared/architecture.js';
3
3
  import { LinuxRepoError } from '../shared/errors.js';
4
4
  import type { IDebianPackageRecord, ILinuxIndexFile } from '../shared/types.js';
5
- import { assertSegmentName, assertString, assertTimestamp, hasControlCharacter } from '../shared/validation.js';
5
+ import { assertString, assertTimestamp } from '../shared/validation.js';
6
6
  import type { IOpenPgpSigner } from '../signing/signers.js';
7
7
  import { createCleartextSignedMessage, createDetachedArmoredSignature } from '../signing/openpgpframing.js';
8
- import { DEBIAN_SYMBOL_PATTERN } from './control.js';
9
8
  import { compareUtf8, goTrimSpace, utf8ByteString } from './gostring.js';
9
+ import { DEBIAN_SYMBOL_PATTERN, hasPoolFileNameRefusedCharacter, isDebianReleaseValue, isDebianSymbol } from './rules.js';
10
10
 
11
11
  /** The fields the `Packages` stanza writes itself; a control file's own copies are dropped. */
12
12
  const INDEX_OWNED_FIELDS = new Set(['filename', 'size', 'md5sum', 'sha1', 'sha256', 'sha512']);
@@ -29,6 +29,7 @@ const HASH_PATTERNS = {
29
29
  sha512: /^[0-9a-f]{128}$/,
30
30
  };
31
31
 
32
+ /** One package of a distribution's index: the record and the component it is listed under. */
32
33
  export interface IDebianIndexEntry {
33
34
  component: string;
34
35
  record: IDebianPackageRecord;
@@ -52,7 +53,7 @@ export interface IDebianIndexEntry {
52
53
  */
53
54
  export async function buildDebianIndex(input: {
54
55
  distribution: string;
55
- entries: Array<{ component: string; record: IDebianPackageRecord }>;
56
+ entries: IDebianIndexEntry[];
56
57
  origin: string;
57
58
  label: string;
58
59
  signer: IOpenPgpSigner;
@@ -233,7 +234,8 @@ export function renderDebianPackagesStanza(input: {
233
234
 
234
235
  /**
235
236
  * Refuses a record the index cannot list: wrong format, an architecture that
236
- * is not a path segment, a file name that is not one, digests that are not
237
+ * is not a path segment, a file name apt cannot fetch as one pool path
238
+ * segment (`hasPoolFileNameRefusedCharacter`), digests that are not
237
239
  * hex of the right length, or a control text that is not one stanza (empty,
238
240
  * or holding a blank line that would start another stanza).
239
241
  */
@@ -248,12 +250,13 @@ function assertRecord(recordArg: IDebianPackageRecord): void {
248
250
  { field: 'record.architecture', value: String(recordArg.fileName) },
249
251
  );
250
252
  }
251
- assertSegmentName(recordArg.fileName, 'record.fileName');
252
- if (/\s/.test(recordArg.fileName)) {
253
- throw new LinuxRepoError('invalid-input', 'record.fileName must not contain whitespace', {
254
- field: 'record.fileName',
255
- value: recordArg.fileName,
256
- });
253
+ assertString(recordArg.fileName, 'record.fileName');
254
+ if (recordArg.fileName === '' || hasPoolFileNameRefusedCharacter(recordArg.fileName)) {
255
+ throw new LinuxRepoError(
256
+ 'invalid-input',
257
+ 'record.fileName must be non-empty and contain no "/", "#", "?", whitespace or control characters',
258
+ { field: 'record.fileName', value: recordArg.fileName },
259
+ );
257
260
  }
258
261
  const digests = recordArg.digests;
259
262
  if (digests === null || typeof digests !== 'object' || !Number.isSafeInteger(digests.size) || digests.size < 0) {
@@ -282,7 +285,7 @@ function assertRecord(recordArg: IDebianPackageRecord): void {
282
285
 
283
286
  function assertSymbol(valueArg: unknown, fieldArg: string): asserts valueArg is string {
284
287
  assertString(valueArg, fieldArg);
285
- if (!DEBIAN_SYMBOL_PATTERN.test(valueArg)) {
288
+ if (!isDebianSymbol(valueArg)) {
286
289
  throw new LinuxRepoError('invalid-input', `${fieldArg} must match ${DEBIAN_SYMBOL_PATTERN.source}`, {
287
290
  field: fieldArg,
288
291
  value: valueArg,
@@ -293,7 +296,7 @@ function assertSymbol(valueArg: unknown, fieldArg: string): asserts valueArg is
293
296
  /** A Release header value: one line, non-empty, no leading or trailing whitespace (canonical cleartext). */
294
297
  function assertReleaseValue(valueArg: unknown, fieldArg: string): asserts valueArg is string {
295
298
  assertString(valueArg, fieldArg);
296
- if (valueArg === '' || hasControlCharacter(valueArg) || valueArg !== valueArg.trim()) {
299
+ if (!isDebianReleaseValue(valueArg)) {
297
300
  throw new LinuxRepoError(
298
301
  'invalid-input',
299
302
  `${fieldArg} must be non-empty, without control characters or surrounding whitespace`,
@@ -0,0 +1,44 @@
1
+ import { hasControlCharacter } from '../shared/validation.js';
2
+
3
+ /** Gitea's distribution and component rule (`modules/packages/debian/metadata.go:67`, applied at upload). */
4
+ export const DEBIAN_SYMBOL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.~+_-]*$/;
5
+
6
+ /**
7
+ * Whether a value is a distribution or component name `buildDebianIndex`
8
+ * accepts: Gitea's rule, `^[a-zA-Z0-9][a-zA-Z0-9.~+_-]*$`. A non-string is
9
+ * not one.
10
+ */
11
+ export function isDebianSymbol(valueArg: string): boolean {
12
+ return typeof valueArg === 'string' && DEBIAN_SYMBOL_PATTERN.test(valueArg);
13
+ }
14
+
15
+ /**
16
+ * Whether a value can be a Release header value (`Origin`, `Label`) that
17
+ * `buildDebianIndex` accepts: non-empty, no control characters (so one line)
18
+ * and no leading or trailing whitespace (canonical cleartext). A non-string
19
+ * is not one.
20
+ */
21
+ export function isDebianReleaseValue(valueArg: string): boolean {
22
+ return typeof valueArg === 'string' && valueArg !== '' && !hasControlCharacter(valueArg) && valueArg === valueArg.trim();
23
+ }
24
+
25
+ /**
26
+ * The characters a pool file name (`<name>_<version>_<architecture>.deb`,
27
+ * written as a `Packages` `Filename`) must not hold: `/`, which would make it
28
+ * more than one path segment; whitespace and control characters, which split
29
+ * or end the field; and `#` and `?`, which apt sends unescaped in the request
30
+ * path (apt 2.6.1 `pkgAcquire::URIEncode` escapes only `%`, `+`, `~`, space,
31
+ * control and non-ASCII bytes), so a server reads the rest as a fragment or a
32
+ * query and apt cannot fetch the file.
33
+ */
34
+ const POOL_FILE_NAME_REFUSED_PATTERN = /[/#?\s\p{Cc}]/u;
35
+
36
+ /**
37
+ * Whether a text holds a character a pool file name must not hold. The one
38
+ * rule both `buildDebianIndex` (on `record.fileName`) and `readDebianPackage`
39
+ * (on the version and the decoded architecture) apply, so the reader never
40
+ * returns a record the builder refuses for its file name.
41
+ */
42
+ export function hasPoolFileNameRefusedCharacter(textArg: string): boolean {
43
+ return POOL_FILE_NAME_REFUSED_PATTERN.test(textArg);
44
+ }
package/ts/index.ts CHANGED
@@ -29,10 +29,19 @@ export type { TLinuxRepoErrorCode, ILinuxRepoErrorDetail } from './shared/errors
29
29
  export { isIndexableArchitecture } from './shared/architecture.js';
30
30
  export { createAlpineSigner, createOpenPgpSigner } from './signing/signers.js';
31
31
  export type { IAlpineSigner, IOpenPgpSigner } from './signing/signers.js';
32
+ export { createOpenPgpVerifier } from './signing/verifiers.js';
33
+ export type {
34
+ IOpenPgpCleartextVerification,
35
+ IOpenPgpSignatureVerification,
36
+ IOpenPgpVerifier,
37
+ TOpenPgpHashAlgorithm,
38
+ } from './signing/verifiers.js';
32
39
  export { generateAlpineKeyPair, generateOpenPgpKeyPair, giteaAlpineKeyName } from './signing/keys.js';
33
40
  export type { IAlpineKeyPair, IOpenPgpKeyPair } from './signing/keys.js';
34
41
  export { readDebianPackage } from './debian/reader.js';
35
42
  export { buildDebianIndex } from './debian/indexbuilder.js';
43
+ export type { IDebianIndexEntry } from './debian/indexbuilder.js';
44
+ export { isDebianReleaseValue, isDebianSymbol } from './debian/rules.js';
36
45
  export { readRpmPackage } from './rpm/reader.js';
37
46
  export { signRpmPackage } from './rpm/sign.js';
38
47
  export { buildRpmIndex } from './rpm/builder.js';
@@ -81,6 +81,15 @@ function decodeGoUtf8Slow(bytesArg: Uint8Array): string {
81
81
  return parts.join('');
82
82
  }
83
83
 
84
+ /** Decodes strict UTF-8, keeping a leading BOM; `null` when any sequence is invalid. */
85
+ export function decodeStrictUtf8(bytesArg: Uint8Array): string | null {
86
+ try {
87
+ return strictUtf8.decode(bytesArg);
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
84
93
  /** ASCII/Latin-1 view of header bytes (magic values, numeric fields). */
85
94
  export function decodeLatin1(bytesArg: Uint8Array): string {
86
95
  let text = '';
@@ -2,7 +2,11 @@
2
2
  * Whether an architecture can name an index path segment. Readers record any
3
3
  * architecture a package states (Gitea accepts them); builders refuse an entry
4
4
  * whose architecture fails this rule with `invalid-input`, and never skip it
5
- * silently. Refused: the empty string, `.` and `..`, and any value containing
5
+ * silently. The exception is `readDebianPackage`: the record's file name
6
+ * (`<name>_<version>_<architecture>.deb`) embeds the architecture, so it
7
+ * refuses one that fails this rule or the pool file name rule with
8
+ * `invalid-field` (`Architecture`), and never returns a record
9
+ * `buildDebianIndex` refuses. Refused: the empty string, `.` and `..`, and any value containing
6
10
  * `/`, whitespace or a control character.
7
11
  */
8
12
  export function isIndexableArchitecture(architectureArg: string): boolean {