@push.rocks/smartlinuxrepo 0.0.0-stage → 1.0.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/.smartconfig.json +65 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/alpine/apkindex.d.ts +39 -0
- package/dist_ts/alpine/apkindex.js +211 -0
- package/dist_ts/alpine/pkginfo.d.ts +45 -0
- package/dist_ts/alpine/pkginfo.js +159 -0
- package/dist_ts/alpine/reader.d.ts +20 -0
- package/dist_ts/alpine/reader.js +89 -0
- package/dist_ts/arch/builder.d.ts +41 -0
- package/dist_ts/arch/builder.js +234 -0
- package/dist_ts/arch/reader.d.ts +16 -0
- package/dist_ts/arch/reader.js +271 -0
- package/dist_ts/arch/signer.d.ts +21 -0
- package/dist_ts/arch/signer.js +66 -0
- package/dist_ts/arch/vercmp.d.ts +12 -0
- package/dist_ts/arch/vercmp.js +170 -0
- package/dist_ts/debian/control.d.ts +22 -0
- package/dist_ts/debian/control.js +137 -0
- package/dist_ts/debian/gostring.d.ts +26 -0
- package/dist_ts/debian/gostring.js +145 -0
- package/dist_ts/debian/indexbuilder.d.ts +47 -0
- package/dist_ts/debian/indexbuilder.js +299 -0
- package/dist_ts/debian/mailaddress.d.ts +12 -0
- package/dist_ts/debian/mailaddress.js +736 -0
- package/dist_ts/debian/reader.d.ts +30 -0
- package/dist_ts/debian/reader.js +110 -0
- package/dist_ts/index.d.ts +23 -0
- package/dist_ts/index.js +18 -0
- package/dist_ts/plugins.d.ts +8 -0
- package/dist_ts/plugins.js +11 -0
- package/dist_ts/readers/ar.d.ts +31 -0
- package/dist_ts/readers/ar.js +87 -0
- package/dist_ts/readers/bytes.d.ts +58 -0
- package/dist_ts/readers/bytes.js +147 -0
- package/dist_ts/readers/bytesource.d.ts +40 -0
- package/dist_ts/readers/bytesource.js +154 -0
- package/dist_ts/readers/decompress.d.ts +43 -0
- package/dist_ts/readers/decompress.js +269 -0
- package/dist_ts/readers/gzipmember.d.ts +32 -0
- package/dist_ts/readers/gzipmember.js +176 -0
- package/dist_ts/readers/tar.d.ts +85 -0
- package/dist_ts/readers/tar.js +731 -0
- package/dist_ts/readers/text.d.ts +16 -0
- package/dist_ts/readers/text.js +96 -0
- package/dist_ts/rpm/builder.d.ts +28 -0
- package/dist_ts/rpm/builder.js +208 -0
- package/dist_ts/rpm/header.d.ts +138 -0
- package/dist_ts/rpm/header.js +546 -0
- package/dist_ts/rpm/reader.d.ts +20 -0
- package/dist_ts/rpm/reader.js +306 -0
- package/dist_ts/rpm/repoconfig.d.ts +15 -0
- package/dist_ts/rpm/repoconfig.js +38 -0
- package/dist_ts/rpm/repodata.d.ts +44 -0
- package/dist_ts/rpm/repodata.js +148 -0
- package/dist_ts/rpm/sign.d.ts +30 -0
- package/dist_ts/rpm/sign.js +183 -0
- package/dist_ts/rpm/updates.d.ts +38 -0
- package/dist_ts/rpm/updates.js +265 -0
- package/dist_ts/rpm/xml.d.ts +18 -0
- package/dist_ts/rpm/xml.js +90 -0
- package/dist_ts/shared/architecture.d.ts +8 -0
- package/dist_ts/shared/architecture.js +17 -0
- package/dist_ts/shared/errors.d.ts +38 -0
- package/dist_ts/shared/errors.js +22 -0
- package/dist_ts/shared/gopath.d.ts +13 -0
- package/dist_ts/shared/gopath.js +53 -0
- package/dist_ts/shared/gostrings.d.ts +14 -0
- package/dist_ts/shared/gostrings.js +29 -0
- package/dist_ts/shared/limits.d.ts +47 -0
- package/dist_ts/shared/limits.js +57 -0
- package/dist_ts/shared/types.d.ts +213 -0
- package/dist_ts/shared/types.js +13 -0
- package/dist_ts/shared/url.d.ts +18 -0
- package/dist_ts/shared/url.js +371 -0
- package/dist_ts/shared/validation.d.ts +21 -0
- package/dist_ts/shared/validation.js +58 -0
- package/dist_ts/signing/keys.d.ts +36 -0
- package/dist_ts/signing/keys.js +89 -0
- package/dist_ts/signing/openpgpframing.d.ts +33 -0
- package/dist_ts/signing/openpgpframing.js +93 -0
- package/dist_ts/signing/signers.d.ts +63 -0
- package/dist_ts/signing/signers.js +248 -0
- package/dist_ts/writers/gotar.d.ts +43 -0
- package/dist_ts/writers/gotar.js +218 -0
- package/license.md +19 -0
- package/package.json +56 -3
- package/readme.md +176 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/alpine/apkindex.ts +241 -0
- package/ts/alpine/pkginfo.ts +183 -0
- package/ts/alpine/reader.ts +96 -0
- package/ts/arch/builder.ts +283 -0
- package/ts/arch/reader.ts +307 -0
- package/ts/arch/signer.ts +70 -0
- package/ts/arch/vercmp.ts +188 -0
- package/ts/debian/control.ts +150 -0
- package/ts/debian/gostring.ts +150 -0
- package/ts/debian/indexbuilder.ts +355 -0
- package/ts/debian/mailaddress.ts +754 -0
- package/ts/debian/reader.ts +126 -0
- package/ts/index.ts +46 -0
- package/ts/plugins.ts +13 -0
- package/ts/readers/ar.ts +102 -0
- package/ts/readers/bytes.ts +182 -0
- package/ts/readers/bytesource.ts +164 -0
- package/ts/readers/decompress.ts +332 -0
- package/ts/readers/gzipmember.ts +209 -0
- package/ts/readers/tar.ts +835 -0
- package/ts/readers/text.ts +91 -0
- package/ts/rpm/builder.ts +253 -0
- package/ts/rpm/header.ts +628 -0
- package/ts/rpm/reader.ts +317 -0
- package/ts/rpm/repoconfig.ts +45 -0
- package/ts/rpm/repodata.ts +219 -0
- package/ts/rpm/sign.ts +200 -0
- package/ts/rpm/updates.ts +314 -0
- package/ts/rpm/xml.ts +101 -0
- package/ts/shared/architecture.ts +16 -0
- package/ts/shared/errors.ts +78 -0
- package/ts/shared/gopath.ts +54 -0
- package/ts/shared/gostrings.ts +31 -0
- package/ts/shared/limits.ts +93 -0
- package/ts/shared/types.ts +251 -0
- package/ts/shared/url.ts +387 -0
- package/ts/shared/validation.ts +71 -0
- package/ts/signing/keys.ts +110 -0
- package/ts/signing/openpgpframing.ts +115 -0
- package/ts/signing/signers.ts +315 -0
- package/ts/writers/gotar.ts +256 -0
- package/README.md +0 -3
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { concatBytes } from '../readers/bytes.js';
|
|
3
|
+
import { LinuxRepoError } from '../shared/errors.js';
|
|
4
|
+
import {
|
|
5
|
+
assertAsyncIterable,
|
|
6
|
+
assertBytes,
|
|
7
|
+
assertSegmentName,
|
|
8
|
+
assertString,
|
|
9
|
+
assertTimestamp,
|
|
10
|
+
} from '../shared/validation.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Signs Alpine indexes. Consumers may implement it; adding a required member
|
|
14
|
+
* later needs a major release.
|
|
15
|
+
*/
|
|
16
|
+
export interface IAlpineSigner {
|
|
17
|
+
/**
|
|
18
|
+
* The key name apk looks up in `/etc/apk/keys`; the index signature entry is
|
|
19
|
+
* `.SIGN.RSA.<keyName>`. Gitea's form is `<owner>@<sha256 hex of the SPKI
|
|
20
|
+
* DER public key>.rsa.pub` (see `giteaAlpineKeyName`).
|
|
21
|
+
*/
|
|
22
|
+
readonly keyName: string;
|
|
23
|
+
/** RSASSA-PKCS1-v1_5 over SHA-1 of `data`; returns the raw signature bytes. */
|
|
24
|
+
sign(data: Uint8Array): Promise<Uint8Array>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Signs Debian, RPM and Arch documents. Consumers may implement it; adding a
|
|
29
|
+
* required member later needs a major release.
|
|
30
|
+
*
|
|
31
|
+
* Contract: each call returns exactly one binary OpenPGP v4 signature packet
|
|
32
|
+
* (packet header included, no armor), hashed with SHA-256, whose creation time
|
|
33
|
+
* is `timestamp` (whole seconds). The codec does all framing (armor, cleartext
|
|
34
|
+
* dash-escaping, the `Hash: SHA256` header) and treats the packet as opaque.
|
|
35
|
+
*/
|
|
36
|
+
export interface IOpenPgpSigner {
|
|
37
|
+
/**
|
|
38
|
+
* A signature of type 0x00 (binary document) over the concatenation of the
|
|
39
|
+
* chunks of `data`. The signer reads `data` to its end exactly once.
|
|
40
|
+
*/
|
|
41
|
+
signBinary(data: AsyncIterable<Uint8Array>, timestamp: Date): Promise<Uint8Array>;
|
|
42
|
+
/**
|
|
43
|
+
* A signature of type 0x01 (canonical text document) over `text`. The codec
|
|
44
|
+
* passes canonical text only: LF line endings and no trailing spaces or tabs
|
|
45
|
+
* on any line; the signature hashes it with CRLF line endings.
|
|
46
|
+
*/
|
|
47
|
+
signText(text: string, timestamp: Date): Promise<Uint8Array>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ALPINE_PRIVATE_KEY_LABELS = ['RSA PRIVATE KEY', 'PRIVATE KEY'];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Builds an Alpine signer from an unencrypted RSA private key in PEM form:
|
|
54
|
+
* PKCS#1 (`RSA PRIVATE KEY`, Gitea's stored form) or PKCS#8 (`PRIVATE KEY`).
|
|
55
|
+
* Anything else is `invalid-key`. Signatures are deterministic.
|
|
56
|
+
*/
|
|
57
|
+
export function createAlpineSigner(inputArg: { privateKeyPem: string; keyName: string }): IAlpineSigner {
|
|
58
|
+
if (inputArg === null || typeof inputArg !== 'object') {
|
|
59
|
+
throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
|
|
60
|
+
}
|
|
61
|
+
assertString(inputArg.privateKeyPem, 'privateKeyPem');
|
|
62
|
+
assertSegmentName(inputArg.keyName, 'keyName');
|
|
63
|
+
const label = /-----BEGIN ([A-Z0-9 ]+)-----/.exec(inputArg.privateKeyPem)?.[1];
|
|
64
|
+
if (label === undefined || !ALPINE_PRIVATE_KEY_LABELS.includes(label)) {
|
|
65
|
+
throw new LinuxRepoError('invalid-key', 'the Alpine key must be an unencrypted PKCS#1 or PKCS#8 RSA private key PEM', {
|
|
66
|
+
field: 'privateKeyPem',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
let key: plugins.crypto.KeyObject;
|
|
70
|
+
try {
|
|
71
|
+
key = plugins.crypto.createPrivateKey({ key: inputArg.privateKeyPem, format: 'pem' });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new LinuxRepoError('invalid-key', 'the Alpine private key cannot be read', { field: 'privateKeyPem' }, {
|
|
74
|
+
cause: error,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (key.asymmetricKeyType !== 'rsa') {
|
|
78
|
+
throw new LinuxRepoError('invalid-key', 'the Alpine key must be an RSA key', { field: 'privateKeyPem' });
|
|
79
|
+
}
|
|
80
|
+
const keyName = inputArg.keyName;
|
|
81
|
+
return {
|
|
82
|
+
keyName,
|
|
83
|
+
sign: async (data: Uint8Array): Promise<Uint8Array> => {
|
|
84
|
+
assertBytes(data, 'data');
|
|
85
|
+
try {
|
|
86
|
+
const signature = plugins.crypto.sign('sha1', data, {
|
|
87
|
+
key,
|
|
88
|
+
padding: plugins.crypto.constants.RSA_PKCS1_PADDING,
|
|
89
|
+
});
|
|
90
|
+
return new Uint8Array(signature.buffer, signature.byteOffset, signature.byteLength);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new LinuxRepoError('signing-failed', 'RSA PKCS#1 v1.5 SHA-1 signing failed', { field: 'keyName' }, {
|
|
93
|
+
cause: error,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Builds an OpenPGP signer from an armored private key: exactly one key,
|
|
102
|
+
* OpenPGP v4, not encrypted, able to sign. Anything else is `invalid-key`.
|
|
103
|
+
* Signatures are SHA-256 and keep openpgp.js's salt notation, so their bytes
|
|
104
|
+
* vary per call; the produced packet is checked against the signer contract.
|
|
105
|
+
*
|
|
106
|
+
* The factory makes one trial `signBinary` and refuses the key with
|
|
107
|
+
* `invalid-key` when it fails, so a key that cannot meet the contract at all
|
|
108
|
+
* (one whose curve makes openpgp.js choose a stronger hash than SHA-256) is
|
|
109
|
+
* refused here. The trial signs at the latest creation time of the key, its
|
|
110
|
+
* subkeys and their self-signatures; a failure that depends on the signing
|
|
111
|
+
* time stays `signing-failed` at signing time.
|
|
112
|
+
*/
|
|
113
|
+
export async function createOpenPgpSigner(inputArg: { privateKeyArmored: string }): Promise<IOpenPgpSigner> {
|
|
114
|
+
if (inputArg === null || typeof inputArg !== 'object') {
|
|
115
|
+
throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
|
|
116
|
+
}
|
|
117
|
+
assertString(inputArg.privateKeyArmored, 'privateKeyArmored');
|
|
118
|
+
// openpgp.js reads the first armor block and ignores anything after it
|
|
119
|
+
const armorBlocks = inputArg.privateKeyArmored.match(/^-----BEGIN PGP [A-Z ,/0-9]+-----\s*$/gm) ?? [];
|
|
120
|
+
if (armorBlocks.length > 1) {
|
|
121
|
+
throw new LinuxRepoError('invalid-key', `expected one armored OpenPGP private key block, got ${armorBlocks.length}`, {
|
|
122
|
+
field: 'privateKeyArmored',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
let keys: plugins.openpgp.PrivateKey[];
|
|
126
|
+
try {
|
|
127
|
+
keys = await plugins.openpgp.readPrivateKeys({ armoredKeys: inputArg.privateKeyArmored });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new LinuxRepoError('invalid-key', 'the OpenPGP private key cannot be read', { field: 'privateKeyArmored' }, {
|
|
130
|
+
cause: error,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (keys.length !== 1) {
|
|
134
|
+
throw new LinuxRepoError('invalid-key', `expected exactly one OpenPGP private key, got ${keys.length}`, {
|
|
135
|
+
field: 'privateKeyArmored',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const key = keys[0];
|
|
139
|
+
if (key.keyPacket.version !== 4) {
|
|
140
|
+
throw new LinuxRepoError('invalid-key', `only OpenPGP v4 keys are accepted, got v${key.keyPacket.version}`, {
|
|
141
|
+
field: 'privateKeyArmored',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (!key.isDecrypted()) {
|
|
145
|
+
throw new LinuxRepoError('invalid-key', 'encrypted OpenPGP keys are not accepted', { field: 'privateKeyArmored' });
|
|
146
|
+
}
|
|
147
|
+
const sign = async (
|
|
148
|
+
produceArg: () => Promise<Uint8Array>,
|
|
149
|
+
timestampArg: Date,
|
|
150
|
+
signatureTypeArg: number,
|
|
151
|
+
inputFailureArg: () => { error: unknown } | undefined = () => undefined,
|
|
152
|
+
): Promise<Uint8Array> => {
|
|
153
|
+
let packet: Uint8Array;
|
|
154
|
+
try {
|
|
155
|
+
packet = await produceArg();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// an invalid chunk or the caller's own source error, not a signing failure
|
|
158
|
+
const inputFailure = inputFailureArg();
|
|
159
|
+
if (inputFailure !== undefined) {
|
|
160
|
+
throw inputFailure.error;
|
|
161
|
+
}
|
|
162
|
+
throw new LinuxRepoError('signing-failed', 'OpenPGP signing failed', { field: 'signingKey' }, { cause: error });
|
|
163
|
+
}
|
|
164
|
+
await assertSignaturePacket(packet, timestampArg, signatureTypeArg);
|
|
165
|
+
return packet;
|
|
166
|
+
};
|
|
167
|
+
const config = { preferredHashAlgorithm: plugins.openpgp.enums.hash.sha256 };
|
|
168
|
+
const signer: IOpenPgpSigner = {
|
|
169
|
+
signBinary: async (data: AsyncIterable<Uint8Array>, timestamp: Date): Promise<Uint8Array> => {
|
|
170
|
+
assertAsyncIterable(data, 'data');
|
|
171
|
+
assertTimestamp(timestamp);
|
|
172
|
+
const source = new CheckedChunkSource(data);
|
|
173
|
+
return sign(
|
|
174
|
+
async () => {
|
|
175
|
+
const signature = await plugins.openpgp.sign({
|
|
176
|
+
message: await plugins.openpgp.createMessage({ binary: source.toReadableStream() }),
|
|
177
|
+
signingKeys: key,
|
|
178
|
+
detached: true,
|
|
179
|
+
format: 'binary',
|
|
180
|
+
date: timestamp,
|
|
181
|
+
config,
|
|
182
|
+
});
|
|
183
|
+
const packet = await readAll(signature);
|
|
184
|
+
if (!source.ended) {
|
|
185
|
+
throw new Error('openpgp.js returned a signature before reading the data to its end');
|
|
186
|
+
}
|
|
187
|
+
return packet;
|
|
188
|
+
},
|
|
189
|
+
timestamp,
|
|
190
|
+
plugins.openpgp.enums.signature.binary,
|
|
191
|
+
() => source.failure,
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
signText: async (text: string, timestamp: Date): Promise<Uint8Array> => {
|
|
195
|
+
assertString(text, 'text');
|
|
196
|
+
assertTimestamp(timestamp);
|
|
197
|
+
return sign(
|
|
198
|
+
async () =>
|
|
199
|
+
plugins.openpgp.sign({
|
|
200
|
+
message: await plugins.openpgp.createMessage({ text }),
|
|
201
|
+
signingKeys: key,
|
|
202
|
+
detached: true,
|
|
203
|
+
format: 'binary',
|
|
204
|
+
date: timestamp,
|
|
205
|
+
config,
|
|
206
|
+
}),
|
|
207
|
+
timestamp,
|
|
208
|
+
plugins.openpgp.enums.signature.text,
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
await signer.signBinary(singleChunk(new Uint8Array(0)), latestKeyTime(key));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw new LinuxRepoError('invalid-key', 'the OpenPGP key cannot sign a v4 SHA-256 signature', { field: 'privateKeyArmored' }, {
|
|
216
|
+
cause: error,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return signer;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** `bytesArg` as a one-chunk source, for callers that hold the whole document. */
|
|
223
|
+
export async function* singleChunk(bytesArg: Uint8Array): AsyncGenerator<Uint8Array> {
|
|
224
|
+
yield bytesArg;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Pulls the caller's source with `next()` only (never `return()` or
|
|
229
|
+
* `throw()`, so a cancelled stream never cancels the source), refuses a chunk
|
|
230
|
+
* that is not a `Uint8Array` with `invalid-input`, and records the first
|
|
231
|
+
* error it raised or met so the signer can rethrow it unchanged.
|
|
232
|
+
*/
|
|
233
|
+
class CheckedChunkSource {
|
|
234
|
+
public ended = false;
|
|
235
|
+
public failure: { error: unknown } | undefined = undefined;
|
|
236
|
+
private readonly source: AsyncIterable<unknown>;
|
|
237
|
+
|
|
238
|
+
constructor(sourceArg: AsyncIterable<unknown>) {
|
|
239
|
+
this.source = sourceArg;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public toReadableStream(): plugins.streamWeb.ReadableStream<Uint8Array> {
|
|
243
|
+
const iterator = this.source[Symbol.asyncIterator]();
|
|
244
|
+
const next = async (): Promise<IteratorResult<Uint8Array>> => {
|
|
245
|
+
let result: IteratorResult<unknown>;
|
|
246
|
+
try {
|
|
247
|
+
result = await iterator.next();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.failure ??= { error };
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
if (result.done === true) {
|
|
253
|
+
this.ended = true;
|
|
254
|
+
return { done: true, value: undefined };
|
|
255
|
+
}
|
|
256
|
+
if (!(result.value instanceof Uint8Array)) {
|
|
257
|
+
const error = new LinuxRepoError('invalid-input', 'every chunk of data must be a Uint8Array', { field: 'data' });
|
|
258
|
+
this.failure ??= { error };
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
return { done: false, value: result.value };
|
|
262
|
+
};
|
|
263
|
+
return plugins.streamWeb.ReadableStream.from<Uint8Array>({ [Symbol.asyncIterator]: () => ({ next }) });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Reads openpgp.js's streamed output to its end. */
|
|
268
|
+
async function readAll(streamArg: plugins.streamWeb.ReadableStream<Uint8Array>): Promise<Uint8Array> {
|
|
269
|
+
const reader = streamArg.getReader();
|
|
270
|
+
const chunks: Uint8Array[] = [];
|
|
271
|
+
let total = 0;
|
|
272
|
+
for (let result = await reader.read(); !result.done; result = await reader.read()) {
|
|
273
|
+
chunks.push(result.value);
|
|
274
|
+
total += result.value.length;
|
|
275
|
+
}
|
|
276
|
+
return concatBytes(chunks, total);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The latest creation time of the key, its subkeys and their self-signatures: the key's current state applies. */
|
|
280
|
+
function latestKeyTime(keyArg: plugins.openpgp.PrivateKey): Date {
|
|
281
|
+
const times = [keyArg.getCreationTime().getTime()];
|
|
282
|
+
for (const user of keyArg.users) {
|
|
283
|
+
for (const signature of user.selfCertifications) {
|
|
284
|
+
times.push(signature.created?.getTime() ?? 0);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const subkey of keyArg.subkeys) {
|
|
288
|
+
times.push(subkey.getCreationTime().getTime());
|
|
289
|
+
for (const signature of subkey.bindingSignatures) {
|
|
290
|
+
times.push(signature.created?.getTime() ?? 0);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return new Date(Math.max(...times));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Checks that openpgp.js produced exactly the packet the signer contract promises. */
|
|
297
|
+
async function assertSignaturePacket(packetArg: Uint8Array, timestampArg: Date, signatureTypeArg: number): Promise<void> {
|
|
298
|
+
const signature = await plugins.openpgp.readSignature({ binarySignature: packetArg });
|
|
299
|
+
const packets = signature.packets;
|
|
300
|
+
const expectedSeconds = Math.floor(timestampArg.getTime() / 1000);
|
|
301
|
+
const ok =
|
|
302
|
+
packets.length === 1 &&
|
|
303
|
+
packets[0].version === 4 &&
|
|
304
|
+
packets[0].signatureType === signatureTypeArg &&
|
|
305
|
+
packets[0].hashAlgorithm === plugins.openpgp.enums.hash.sha256 &&
|
|
306
|
+
packets[0].created !== null &&
|
|
307
|
+
Math.floor(packets[0].created.getTime() / 1000) === expectedSeconds;
|
|
308
|
+
if (!ok) {
|
|
309
|
+
throw new LinuxRepoError(
|
|
310
|
+
'signing-failed',
|
|
311
|
+
'the signing key cannot produce one v4 SHA-256 signature packet at the requested time',
|
|
312
|
+
{ field: 'signingKey' },
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { concatBytes } from '../readers/bytes.js';
|
|
2
|
+
import { LinuxRepoError } from '../shared/errors.js';
|
|
3
|
+
import { goPathJoin } from '../shared/gopath.js';
|
|
4
|
+
|
|
5
|
+
export interface IGoTarFile {
|
|
6
|
+
name: string;
|
|
7
|
+
mode: number;
|
|
8
|
+
data: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IGoTarWriteOptions {
|
|
12
|
+
/**
|
|
13
|
+
* The two zero blocks Go `Writer.Close` writes. `false` for Gitea's Alpine
|
|
14
|
+
* signature member (`writeGzipStream` with `addTarEnd=false`). Gitea never
|
|
15
|
+
* calls `Close` there, so Go also leaves the last entry unpadded; the codec
|
|
16
|
+
* pads every entry to the block regardless (a difference from Gitea).
|
|
17
|
+
*/
|
|
18
|
+
endOfArchive: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const BLOCK = 512;
|
|
22
|
+
const NAME_SIZE = 100;
|
|
23
|
+
const PREFIX_SIZE = 155;
|
|
24
|
+
/** Go `maxSpecialFileSize`: the largest PAX record block Go writes or reads. */
|
|
25
|
+
const MAX_SPECIAL_FILE_SIZE = 1 << 20;
|
|
26
|
+
/** Go `fitsInOctal(8, x)` for the mode field: below 8^7. */
|
|
27
|
+
const MODE_LIMIT = 8 ** 7;
|
|
28
|
+
/** Go `fitsInOctal(12, x)` for the size field: below 8^11 (8 GiB). */
|
|
29
|
+
const SIZE_LIMIT = 8 ** 11;
|
|
30
|
+
const encoder = new TextEncoder();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The subset of Go 1.27 `archive/tar` `Writer` that Gitea 28.2.0 uses for its
|
|
34
|
+
* index archives (Arch `services/packages/arch/repository.go:381-399`, Alpine
|
|
35
|
+
* `services/packages/alpine/repository.go:331-351`): regular files from
|
|
36
|
+
* `tar.Header{Name, Mode, Size}` with every other field zero, so the zero
|
|
37
|
+
* `ModTime` is written as 0, uid and gid are 0 and user and group names are
|
|
38
|
+
* empty.
|
|
39
|
+
*
|
|
40
|
+
* Format choice follows Go's `allowedFormats` and `WriteHeader`: USTAR when the
|
|
41
|
+
* name is ASCII and at most 100 bytes, USTAR with a prefix split when the name
|
|
42
|
+
* splits, else PAX with one `path` record (refused when the records exceed
|
|
43
|
+
* Go's 1 MiB `maxSpecialFileSize`, as Go returns `ErrFieldTooLong`).
|
|
44
|
+
*
|
|
45
|
+
* Refused with `invalid-input` where Go would not write a regular file in USTAR
|
|
46
|
+
* or PAX-`path` form:
|
|
47
|
+
* - a name with NUL (Go fails with an invalid PAX record) or ending with `/`
|
|
48
|
+
* (Go refuses it for a regular file; Gitea's untyped header would become a
|
|
49
|
+
* directory entry instead);
|
|
50
|
+
* - a mode outside 7 octal digits (Go falls back to the GNU format);
|
|
51
|
+
* - a size of 8 GiB or more (Go adds a PAX `size` record; the codec does not);
|
|
52
|
+
* - a string that is not well-formed UTF-16 (Go strings never carry one).
|
|
53
|
+
*/
|
|
54
|
+
export function writeGoTar(
|
|
55
|
+
filesArg: readonly IGoTarFile[],
|
|
56
|
+
optionsArg: IGoTarWriteOptions = { endOfArchive: true },
|
|
57
|
+
): Uint8Array {
|
|
58
|
+
const parts: Uint8Array[] = [];
|
|
59
|
+
for (const file of filesArg) {
|
|
60
|
+
parts.push(goTarHeader(file.name, file.mode, file.data.length), file.data, new Uint8Array(padding(file.data.length)));
|
|
61
|
+
}
|
|
62
|
+
if (optionsArg.endOfArchive) {
|
|
63
|
+
parts.push(new Uint8Array(BLOCK * 2));
|
|
64
|
+
}
|
|
65
|
+
return concatBytes(parts);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The bytes Go `Writer.WriteHeader` writes for a regular file: the main
|
|
70
|
+
* header block, preceded by a PAX `x` header and its padded records when the
|
|
71
|
+
* name needs one.
|
|
72
|
+
*/
|
|
73
|
+
export function goTarHeader(nameArg: string, modeArg: number, sizeArg: number): Uint8Array {
|
|
74
|
+
if (!nameArg.isWellFormed()) {
|
|
75
|
+
throw new LinuxRepoError('invalid-input', 'a tar entry name is not well-formed UTF-16', { field: 'name' });
|
|
76
|
+
}
|
|
77
|
+
if (nameArg.includes('\0')) {
|
|
78
|
+
throw new LinuxRepoError('invalid-input', 'a tar entry name contains NUL', { field: 'name' });
|
|
79
|
+
}
|
|
80
|
+
if (nameArg.endsWith('/')) {
|
|
81
|
+
throw new LinuxRepoError('invalid-input', 'a regular tar entry name ends with /', { field: 'name' });
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isSafeInteger(modeArg) || modeArg < 0 || modeArg >= MODE_LIMIT) {
|
|
84
|
+
throw new LinuxRepoError('invalid-input', 'a tar entry mode does not fit the USTAR mode field', {
|
|
85
|
+
field: 'mode',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!Number.isSafeInteger(sizeArg) || sizeArg < 0 || sizeArg >= SIZE_LIMIT) {
|
|
89
|
+
throw new LinuxRepoError('invalid-input', 'a tar entry size does not fit the USTAR size field', {
|
|
90
|
+
field: 'size',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const nameBytes = encoder.encode(nameArg);
|
|
94
|
+
if (isAscii(nameBytes) && nameBytes.length <= NAME_SIZE) {
|
|
95
|
+
return header({ name: nameBytes, prefix: null, size: sizeArg, mode: modeArg, typeflag: '0' });
|
|
96
|
+
}
|
|
97
|
+
const split = splitUstarPath(nameBytes);
|
|
98
|
+
if (split !== null) {
|
|
99
|
+
return header({ name: split.name, prefix: split.prefix, size: sizeArg, mode: modeArg, typeflag: '0' });
|
|
100
|
+
}
|
|
101
|
+
return paxHeader(nameArg, modeArg, sizeArg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Go `splitUSTARPath` (`archive/tar/writer.go:454`). */
|
|
105
|
+
function splitUstarPath(nameArg: Uint8Array): { prefix: Uint8Array; name: Uint8Array } | null {
|
|
106
|
+
let length = nameArg.length;
|
|
107
|
+
if (length <= NAME_SIZE || !isAscii(nameArg)) {
|
|
108
|
+
return null;
|
|
109
|
+
} else if (length > PREFIX_SIZE + 1) {
|
|
110
|
+
length = PREFIX_SIZE + 1;
|
|
111
|
+
} else if (nameArg[length - 1] === 0x2f) {
|
|
112
|
+
length--;
|
|
113
|
+
}
|
|
114
|
+
const i = nameArg.subarray(0, length).lastIndexOf(0x2f);
|
|
115
|
+
const nameLength = nameArg.length - i - 1;
|
|
116
|
+
const prefixLength = i;
|
|
117
|
+
if (i <= 0 || nameLength > NAME_SIZE || nameLength === 0 || prefixLength > PREFIX_SIZE) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { prefix: nameArg.subarray(0, i), name: nameArg.subarray(i + 1) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Go `writePAXHeader` (`archive/tar/writer.go:131`) for one `path` record:
|
|
125
|
+
* the `x` header written by `writeRawFile`, the records, their padding, then
|
|
126
|
+
* the main header with the ASCII-folded name.
|
|
127
|
+
*/
|
|
128
|
+
function paxHeader(nameArg: string, modeArg: number, sizeArg: number): Uint8Array {
|
|
129
|
+
const records = paxRecord('path', nameArg);
|
|
130
|
+
if (records.length > MAX_SPECIAL_FILE_SIZE) {
|
|
131
|
+
throw new LinuxRepoError('invalid-input', 'a tar entry name is longer than Go archive/tar writes', {
|
|
132
|
+
field: 'name',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// path.Split, then path.Join(dir, "PaxHeaders.0", file); writeRawFile folds
|
|
136
|
+
// it to ASCII, cuts it to 100 bytes and trims trailing slashes
|
|
137
|
+
const slash = nameArg.lastIndexOf('/');
|
|
138
|
+
const dir = nameArg.slice(0, slash + 1);
|
|
139
|
+
const file = nameArg.slice(slash + 1);
|
|
140
|
+
let paxName = encoder.encode(toAscii(goPathJoin([dir, 'PaxHeaders.0', file])));
|
|
141
|
+
if (paxName.length > NAME_SIZE) {
|
|
142
|
+
paxName = paxName.subarray(0, NAME_SIZE);
|
|
143
|
+
}
|
|
144
|
+
let end = paxName.length;
|
|
145
|
+
while (end > 0 && paxName[end - 1] === 0x2f) {
|
|
146
|
+
end--;
|
|
147
|
+
}
|
|
148
|
+
return concatBytes([
|
|
149
|
+
header({ name: paxName.subarray(0, end), prefix: null, size: records.length, mode: 0, typeflag: 'x', raw: true }),
|
|
150
|
+
records,
|
|
151
|
+
new Uint8Array(padding(records.length)),
|
|
152
|
+
header({ name: encoder.encode(toAscii(nameArg)), prefix: null, size: sizeArg, mode: modeArg, typeflag: '0' }),
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Go `formatPAXRecord` (`archive/tar/strconv.go:291`). */
|
|
157
|
+
function paxRecord(keyArg: string, valueArg: string): Uint8Array {
|
|
158
|
+
const payload = encoder.encode(` ${keyArg}=${valueArg}\n`);
|
|
159
|
+
let size = payload.length;
|
|
160
|
+
size += String(size).length;
|
|
161
|
+
let record = concatBytes([encoder.encode(String(size)), payload]);
|
|
162
|
+
if (record.length !== size) {
|
|
163
|
+
size = record.length;
|
|
164
|
+
record = concatBytes([encoder.encode(String(size)), payload]);
|
|
165
|
+
}
|
|
166
|
+
return record;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface IHeaderSpec {
|
|
170
|
+
name: Uint8Array;
|
|
171
|
+
prefix: Uint8Array | null;
|
|
172
|
+
size: number;
|
|
173
|
+
mode: number;
|
|
174
|
+
typeflag: '0' | 'x';
|
|
175
|
+
/** `writeRawFile` (`archive/tar/writer.go:353`): devmajor and devminor stay zero bytes. */
|
|
176
|
+
raw?: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Go `templateV7Plus` plus `setFormat` (`archive/tar/writer.go:323`, `format.go:205`). */
|
|
180
|
+
function header(specArg: IHeaderSpec): Uint8Array {
|
|
181
|
+
const block = new Uint8Array(BLOCK);
|
|
182
|
+
formatString(block, 0, NAME_SIZE, specArg.name);
|
|
183
|
+
formatOctal(block, 100, 8, specArg.mode);
|
|
184
|
+
formatOctal(block, 108, 8, 0); // uid
|
|
185
|
+
formatOctal(block, 116, 8, 0); // gid
|
|
186
|
+
formatOctal(block, 124, 12, specArg.size);
|
|
187
|
+
formatOctal(block, 136, 12, 0); // mtime: the zero time.Time is written as 0
|
|
188
|
+
block[156] = specArg.typeflag.charCodeAt(0);
|
|
189
|
+
// linkname, uname and gname stay empty
|
|
190
|
+
if (!specArg.raw) {
|
|
191
|
+
formatOctal(block, 329, 8, 0); // devmajor
|
|
192
|
+
formatOctal(block, 337, 8, 0); // devminor
|
|
193
|
+
}
|
|
194
|
+
if (specArg.prefix !== null) {
|
|
195
|
+
formatString(block, 345, PREFIX_SIZE, specArg.prefix);
|
|
196
|
+
}
|
|
197
|
+
block.set(encoder.encode('ustar\0'), 257);
|
|
198
|
+
block.set(encoder.encode('00'), 263);
|
|
199
|
+
let sum = 0;
|
|
200
|
+
for (let i = 0; i < BLOCK; i++) {
|
|
201
|
+
sum += i >= 148 && i < 156 ? 0x20 : block[i];
|
|
202
|
+
}
|
|
203
|
+
formatOctal(block, 148, 7, sum);
|
|
204
|
+
block[155] = 0x20;
|
|
205
|
+
return block;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Go `formatter.formatString` (`archive/tar/strconv.go:63`): copy, NUL-terminate when shorter, cut when longer. */
|
|
209
|
+
function formatString(blockArg: Uint8Array, offsetArg: number, lengthArg: number, valueArg: Uint8Array): void {
|
|
210
|
+
const field = blockArg.subarray(offsetArg, offsetArg + lengthArg);
|
|
211
|
+
field.set(valueArg.subarray(0, lengthArg));
|
|
212
|
+
if (valueArg.length < lengthArg) {
|
|
213
|
+
field[valueArg.length] = 0;
|
|
214
|
+
}
|
|
215
|
+
if (valueArg.length > lengthArg && field[lengthArg - 1] === 0x2f) {
|
|
216
|
+
let n = lengthArg - 1;
|
|
217
|
+
while (n > 0 && valueArg[n - 1] === 0x2f) {
|
|
218
|
+
n--;
|
|
219
|
+
}
|
|
220
|
+
field[n] = 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Go `formatter.formatOctal` (`archive/tar/strconv.go:176`): zero-padded octal
|
|
226
|
+
* leaving room for a NUL. Callers check the range first; Go would write 0.
|
|
227
|
+
*/
|
|
228
|
+
function formatOctal(blockArg: Uint8Array, offsetArg: number, lengthArg: number, valueArg: number): void {
|
|
229
|
+
const digits = valueArg.toString(8).padStart(lengthArg - 1, '0');
|
|
230
|
+
formatString(blockArg, offsetArg, lengthArg, encoder.encode(digits));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Go `toASCII` (`archive/tar/strconv.go:32`): drops every non-ASCII rune (NUL is refused earlier). */
|
|
234
|
+
function toAscii(valueArg: string): string {
|
|
235
|
+
let result = '';
|
|
236
|
+
for (const char of valueArg) {
|
|
237
|
+
if (char.codePointAt(0)! < 0x80) {
|
|
238
|
+
result += char;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Go `isASCII` (`archive/tar/strconv.go:21`) for a name without NUL. */
|
|
245
|
+
function isAscii(bytesArg: Uint8Array): boolean {
|
|
246
|
+
for (let i = 0; i < bytesArg.length; i++) {
|
|
247
|
+
if (bytesArg[i] >= 0x80) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function padding(sizeArg: number): number {
|
|
255
|
+
return (BLOCK - (sizeArg % BLOCK)) % BLOCK;
|
|
256
|
+
}
|
package/README.md
DELETED