@passportsign/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/badge.d.ts +5 -0
- package/dist/badge.d.ts.map +1 -1
- package/dist/badge.js +8 -2
- package/dist/badge.js.map +1 -1
- package/dist/bind.d.ts.map +1 -1
- package/dist/bind.js +2 -8
- package/dist/bind.js.map +1 -1
- package/dist/bundle-fs.d.ts +16 -0
- package/dist/bundle-fs.d.ts.map +1 -0
- package/dist/bundle-fs.js +31 -0
- package/dist/bundle-fs.js.map +1 -0
- package/dist/bundle.d.ts +13 -5
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +18 -20
- package/dist/bundle.js.map +1 -1
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +3 -4
- package/dist/canonical.js.map +1 -1
- package/dist/classify.d.ts +68 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +117 -0
- package/dist/classify.js.map +1 -0
- package/dist/dsse-common.d.ts +32 -0
- package/dist/dsse-common.d.ts.map +1 -0
- package/dist/dsse-common.js +26 -0
- package/dist/dsse-common.js.map +1 -0
- package/dist/dsse-web.d.ts +28 -0
- package/dist/dsse-web.d.ts.map +1 -0
- package/dist/dsse-web.js +81 -0
- package/dist/dsse-web.js.map +1 -0
- package/dist/dsse.d.ts +2 -26
- package/dist/dsse.d.ts.map +1 -1
- package/dist/dsse.js +2 -19
- package/dist/dsse.js.map +1 -1
- package/dist/encoding.d.ts +20 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +88 -0
- package/dist/encoding.js.map +1 -0
- package/dist/github.js +2 -2
- package/dist/github.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/log/rekor.d.ts +1 -1
- package/dist/log/rekor.d.ts.map +1 -1
- package/dist/log/rekor.js +7 -10
- package/dist/log/rekor.js.map +1 -1
- package/dist/lookup.d.ts +46 -0
- package/dist/lookup.d.ts.map +1 -0
- package/dist/lookup.js +101 -0
- package/dist/lookup.js.map +1 -0
- package/dist/merkle.js +3 -3
- package/dist/merkle.js.map +1 -1
- package/dist/nonce.js +1 -1
- package/dist/nonce.js.map +1 -1
- package/dist/profile-index.d.ts +64 -0
- package/dist/profile-index.d.ts.map +1 -0
- package/dist/profile-index.js +161 -0
- package/dist/profile-index.js.map +1 -0
- package/dist/revoke.d.ts +30 -0
- package/dist/revoke.d.ts.map +1 -0
- package/dist/revoke.js +42 -0
- package/dist/revoke.js.map +1 -0
- package/dist/sdk-payload.d.ts.map +1 -1
- package/dist/sdk-payload.js +4 -6
- package/dist/sdk-payload.js.map +1 -1
- package/dist/statement.d.ts +41 -0
- package/dist/statement.d.ts.map +1 -1
- package/dist/statement.js +43 -0
- package/dist/statement.js.map +1 -1
- package/dist/submit.d.ts +3 -3
- package/dist/submit.d.ts.map +1 -1
- package/dist/submit.js +3 -14
- package/dist/submit.js.map +1 -1
- package/dist/verifier.d.ts.map +1 -1
- package/dist/verifier.js +4 -14
- package/dist/verifier.js.map +1 -1
- package/dist/web.d.ts +35 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +35 -0
- package/dist/web.js.map +1 -0
- package/package.json +6 -2
- package/src/badge.ts +124 -113
- package/src/bind.ts +128 -137
- package/src/bundle-fs.ts +40 -0
- package/src/bundle.ts +138 -127
- package/src/canonical.ts +33 -33
- package/src/classify.ts +165 -0
- package/src/dsse-common.ts +45 -0
- package/src/dsse-web.ts +97 -0
- package/src/dsse.ts +63 -91
- package/src/encoding.ts +96 -0
- package/src/github.ts +196 -196
- package/src/index.ts +59 -2
- package/src/log/rekor.ts +330 -334
- package/src/lookup.ts +175 -0
- package/src/merkle.ts +187 -187
- package/src/nonce.ts +53 -53
- package/src/profile-index.ts +222 -0
- package/src/revoke.ts +67 -0
- package/src/sdk-payload.ts +60 -62
- package/src/statement.ts +203 -119
- package/src/submit.ts +38 -54
- package/src/verifier.ts +304 -317
- package/src/web.ts +175 -0
package/src/log/rekor.ts
CHANGED
|
@@ -1,334 +1,330 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rekor client for in-toto v0.0.2 entries.
|
|
3
|
-
*
|
|
4
|
-
* Behavior pinned from the Day 5 smoke test against
|
|
5
|
-
* `rekor.sigstore.dev` (see docs/v0-acceptance.md Day 5 evidence).
|
|
6
|
-
* The most non-obvious bits live in `buildIntotoEntryBody` below.
|
|
7
|
-
*
|
|
8
|
-
* All HTTP failures surface as
|
|
9
|
-
* `PassportsignError('log_submission_failed', …)` to match spec §4.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
private readonly
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
typeof
|
|
137
|
-
typeof
|
|
138
|
-
typeof
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
'
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
'
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* - `
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* - `
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
'
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
: {}),
|
|
332
|
-
verification: { inclusionProof, signedEntryTimestamp },
|
|
333
|
-
};
|
|
334
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Rekor client for in-toto v0.0.2 entries.
|
|
3
|
+
*
|
|
4
|
+
* Behavior pinned from the Day 5 smoke test against
|
|
5
|
+
* `rekor.sigstore.dev` (see docs/v0-acceptance.md Day 5 evidence).
|
|
6
|
+
* The most non-obvious bits live in `buildIntotoEntryBody` below.
|
|
7
|
+
*
|
|
8
|
+
* All HTTP failures surface as
|
|
9
|
+
* `PassportsignError('log_submission_failed', …)` to match spec §4.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { canonicalize } from '../canonical.js';
|
|
13
|
+
import { type DsseEnvelope } from '../dsse-common.js';
|
|
14
|
+
import { base64ToBytes, bytesToBase64, sha256Hex, utf8ToBytes } from '../encoding.js';
|
|
15
|
+
import { PassportsignError } from '../errors.js';
|
|
16
|
+
|
|
17
|
+
export interface InclusionProof {
|
|
18
|
+
checkpoint: string;
|
|
19
|
+
hashes: string[];
|
|
20
|
+
logIndex: number;
|
|
21
|
+
rootHash: string;
|
|
22
|
+
treeSize: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RekorEntryResponse {
|
|
26
|
+
uuid: string;
|
|
27
|
+
logIndex: number;
|
|
28
|
+
integratedTime: number;
|
|
29
|
+
logID: string;
|
|
30
|
+
/** base64-encoded canonicalised entry body the server stored. */
|
|
31
|
+
body: string;
|
|
32
|
+
/** Optional server-stored attestation (base64). */
|
|
33
|
+
attestation?: { data?: string } | undefined;
|
|
34
|
+
verification: {
|
|
35
|
+
inclusionProof: InclusionProof;
|
|
36
|
+
/** Rekor's signed timestamp over the entry (base64). */
|
|
37
|
+
signedEntryTimestamp: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RekorLogInfo {
|
|
42
|
+
/** Hex-encoded current root hash of the active tree. */
|
|
43
|
+
rootHash: string;
|
|
44
|
+
/** Number of entries currently in the active tree. */
|
|
45
|
+
treeSize: number;
|
|
46
|
+
/** Signed tree head (Rekor's signature over the current root + size). */
|
|
47
|
+
signedTreeHead: string;
|
|
48
|
+
/** Active tree ID (string per Rekor's API). */
|
|
49
|
+
treeID: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RekorConsistencyProof {
|
|
53
|
+
/** Hex hashes proving treeSize=first is a prefix of treeSize=last. */
|
|
54
|
+
hashes: string[];
|
|
55
|
+
/** Hex root hash at the new size (informational; we verify against our own captured one). */
|
|
56
|
+
rootHash: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RekorClient {
|
|
60
|
+
submitIntoto(envelope: DsseEnvelope): Promise<RekorEntryResponse>;
|
|
61
|
+
getEntry(uuid: string): Promise<RekorEntryResponse>;
|
|
62
|
+
getLogInfo(): Promise<RekorLogInfo>;
|
|
63
|
+
getConsistencyProof(firstSize: number, lastSize: number): Promise<RekorConsistencyProof>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PublicSigstoreRekorClientOptions {
|
|
67
|
+
baseUrl?: string;
|
|
68
|
+
fetch?: typeof fetch;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const DEFAULT_REKOR_BASE_URL = 'https://rekor.sigstore.dev';
|
|
72
|
+
|
|
73
|
+
export class PublicSigstoreRekorClient implements RekorClient {
|
|
74
|
+
private readonly baseUrl: string;
|
|
75
|
+
private readonly fetchImpl: typeof fetch;
|
|
76
|
+
|
|
77
|
+
constructor(opts: PublicSigstoreRekorClientOptions = {}) {
|
|
78
|
+
this.baseUrl = opts.baseUrl ?? DEFAULT_REKOR_BASE_URL;
|
|
79
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async submitIntoto(envelope: DsseEnvelope): Promise<RekorEntryResponse> {
|
|
83
|
+
const body = buildIntotoEntryBody(envelope);
|
|
84
|
+
return this.postEntry(body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getEntry(uuid: string): Promise<RekorEntryResponse> {
|
|
88
|
+
let response: Response;
|
|
89
|
+
try {
|
|
90
|
+
response = await this.fetchImpl(`${this.baseUrl}/api/v1/log/entries/${uuid}`);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new PassportsignError(
|
|
93
|
+
'log_submission_failed',
|
|
94
|
+
`Rekor get-entry request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
95
|
+
err,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
let errBody = '';
|
|
100
|
+
try { errBody = await response.text(); } catch { /* ignore */ }
|
|
101
|
+
throw new PassportsignError(
|
|
102
|
+
'log_submission_failed',
|
|
103
|
+
`Rekor get-entry returned ${response.status}: ${errBody}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return parseEntryResponse(await response.json().catch(() => null));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getLogInfo(): Promise<RekorLogInfo> {
|
|
110
|
+
let response: Response;
|
|
111
|
+
try {
|
|
112
|
+
response = await this.fetchImpl(`${this.baseUrl}/api/v1/log`);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new PassportsignError(
|
|
115
|
+
'log_submission_failed',
|
|
116
|
+
`Rekor log-info request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
117
|
+
err,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new PassportsignError(
|
|
122
|
+
'log_submission_failed',
|
|
123
|
+
`Rekor log-info returned ${response.status}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const body = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
127
|
+
if (!body || typeof body !== 'object') {
|
|
128
|
+
throw new PassportsignError('log_submission_failed', 'Rekor log-info returned non-object');
|
|
129
|
+
}
|
|
130
|
+
const rootHash = body['rootHash'];
|
|
131
|
+
const treeSize = body['treeSize'];
|
|
132
|
+
const signedTreeHead = body['signedTreeHead'];
|
|
133
|
+
const treeID = body['treeID'];
|
|
134
|
+
if (
|
|
135
|
+
typeof rootHash !== 'string' ||
|
|
136
|
+
typeof treeSize !== 'number' ||
|
|
137
|
+
typeof signedTreeHead !== 'string' ||
|
|
138
|
+
typeof treeID !== 'string'
|
|
139
|
+
) {
|
|
140
|
+
throw new PassportsignError('log_submission_failed', 'Rekor log-info missing required fields');
|
|
141
|
+
}
|
|
142
|
+
return { rootHash, treeSize, signedTreeHead, treeID };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getConsistencyProof(firstSize: number, lastSize: number): Promise<RekorConsistencyProof> {
|
|
146
|
+
let response: Response;
|
|
147
|
+
try {
|
|
148
|
+
response = await this.fetchImpl(
|
|
149
|
+
`${this.baseUrl}/api/v1/log/proof?firstSize=${firstSize}&lastSize=${lastSize}`,
|
|
150
|
+
);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
throw new PassportsignError(
|
|
153
|
+
'log_submission_failed',
|
|
154
|
+
`Rekor consistency-proof request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
155
|
+
err,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new PassportsignError(
|
|
160
|
+
'log_submission_failed',
|
|
161
|
+
`Rekor consistency-proof returned ${response.status}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const body = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
165
|
+
if (!body || typeof body !== 'object') {
|
|
166
|
+
throw new PassportsignError(
|
|
167
|
+
'log_submission_failed',
|
|
168
|
+
'Rekor consistency-proof returned non-object',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const hashes = body['hashes'];
|
|
172
|
+
const rootHash = body['rootHash'];
|
|
173
|
+
if (!Array.isArray(hashes) || !hashes.every((h) => typeof h === 'string')) {
|
|
174
|
+
throw new PassportsignError(
|
|
175
|
+
'log_submission_failed',
|
|
176
|
+
'Rekor consistency-proof has no hashes array',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (typeof rootHash !== 'string') {
|
|
180
|
+
throw new PassportsignError(
|
|
181
|
+
'log_submission_failed',
|
|
182
|
+
'Rekor consistency-proof has no rootHash',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return { hashes: hashes as string[], rootHash };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async postEntry(body: unknown): Promise<RekorEntryResponse> {
|
|
189
|
+
let response: Response;
|
|
190
|
+
try {
|
|
191
|
+
response = await this.fetchImpl(`${this.baseUrl}/api/v1/log/entries`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
194
|
+
body: JSON.stringify(body),
|
|
195
|
+
});
|
|
196
|
+
} catch (err) {
|
|
197
|
+
throw new PassportsignError(
|
|
198
|
+
'log_submission_failed',
|
|
199
|
+
`Rekor submit request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
200
|
+
err,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
let errBody = '';
|
|
205
|
+
try { errBody = await response.text(); } catch { /* ignore */ }
|
|
206
|
+
throw new PassportsignError(
|
|
207
|
+
'log_submission_failed',
|
|
208
|
+
`Rekor submit returned ${response.status}: ${errBody}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return parseEntryResponse(await response.json().catch(() => null));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build the Rekor in-toto v0.0.2 entry submission body from a
|
|
217
|
+
* DSSE envelope. Encoding quirks pinned during the Day 5 smoke test:
|
|
218
|
+
*
|
|
219
|
+
* - `payload` and `sig` are **double-base64** at the API boundary
|
|
220
|
+
* (Rekor's go-openapi `strfmt.Base64` re-encodes the already-base64
|
|
221
|
+
* DSSE strings).
|
|
222
|
+
* - `publicKey` is **single-base64** over the PEM bytes (raw PEM text).
|
|
223
|
+
* - `keyid` is **omitted entirely** if empty — sending `""` causes the
|
|
224
|
+
* server's canonicalised entry to differ from the client's and the
|
|
225
|
+
* submission fails with "error generating canonicalized entry".
|
|
226
|
+
* - `hash` and `payloadHash` are **required despite the readOnly
|
|
227
|
+
* schema markers**. The server compares them to its own computation
|
|
228
|
+
* and rejects the submission on mismatch.
|
|
229
|
+
*
|
|
230
|
+
* @internal exported for direct testing.
|
|
231
|
+
*/
|
|
232
|
+
export function buildIntotoEntryBody(envelope: DsseEnvelope): unknown {
|
|
233
|
+
if (envelope.signatures.length === 0) {
|
|
234
|
+
throw new PassportsignError(
|
|
235
|
+
'log_submission_failed',
|
|
236
|
+
'envelope must have at least one signature',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const sig0 = envelope.signatures[0]!;
|
|
240
|
+
|
|
241
|
+
// payloadHash = sha256 of raw payload bytes.
|
|
242
|
+
const payloadHashHex = sha256Hex(base64ToBytes(envelope.payload));
|
|
243
|
+
|
|
244
|
+
// envelopeHash = sha256 of canonical JSON of {payloadType, payload-base64,
|
|
245
|
+
// signatures:[{sig-base64, publicKey: PEM-string [, keyid]}]} — note
|
|
246
|
+
// publicKey is the raw PEM string for this hash (not base64).
|
|
247
|
+
const sigForHash: Record<string, string> = {
|
|
248
|
+
sig: sig0.sig,
|
|
249
|
+
publicKey: sig0.publicKey,
|
|
250
|
+
};
|
|
251
|
+
if (sig0.keyid && sig0.keyid.length > 0) {
|
|
252
|
+
sigForHash['keyid'] = sig0.keyid;
|
|
253
|
+
}
|
|
254
|
+
const envelopeForHash = {
|
|
255
|
+
payloadType: envelope.payloadType,
|
|
256
|
+
payload: envelope.payload,
|
|
257
|
+
signatures: [sigForHash],
|
|
258
|
+
};
|
|
259
|
+
const envelopeHashHex = sha256Hex(canonicalize(envelopeForHash));
|
|
260
|
+
|
|
261
|
+
// Build the actual submission body.
|
|
262
|
+
const sigItem: Record<string, string> = {
|
|
263
|
+
sig: bytesToBase64(utf8ToBytes(sig0.sig)),
|
|
264
|
+
publicKey: bytesToBase64(utf8ToBytes(sig0.publicKey)),
|
|
265
|
+
};
|
|
266
|
+
if (sig0.keyid && sig0.keyid.length > 0) {
|
|
267
|
+
sigItem['keyid'] = sig0.keyid;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
apiVersion: '0.0.2',
|
|
272
|
+
kind: 'intoto',
|
|
273
|
+
spec: {
|
|
274
|
+
content: {
|
|
275
|
+
envelope: {
|
|
276
|
+
payloadType: envelope.payloadType,
|
|
277
|
+
payload: bytesToBase64(utf8ToBytes(envelope.payload)),
|
|
278
|
+
signatures: [sigItem],
|
|
279
|
+
},
|
|
280
|
+
hash: { algorithm: 'sha256', value: envelopeHashHex },
|
|
281
|
+
payloadHash: { algorithm: 'sha256', value: payloadHashHex },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseEntryResponse(raw: unknown): RekorEntryResponse {
|
|
288
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
289
|
+
throw new PassportsignError(
|
|
290
|
+
'log_submission_failed',
|
|
291
|
+
'malformed Rekor response (not a JSON object)',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const entries = Object.entries(raw as Record<string, unknown>);
|
|
295
|
+
if (entries.length !== 1) {
|
|
296
|
+
throw new PassportsignError(
|
|
297
|
+
'log_submission_failed',
|
|
298
|
+
`expected exactly one UUID in Rekor response, got ${entries.length}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const [uuid, entryRaw] = entries[0]!;
|
|
302
|
+
const entry = entryRaw as Record<string, unknown>;
|
|
303
|
+
const verification = entry['verification'] as Record<string, unknown> | undefined;
|
|
304
|
+
if (!verification) {
|
|
305
|
+
throw new PassportsignError(
|
|
306
|
+
'log_submission_failed',
|
|
307
|
+
'Rekor response missing verification block',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const inclusionProof = verification['inclusionProof'] as InclusionProof | undefined;
|
|
311
|
+
const signedEntryTimestamp = verification['signedEntryTimestamp'] as string | undefined;
|
|
312
|
+
if (!inclusionProof || typeof signedEntryTimestamp !== 'string') {
|
|
313
|
+
throw new PassportsignError(
|
|
314
|
+
'log_submission_failed',
|
|
315
|
+
'Rekor response missing inclusionProof or signedEntryTimestamp',
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
uuid,
|
|
321
|
+
logIndex: entry['logIndex'] as number,
|
|
322
|
+
integratedTime: entry['integratedTime'] as number,
|
|
323
|
+
logID: entry['logID'] as string,
|
|
324
|
+
body: entry['body'] as string,
|
|
325
|
+
...(entry['attestation']
|
|
326
|
+
? { attestation: entry['attestation'] as { data?: string } }
|
|
327
|
+
: {}),
|
|
328
|
+
verification: { inclusionProof, signedEntryTimestamp },
|
|
329
|
+
};
|
|
330
|
+
}
|