@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.
- package/.smartconfig.json +0 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/debian/control.d.ts +7 -3
- package/dist_ts/debian/control.js +26 -6
- package/dist_ts/debian/indexbuilder.d.ts +2 -4
- package/dist_ts/debian/indexbuilder.js +10 -12
- package/dist_ts/debian/rules.d.ts +22 -0
- package/dist_ts/debian/rules.js +40 -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/architecture.d.ts +5 -1
- package/dist_ts/shared/architecture.js +6 -2
- 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 +35 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/debian/control.ts +25 -5
- package/ts/debian/indexbuilder.ts +15 -12
- package/ts/debian/rules.ts +44 -0
- package/ts/index.ts +9 -0
- package/ts/readers/text.ts +9 -0
- package/ts/shared/architecture.ts +5 -1
- package/ts/shared/errors.ts +18 -1
- package/ts/signing/openpgpframing.ts +295 -2
- package/ts/signing/verifiers.ts +357 -0
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 {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeLatin1, decodeStrictUtf8 } from '../readers/text.js';
|
|
2
|
+
import { LinuxRepoError, type TLinuxRepoErrorCode } from '../shared/errors.js';
|
|
2
3
|
import { assertAsyncIterable, assertBytes, assertString, assertTimestamp } from '../shared/validation.js';
|
|
3
4
|
import { singleChunk, type IOpenPgpSigner } from './signers.js';
|
|
4
5
|
|
|
@@ -6,6 +7,10 @@ import { singleChunk, type IOpenPgpSigner } from './signers.js';
|
|
|
6
7
|
* The OpenPGP framing the codec owns: only the bytes of the signature packet
|
|
7
8
|
* may vary; framing and armor are deterministic.
|
|
8
9
|
* Armor has no headers, 64-column base64 and a CRC-24 line.
|
|
10
|
+
*
|
|
11
|
+
* The readers below are the strict inverses the verifier uses; openpgp.js's
|
|
12
|
+
* own dearmoring is never used (it skips text around the block, ignores the
|
|
13
|
+
* CRC-24 and strips trailing whitespace inside cleartext).
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
const BASE64_LINE = 64;
|
|
@@ -47,7 +52,7 @@ export async function createDetachedArmoredSignature(inputArg: {
|
|
|
47
52
|
* Cleartext-signs `text` (`InRelease`). The text must be canonical: no CR and
|
|
48
53
|
* no trailing spaces or tabs on any line. The signed text is `text` without
|
|
49
54
|
* its final line break; the output always ends the text with one line break
|
|
50
|
-
* before the signature block, as GnuPG
|
|
55
|
+
* before the signature block, as GnuPG does. Lines starting with `-` are
|
|
51
56
|
* dash-escaped. The header is always `Hash: SHA256`.
|
|
52
57
|
*/
|
|
53
58
|
export async function createCleartextSignedMessage(inputArg: {
|
|
@@ -98,6 +103,294 @@ function assertSignatureBytes(packetArg: unknown): asserts packetArg is Uint8Arr
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
/** How a framing reader refuses its input: the error code and the input field it names. */
|
|
107
|
+
export interface IFramingFailure {
|
|
108
|
+
code: TLinuxRepoErrorCode;
|
|
109
|
+
field: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type TArmorLabel = 'PUBLIC KEY BLOCK' | 'SIGNATURE';
|
|
113
|
+
|
|
114
|
+
/** A well-formed armor header line (`Key: Value`), as openpgp.js 6.3.2 checks it (`verifyHeaders$1`). */
|
|
115
|
+
const ARMOR_HEADER_PATTERN = /^([^\s:]|[^\s:][^:]*[^\s:]): .+$/;
|
|
116
|
+
const BASE64_LINE_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
117
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
118
|
+
const CRC_LINE_PATTERN = /^=[A-Za-z0-9+/]{4}$/;
|
|
119
|
+
const CLEARTEXT_BEGIN = '-----BEGIN PGP SIGNED MESSAGE-----';
|
|
120
|
+
const SIGNATURE_BEGIN = '-----BEGIN PGP SIGNATURE-----';
|
|
121
|
+
|
|
122
|
+
/** RFC 9580 table 23 hash text names (the `Hash:` header) and their algorithm IDs. */
|
|
123
|
+
const HASH_TEXT_NAMES: ReadonlyMap<string, number> = new Map([
|
|
124
|
+
['MD5', 1],
|
|
125
|
+
['SHA1', 2],
|
|
126
|
+
['RIPEMD160', 3],
|
|
127
|
+
['SHA256', 8],
|
|
128
|
+
['SHA384', 9],
|
|
129
|
+
['SHA512', 10],
|
|
130
|
+
['SHA224', 11],
|
|
131
|
+
['SHA3-256', 12],
|
|
132
|
+
['SHA3-512', 14],
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
function framingError(failureArg: IFramingFailure, messageArg: string): LinuxRepoError {
|
|
136
|
+
return new LinuxRepoError(failureArg.code, messageArg, { field: failureArg.field });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Bytes that must be ASCII armor, as text; any byte above 0x7F is refused. */
|
|
140
|
+
export function decodeArmorBytes(bytesArg: Uint8Array, failureArg: IFramingFailure): string {
|
|
141
|
+
for (let i = 0; i < bytesArg.length; i++) {
|
|
142
|
+
if (bytesArg[i] > 0x7f) {
|
|
143
|
+
throw framingError(failureArg, 'ASCII armor must be ASCII');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return decodeLatin1(bytesArg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Reads exactly one ASCII armor block labelled `labelArg` (RFC 9580 section
|
|
151
|
+
* 6.2) and returns its bytes; the inverse of `armorSignature`. Lines may end
|
|
152
|
+
* in LF or CRLF and carry trailing spaces or tabs, and blank lines may
|
|
153
|
+
* surround the block. Well-formed `Key: Value` headers are ignored; the
|
|
154
|
+
* CRC-24 line is optional and must match when present. Refused with
|
|
155
|
+
* `failureArg`: non-ASCII text, another label, a second block or any other
|
|
156
|
+
* text outside the block, a malformed header line, no blank line before the
|
|
157
|
+
* data, base64 that is not canonical, no data, and a CRC-24 that does not
|
|
158
|
+
* match.
|
|
159
|
+
*/
|
|
160
|
+
export function dearmor(textArg: string, labelArg: TArmorLabel, failureArg: IFramingFailure): Uint8Array {
|
|
161
|
+
const lines = textArg.split('\n').map(trimLineEnd);
|
|
162
|
+
let first = 0;
|
|
163
|
+
while (first < lines.length && lines[first] === '') {
|
|
164
|
+
first += 1;
|
|
165
|
+
}
|
|
166
|
+
let last = lines.length - 1;
|
|
167
|
+
while (last >= first && lines[last] === '') {
|
|
168
|
+
last -= 1;
|
|
169
|
+
}
|
|
170
|
+
return readArmorBlock(lines.slice(first, last + 1), labelArg, failureArg);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The line without trailing spaces, tabs and CRs; a loop, as a `+$` regex is quadratic on a long run of them. */
|
|
174
|
+
function trimLineEnd(lineArg: string): string {
|
|
175
|
+
let end = lineArg.length;
|
|
176
|
+
while (end > 0 && (lineArg[end - 1] === ' ' || lineArg[end - 1] === '\t' || lineArg[end - 1] === '\r')) {
|
|
177
|
+
end -= 1;
|
|
178
|
+
}
|
|
179
|
+
return lineArg.slice(0, end);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** One armor block, from its BEGIN line to its END line, as exactly these lines; ASCII only. */
|
|
183
|
+
function readArmorBlock(linesArg: readonly string[], labelArg: TArmorLabel, failureArg: IFramingFailure): Uint8Array {
|
|
184
|
+
if (linesArg.some((line) => !/^[\x00-\x7f]*$/.test(line))) {
|
|
185
|
+
throw framingError(failureArg, 'ASCII armor must be ASCII');
|
|
186
|
+
}
|
|
187
|
+
const begin = `-----BEGIN PGP ${labelArg}-----`;
|
|
188
|
+
const end = `-----END PGP ${labelArg}-----`;
|
|
189
|
+
if (linesArg[0] !== begin) {
|
|
190
|
+
throw framingError(failureArg, `expected exactly one ${begin} block and nothing else`);
|
|
191
|
+
}
|
|
192
|
+
const endIndex = linesArg.indexOf(end);
|
|
193
|
+
if (endIndex < 0 || endIndex !== linesArg.length - 1) {
|
|
194
|
+
throw framingError(failureArg, `expected exactly one ${begin} block ending in ${end} and nothing else`);
|
|
195
|
+
}
|
|
196
|
+
let index = 1;
|
|
197
|
+
for (; index < endIndex && linesArg[index] !== ''; index++) {
|
|
198
|
+
if (!ARMOR_HEADER_PATTERN.test(linesArg[index])) {
|
|
199
|
+
throw framingError(failureArg, 'malformed armor header line');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (index >= endIndex) {
|
|
203
|
+
throw framingError(failureArg, 'the armor has no blank line before its data');
|
|
204
|
+
}
|
|
205
|
+
index += 1;
|
|
206
|
+
const dataLines: string[] = [];
|
|
207
|
+
let crcLine: string | null = null;
|
|
208
|
+
for (; index < endIndex; index++) {
|
|
209
|
+
const line = linesArg[index];
|
|
210
|
+
if (index === endIndex - 1 && line.startsWith('=')) {
|
|
211
|
+
crcLine = line;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (!BASE64_LINE_PATTERN.test(line)) {
|
|
215
|
+
throw framingError(failureArg, 'the armor data is not base64');
|
|
216
|
+
}
|
|
217
|
+
dataLines.push(line);
|
|
218
|
+
}
|
|
219
|
+
const base64 = dataLines.join('');
|
|
220
|
+
if (base64.length === 0 || base64.length % 4 !== 0 || !BASE64_PATTERN.test(base64)) {
|
|
221
|
+
throw framingError(failureArg, 'the armor data is not one base64 string');
|
|
222
|
+
}
|
|
223
|
+
const decoded = Buffer.from(base64, 'base64');
|
|
224
|
+
if (decoded.toString('base64') !== base64) {
|
|
225
|
+
throw framingError(failureArg, 'the armor data is not canonical base64');
|
|
226
|
+
}
|
|
227
|
+
const bytes = new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength);
|
|
228
|
+
if (crcLine !== null && (!CRC_LINE_PATTERN.test(crcLine) || crcLine.slice(1) !== crc24Base64(bytes))) {
|
|
229
|
+
throw framingError(failureArg, 'the armor CRC-24 does not match its data');
|
|
230
|
+
}
|
|
231
|
+
return bytes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The packet tags of a binary OpenPGP packet sequence, read from the packet
|
|
236
|
+
* headers alone (RFC 9580 section 4.2); no body is parsed. openpgp.js skips
|
|
237
|
+
* marker, trust and padding packets and unknown non-critical packet types
|
|
238
|
+
* without a trace (`PacketList.read`, `dist/node/openpgp.mjs:12320-12385`),
|
|
239
|
+
* so the verifier counts packets here. Refused with `failureArg`: a header
|
|
240
|
+
* without its high bit, a partial body length (no key or signature packet
|
|
241
|
+
* may use one), and input that ends inside a header or a body.
|
|
242
|
+
*/
|
|
243
|
+
export function readPacketTags(bytesArg: Uint8Array, failureArg: IFramingFailure): number[] {
|
|
244
|
+
const tags: number[] = [];
|
|
245
|
+
let offset = 0;
|
|
246
|
+
while (offset < bytesArg.length) {
|
|
247
|
+
const ctb = bytesArg[offset];
|
|
248
|
+
if ((ctb & 0x80) === 0) {
|
|
249
|
+
throw framingError(failureArg, `no OpenPGP packet header at byte ${offset}`);
|
|
250
|
+
}
|
|
251
|
+
let tag: number;
|
|
252
|
+
let headerLength: number;
|
|
253
|
+
let bodyLength: number;
|
|
254
|
+
if ((ctb & 0x40) !== 0) {
|
|
255
|
+
tag = ctb & 0x3f;
|
|
256
|
+
const first = bytesArg[offset + 1];
|
|
257
|
+
if (first === undefined) {
|
|
258
|
+
throw framingError(failureArg, 'the input ends inside a packet header');
|
|
259
|
+
}
|
|
260
|
+
if (first < 192) {
|
|
261
|
+
headerLength = 2;
|
|
262
|
+
bodyLength = first;
|
|
263
|
+
} else if (first < 224) {
|
|
264
|
+
if (offset + 3 > bytesArg.length) {
|
|
265
|
+
throw framingError(failureArg, 'the input ends inside a packet header');
|
|
266
|
+
}
|
|
267
|
+
headerLength = 3;
|
|
268
|
+
bodyLength = (first - 192) * 256 + bytesArg[offset + 2] + 192;
|
|
269
|
+
} else if (first === 255) {
|
|
270
|
+
if (offset + 6 > bytesArg.length) {
|
|
271
|
+
throw framingError(failureArg, 'the input ends inside a packet header');
|
|
272
|
+
}
|
|
273
|
+
headerLength = 6;
|
|
274
|
+
bodyLength = readUint32(bytesArg, offset + 2);
|
|
275
|
+
} else {
|
|
276
|
+
throw framingError(failureArg, 'a partial body length is not allowed here');
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
tag = (ctb >> 2) & 0x0f;
|
|
280
|
+
const lengthType = ctb & 0x03;
|
|
281
|
+
if (lengthType === 3) {
|
|
282
|
+
// indeterminate length: the packet runs to the end of the input
|
|
283
|
+
headerLength = 1;
|
|
284
|
+
bodyLength = bytesArg.length - offset - 1;
|
|
285
|
+
} else {
|
|
286
|
+
const lengthBytes = 1 << lengthType;
|
|
287
|
+
if (offset + 1 + lengthBytes > bytesArg.length) {
|
|
288
|
+
throw framingError(failureArg, 'the input ends inside a packet header');
|
|
289
|
+
}
|
|
290
|
+
headerLength = 1 + lengthBytes;
|
|
291
|
+
bodyLength = 0;
|
|
292
|
+
for (let i = 0; i < lengthBytes; i++) {
|
|
293
|
+
bodyLength = bodyLength * 256 + bytesArg[offset + 1 + i];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const packetEnd = offset + headerLength + bodyLength;
|
|
298
|
+
if (packetEnd > bytesArg.length) {
|
|
299
|
+
throw framingError(failureArg, 'the input ends inside a packet');
|
|
300
|
+
}
|
|
301
|
+
tags.push(tag);
|
|
302
|
+
offset = packetEnd;
|
|
303
|
+
}
|
|
304
|
+
return tags;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function readUint32(bytesArg: Uint8Array, offsetArg: number): number {
|
|
308
|
+
return ((bytesArg[offsetArg] * 256 + bytesArg[offsetArg + 1]) * 256 + bytesArg[offsetArg + 2]) * 256 + bytesArg[offsetArg + 3];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** A cleartext-signed message split into its parts. */
|
|
312
|
+
export interface ICleartextSignedParts {
|
|
313
|
+
/** Dash-escaping removed, lines joined by LF, without the line break before the signature block. */
|
|
314
|
+
signedText: string;
|
|
315
|
+
/** The RFC 9580 hash algorithm IDs the `Hash:` headers name, in order; empty without headers. */
|
|
316
|
+
headerHashAlgorithms: number[];
|
|
317
|
+
/** The bytes of the signature armor block. */
|
|
318
|
+
signature: Uint8Array;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Splits a cleartext-signed message (`InRelease`, RFC 9580 section 7); the
|
|
323
|
+
* inverse of `createCleartextSignedMessage`, and as strict as apt's own split
|
|
324
|
+
* (apt-pkg `SplitClearSignedFile`). The message is strict UTF-8 with LF line
|
|
325
|
+
* endings: the first line is `-----BEGIN PGP SIGNED MESSAGE-----`, then
|
|
326
|
+
* optional `Hash:` headers (comma-separated RFC 9580 text names), a blank
|
|
327
|
+
* line, the dash-escaped text and exactly one signature armor block, which
|
|
328
|
+
* may be followed by one line break and nothing else. Refused with
|
|
329
|
+
* `failureArg`: invalid UTF-8, a CR, a line ending in a space or tab, text
|
|
330
|
+
* before the message block or after the signature block, any other header, an
|
|
331
|
+
* unknown hash name, a text line starting with `-` but not `- `, and an empty
|
|
332
|
+
* text section. Armor headers inside the signature block are ignored as in
|
|
333
|
+
* `dearmor`.
|
|
334
|
+
*/
|
|
335
|
+
export function splitCleartextSignedMessage(bytesArg: Uint8Array, failureArg: IFramingFailure): ICleartextSignedParts {
|
|
336
|
+
const text = decodeStrictUtf8(bytesArg);
|
|
337
|
+
if (text === null) {
|
|
338
|
+
throw framingError(failureArg, 'the cleartext-signed message is not valid UTF-8');
|
|
339
|
+
}
|
|
340
|
+
if (text.includes('\r')) {
|
|
341
|
+
throw framingError(failureArg, 'the cleartext-signed message must use LF line endings');
|
|
342
|
+
}
|
|
343
|
+
const lines = text.split('\n');
|
|
344
|
+
if (lines[lines.length - 1] === '') {
|
|
345
|
+
lines.pop();
|
|
346
|
+
}
|
|
347
|
+
if (lines.some((line) => /[ \t]$/.test(line))) {
|
|
348
|
+
throw framingError(failureArg, 'a line of the cleartext-signed message ends with a space or tab');
|
|
349
|
+
}
|
|
350
|
+
if (lines[0] !== CLEARTEXT_BEGIN) {
|
|
351
|
+
throw framingError(failureArg, `the message does not start with ${CLEARTEXT_BEGIN}`);
|
|
352
|
+
}
|
|
353
|
+
let index = 1;
|
|
354
|
+
const headerHashAlgorithms: number[] = [];
|
|
355
|
+
for (; index < lines.length && lines[index] !== ''; index++) {
|
|
356
|
+
const header = /^Hash: (.+)$/.exec(lines[index]);
|
|
357
|
+
if (header === null) {
|
|
358
|
+
throw framingError(failureArg, 'only Hash: headers are allowed in a cleartext-signed message');
|
|
359
|
+
}
|
|
360
|
+
for (const name of header[1].split(',')) {
|
|
361
|
+
const algorithm = HASH_TEXT_NAMES.get(name.trim());
|
|
362
|
+
if (algorithm === undefined) {
|
|
363
|
+
throw framingError(failureArg, `unknown hash name in the Hash: header: ${name.trim()}`);
|
|
364
|
+
}
|
|
365
|
+
headerHashAlgorithms.push(algorithm);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
index += 1;
|
|
369
|
+
const textLines: string[] = [];
|
|
370
|
+
for (; index < lines.length && lines[index] !== SIGNATURE_BEGIN; index++) {
|
|
371
|
+
const line = lines[index];
|
|
372
|
+
if (line.startsWith('-')) {
|
|
373
|
+
if (!line.startsWith('- ')) {
|
|
374
|
+
throw framingError(failureArg, 'a text line starts with "-" without the "- " escape');
|
|
375
|
+
}
|
|
376
|
+
textLines.push(line.slice(2));
|
|
377
|
+
} else {
|
|
378
|
+
textLines.push(line);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (index >= lines.length) {
|
|
382
|
+
throw framingError(failureArg, `the message has no ${SIGNATURE_BEGIN} block`);
|
|
383
|
+
}
|
|
384
|
+
if (textLines.length === 0) {
|
|
385
|
+
throw framingError(failureArg, 'the message has no text section');
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
signedText: textLines.join('\n'),
|
|
389
|
+
headerHashAlgorithms,
|
|
390
|
+
signature: readArmorBlock(lines.slice(index), 'SIGNATURE', failureArg),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
101
394
|
/** RFC 4880 section 6.1 CRC-24, base64-encoded. */
|
|
102
395
|
function crc24Base64(bytesArg: Uint8Array): string {
|
|
103
396
|
let crc = 0xb704ce;
|