@push.rocks/smartlinuxrepo 0.1.0 → 1.1.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/debian/control.d.ts +0 -2
- package/dist_ts/debian/control.js +1 -3
- package/dist_ts/debian/indexbuilder.d.ts +2 -4
- package/dist_ts/debian/indexbuilder.js +5 -5
- package/dist_ts/debian/rules.d.ts +15 -0
- package/dist_ts/debian/rules.js +21 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/readers/text.d.ts +2 -0
- package/dist_ts/readers/text.js +10 -1
- package/dist_ts/shared/errors.d.ts +14 -1
- package/dist_ts/shared/errors.js +1 -1
- package/dist_ts/signing/openpgpframing.d.ts +56 -1
- package/dist_ts/signing/openpgpframing.js +273 -2
- package/dist_ts/signing/verifiers.d.ts +77 -0
- package/dist_ts/signing/verifiers.js +263 -0
- package/package.json +1 -1
- package/readme.md +34 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/debian/control.ts +0 -2
- package/ts/debian/indexbuilder.ts +6 -5
- package/ts/debian/rules.ts +23 -0
- package/ts/index.ts +9 -0
- package/ts/readers/text.ts +9 -0
- package/ts/shared/errors.ts +18 -1
- package/ts/signing/openpgpframing.ts +295 -2
- package/ts/signing/verifiers.ts +357 -0
|
@@ -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": "
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
|
@@ -109,7 +140,7 @@ const files = await buildDebianIndex({
|
|
|
109
140
|
|
|
110
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`.
|
|
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.~+_-]
|
|
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;
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartlinuxrepo',
|
|
6
|
-
version: '
|
|
6
|
+
version: '1.1.0',
|
|
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
|
}
|
package/ts/debian/control.ts
CHANGED
|
@@ -11,8 +11,6 @@ import { parseMailAddress } from './mailaddress.js';
|
|
|
11
11
|
*/
|
|
12
12
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9+-.]+$/;
|
|
13
13
|
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
14
|
|
|
17
15
|
export interface IDebianControl {
|
|
18
16
|
name: string;
|
|
@@ -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
|
|
5
|
+
import { assertSegmentName, 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, 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:
|
|
56
|
+
entries: IDebianIndexEntry[];
|
|
56
57
|
origin: string;
|
|
57
58
|
label: string;
|
|
58
59
|
signer: IOpenPgpSigner;
|
|
@@ -282,7 +283,7 @@ function assertRecord(recordArg: IDebianPackageRecord): void {
|
|
|
282
283
|
|
|
283
284
|
function assertSymbol(valueArg: unknown, fieldArg: string): asserts valueArg is string {
|
|
284
285
|
assertString(valueArg, fieldArg);
|
|
285
|
-
if (!
|
|
286
|
+
if (!isDebianSymbol(valueArg)) {
|
|
286
287
|
throw new LinuxRepoError('invalid-input', `${fieldArg} must match ${DEBIAN_SYMBOL_PATTERN.source}`, {
|
|
287
288
|
field: fieldArg,
|
|
288
289
|
value: valueArg,
|
|
@@ -293,7 +294,7 @@ function assertSymbol(valueArg: unknown, fieldArg: string): asserts valueArg is
|
|
|
293
294
|
/** A Release header value: one line, non-empty, no leading or trailing whitespace (canonical cleartext). */
|
|
294
295
|
function assertReleaseValue(valueArg: unknown, fieldArg: string): asserts valueArg is string {
|
|
295
296
|
assertString(valueArg, fieldArg);
|
|
296
|
-
if (
|
|
297
|
+
if (!isDebianReleaseValue(valueArg)) {
|
|
297
298
|
throw new LinuxRepoError(
|
|
298
299
|
'invalid-input',
|
|
299
300
|
`${fieldArg} must be non-empty, without control characters or surrounding whitespace`,
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
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';
|
package/ts/readers/text.ts
CHANGED
|
@@ -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 = '';
|
package/ts/shared/errors.ts
CHANGED
|
@@ -14,6 +14,19 @@ import type { ILinuxPackageLimits } from './limits.js';
|
|
|
14
14
|
* - `invalid-input`: a bad argument from the caller.
|
|
15
15
|
* - `invalid-key`: a key the codec cannot use.
|
|
16
16
|
* - `signing-failed`: a codec signer failed, or a signer returned something that is not signature bytes.
|
|
17
|
+
* - `malformed-signature`: an OpenPGP signature or cleartext-signed message the verifier refuses before
|
|
18
|
+
* any cryptography: armor, encoding, framing, packet count, version, signature type, a hash outside
|
|
19
|
+
* 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.
|
|
21
|
+
* - `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).
|
|
23
|
+
* - `unusable-key`: the issuing key or subkey cannot sign at the signature's creation time or at the
|
|
24
|
+
* verification date: expired, revoked, no sign flag, a bad binding or back-signature, created after
|
|
25
|
+
* the signature, or below the algorithm floor.
|
|
26
|
+
* - `bad-signature`: the cryptographic check failed, the signature was created after the verification
|
|
27
|
+
* date, or it has expired at that date.
|
|
28
|
+
*
|
|
29
|
+
* New codes arrive in minor releases: a consumer that switches over `code` needs a default branch.
|
|
17
30
|
*
|
|
18
31
|
* The codec never wraps an error it did not raise: an error from the caller's
|
|
19
32
|
* source or from a caller-supplied signer propagates unchanged. An underlying
|
|
@@ -31,7 +44,11 @@ export type TLinuxRepoErrorCode =
|
|
|
31
44
|
| 'digest-mismatch'
|
|
32
45
|
| 'invalid-input'
|
|
33
46
|
| 'invalid-key'
|
|
34
|
-
| 'signing-failed'
|
|
47
|
+
| 'signing-failed'
|
|
48
|
+
| 'malformed-signature'
|
|
49
|
+
| 'wrong-key'
|
|
50
|
+
| 'unusable-key'
|
|
51
|
+
| 'bad-signature';
|
|
35
52
|
|
|
36
53
|
/** Names the field, structure or limit an error is about. */
|
|
37
54
|
export interface ILinuxRepoErrorDetail {
|