@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/bind.ts
CHANGED
|
@@ -1,137 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bind-flow orchestrator (no Rekor yet).
|
|
3
|
-
*
|
|
4
|
-
* Composes the pieces from `github.ts`, `statement.ts`, and `canonical.ts`
|
|
5
|
-
* into a single "given these inputs, produce a ready-to-submit binding"
|
|
6
|
-
* function. Day 5 will chain this with the Rekor submission and bundle
|
|
7
|
-
* write to deliver the full `passportsign bind` CLI command.
|
|
8
|
-
*
|
|
9
|
-
* Deliberately does **not** call the zkPassport SDK directly — the proof
|
|
10
|
-
* blob and SDK-derived metadata come in as plain data. The CLI's bind
|
|
11
|
-
* command is the producer of that data (it drives the SDK + UI),
|
|
12
|
-
* keeping this module pure and unit-testable without the SDK.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
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
|
-
github
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
return {
|
|
130
|
-
statement,
|
|
131
|
-
statement_canonical,
|
|
132
|
-
statement_sha256_hex,
|
|
133
|
-
proof_blob_b64: input.proof_blob_b64,
|
|
134
|
-
proof_blob_sha256_hex,
|
|
135
|
-
gist,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Bind-flow orchestrator (no Rekor yet).
|
|
3
|
+
*
|
|
4
|
+
* Composes the pieces from `github.ts`, `statement.ts`, and `canonical.ts`
|
|
5
|
+
* into a single "given these inputs, produce a ready-to-submit binding"
|
|
6
|
+
* function. Day 5 will chain this with the Rekor submission and bundle
|
|
7
|
+
* write to deliver the full `passportsign bind` CLI command.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately does **not** call the zkPassport SDK directly — the proof
|
|
10
|
+
* blob and SDK-derived metadata come in as plain data. The CLI's bind
|
|
11
|
+
* command is the producer of that data (it drives the SDK + UI),
|
|
12
|
+
* keeping this module pure and unit-testable without the SDK.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { canonicalize, canonicalSha256Hex } from './canonical.js';
|
|
16
|
+
import { base64ToBytes, sha256Hex } from './encoding.js';
|
|
17
|
+
import { PassportsignError } from './errors.js';
|
|
18
|
+
import { checkGistControl, type GistEvidence } from './github.js';
|
|
19
|
+
import { buildStatement, type PassportsignStatement } from './statement.js';
|
|
20
|
+
|
|
21
|
+
export interface PrepareBindingInput {
|
|
22
|
+
github_username: string;
|
|
23
|
+
/** Base64-encoded zkPassport proof blob (the SDK callback's serialized output). */
|
|
24
|
+
proof_blob_b64: string;
|
|
25
|
+
/** From the SDK's `onResult` callback. Deterministic for this passport + scope. */
|
|
26
|
+
unique_identifier: string;
|
|
27
|
+
/** ICAO 3-letter code if disclosed, else null. Pass through as-returned by SDK. */
|
|
28
|
+
issuing_country: string | null;
|
|
29
|
+
/** Per-binding nonce that was placed in the user's gist for the control check. */
|
|
30
|
+
nonce: string;
|
|
31
|
+
/** Full scope string (e.g. "passportsign.dev:nationality-disclose:1"). */
|
|
32
|
+
scope: string;
|
|
33
|
+
/** Version string from the zkPassport SDK that produced the proof. */
|
|
34
|
+
zkpassport_sdk_version: string;
|
|
35
|
+
/** Optional GitHub token for the gist check (rate limits only — no special access). */
|
|
36
|
+
github_token?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PrepareBindingInit {
|
|
40
|
+
/** Init timestamp — gist `updated_at` must be on or after this. */
|
|
41
|
+
issuedAt: Date;
|
|
42
|
+
/** Filename to look for in the user's gists. Defaults to `passportsign.txt`. */
|
|
43
|
+
gistFilename?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PrepareBindingDeps {
|
|
47
|
+
/** Inject for tests. Defaults to {@link checkGistControl}. */
|
|
48
|
+
github?: typeof checkGistControl;
|
|
49
|
+
/** Inject a fetch (forwarded to github). */
|
|
50
|
+
fetch?: typeof fetch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PreparedBinding {
|
|
54
|
+
statement: PassportsignStatement;
|
|
55
|
+
statement_canonical: Uint8Array;
|
|
56
|
+
statement_sha256_hex: string;
|
|
57
|
+
proof_blob_b64: string;
|
|
58
|
+
proof_blob_sha256_hex: string;
|
|
59
|
+
gist: GistEvidence;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_GIST_FILENAME = 'passportsign.txt';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run the GitHub gist control check, then build the in-toto statement and
|
|
66
|
+
* compute canonical bytes + hashes for the Rekor handoff.
|
|
67
|
+
*
|
|
68
|
+
* Throws {@link PassportsignError} with the matching §4 code on any
|
|
69
|
+
* failure path.
|
|
70
|
+
*/
|
|
71
|
+
export async function prepareBinding(
|
|
72
|
+
input: PrepareBindingInput,
|
|
73
|
+
init: PrepareBindingInit,
|
|
74
|
+
deps: PrepareBindingDeps = {},
|
|
75
|
+
): Promise<PreparedBinding> {
|
|
76
|
+
const githubImpl = deps.github ?? checkGistControl;
|
|
77
|
+
|
|
78
|
+
// 1. GitHub gist control check (throws PassportsignError on any §4 path).
|
|
79
|
+
const gist = await githubImpl({
|
|
80
|
+
username: input.github_username,
|
|
81
|
+
expected_filename: init.gistFilename ?? DEFAULT_GIST_FILENAME,
|
|
82
|
+
expected_content: input.nonce,
|
|
83
|
+
not_before: init.issuedAt,
|
|
84
|
+
...(input.github_token ? { token: input.github_token } : {}),
|
|
85
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 2. Derive proof_blob sha256 from the base64 input.
|
|
89
|
+
let proofBytes: Uint8Array;
|
|
90
|
+
try {
|
|
91
|
+
proofBytes = base64ToBytes(input.proof_blob_b64);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new PassportsignError(
|
|
94
|
+
'proof_invalid',
|
|
95
|
+
`proof_blob_b64 is not valid base64`,
|
|
96
|
+
err,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (proofBytes.length === 0) {
|
|
100
|
+
throw new PassportsignError('proof_invalid', 'proof_blob_b64 decoded to zero bytes');
|
|
101
|
+
}
|
|
102
|
+
const proof_blob_sha256_hex = sha256Hex(proofBytes);
|
|
103
|
+
|
|
104
|
+
// 3. Build the in-toto statement (enforces hex / non-empty invariants).
|
|
105
|
+
const statement = buildStatement({
|
|
106
|
+
github_username: input.github_username,
|
|
107
|
+
unique_identifier: input.unique_identifier,
|
|
108
|
+
issuing_country: input.issuing_country,
|
|
109
|
+
proof_blob_sha256: proof_blob_sha256_hex,
|
|
110
|
+
gist_url: gist.url,
|
|
111
|
+
gist_content_sha256: gist.content_sha256,
|
|
112
|
+
scope: input.scope,
|
|
113
|
+
zkpassport_sdk_version: input.zkpassport_sdk_version,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 4. Canonical bytes + sha256 for the Rekor entry.
|
|
117
|
+
const statement_canonical = canonicalize(statement);
|
|
118
|
+
const statement_sha256_hex = canonicalSha256Hex(statement);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
statement,
|
|
122
|
+
statement_canonical,
|
|
123
|
+
statement_sha256_hex,
|
|
124
|
+
proof_blob_b64: input.proof_blob_b64,
|
|
125
|
+
proof_blob_sha256_hex,
|
|
126
|
+
gist,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/bundle-fs.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only file I/O for `binding.passportsign.json` bundles. Kept out
|
|
3
|
+
* of `bundle.ts` so the validation logic stays runtime-neutral (the
|
|
4
|
+
* `./web` subpath exports validation but not these).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
BundleValidationError,
|
|
11
|
+
validateBundle,
|
|
12
|
+
type PassportsignBundle,
|
|
13
|
+
} from './bundle.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read and validate a `binding.passportsign.json` file. Throws on
|
|
17
|
+
* invalid JSON or schema violations.
|
|
18
|
+
*/
|
|
19
|
+
export function readBundle(path: string): PassportsignBundle {
|
|
20
|
+
const raw = readFileSync(path, 'utf8');
|
|
21
|
+
let parsed: unknown;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new BundleValidationError(
|
|
26
|
+
'$',
|
|
27
|
+
`invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
validateBundle(parsed);
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate and write a `binding.passportsign.json` file (pretty-printed).
|
|
36
|
+
*/
|
|
37
|
+
export function writeBundle(path: string, bundle: PassportsignBundle): void {
|
|
38
|
+
validateBundle(bundle);
|
|
39
|
+
writeFileSync(path, JSON.stringify(bundle, null, 2) + '\n', 'utf8');
|
|
40
|
+
}
|
package/src/bundle.ts
CHANGED
|
@@ -1,127 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `binding.passportsign.json` bundle format — the portable unit of
|
|
3
|
-
* verification.
|
|
4
|
-
*
|
|
5
|
-
* Rekor stores hashes, not artifacts. To verify a binding, a third party
|
|
6
|
-
* needs both the Rekor entry (hash + inclusion proof) and the artifacts
|
|
7
|
-
* that were hashed. The bundle carries both: the canonical statement bytes
|
|
8
|
-
* (hex), the proof blob (base64), and the Rekor metadata.
|
|
9
|
-
*
|
|
10
|
-
* Shape follows the Sigstore verification-bundle pattern. The
|
|
11
|
-
* `rekor.inclusion_proof` field is intentionally `unknown` for now — its
|
|
12
|
-
* shape gets pinned in Day 5 after we've smoke-tested the public Sigstore
|
|
13
|
-
* Rekor response format.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
statement
|
|
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
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `binding.passportsign.json` bundle format — the portable unit of
|
|
3
|
+
* verification.
|
|
4
|
+
*
|
|
5
|
+
* Rekor stores hashes, not artifacts. To verify a binding, a third party
|
|
6
|
+
* needs both the Rekor entry (hash + inclusion proof) and the artifacts
|
|
7
|
+
* that were hashed. The bundle carries both: the canonical statement bytes
|
|
8
|
+
* (hex), the proof blob (base64), and the Rekor metadata.
|
|
9
|
+
*
|
|
10
|
+
* Shape follows the Sigstore verification-bundle pattern. The
|
|
11
|
+
* `rekor.inclusion_proof` field is intentionally `unknown` for now — its
|
|
12
|
+
* shape gets pinned in Day 5 after we've smoke-tested the public Sigstore
|
|
13
|
+
* Rekor response format.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { bytesToHex } from './encoding.js';
|
|
17
|
+
import { type RekorEntryResponse } from './log/rekor.js';
|
|
18
|
+
|
|
19
|
+
export const BUNDLE_FORMAT_VERSION = 1 as const;
|
|
20
|
+
|
|
21
|
+
export interface RekorBundleFields {
|
|
22
|
+
log_entry_hash: string;
|
|
23
|
+
inclusion_proof: unknown;
|
|
24
|
+
log_root_at_submission: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PassportsignBundle {
|
|
28
|
+
bundle_format_version: typeof BUNDLE_FORMAT_VERSION;
|
|
29
|
+
/** Hex-encoded canonical JCS bytes of the in-toto statement. */
|
|
30
|
+
statement: string;
|
|
31
|
+
/** Base64-encoded zkPassport proof blob. */
|
|
32
|
+
proof_blob: string;
|
|
33
|
+
rekor: RekorBundleFields;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const HEX_EVEN = /^(?:[0-9a-f]{2})+$/;
|
|
37
|
+
// Standard base64: A-Z, a-z, 0-9, +, /, with 0-2 trailing '=' for padding.
|
|
38
|
+
// Length must be multiple of 4.
|
|
39
|
+
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
40
|
+
|
|
41
|
+
export class BundleValidationError extends Error {
|
|
42
|
+
constructor(
|
|
43
|
+
readonly path: string,
|
|
44
|
+
message: string,
|
|
45
|
+
) {
|
|
46
|
+
super(`${path}: ${message}`);
|
|
47
|
+
this.name = 'BundleValidationError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(path: string, message: string): never {
|
|
52
|
+
throw new BundleValidationError(path, message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
56
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Type-guard validator for `PassportsignBundle`. Throws
|
|
61
|
+
* `BundleValidationError` with a structured path on the first issue.
|
|
62
|
+
*/
|
|
63
|
+
export function validateBundle(value: unknown): asserts value is PassportsignBundle {
|
|
64
|
+
if (!isObject(value)) fail('$', 'bundle must be a JSON object');
|
|
65
|
+
|
|
66
|
+
if (value['bundle_format_version'] !== BUNDLE_FORMAT_VERSION) {
|
|
67
|
+
fail(
|
|
68
|
+
'$.bundle_format_version',
|
|
69
|
+
`expected ${BUNDLE_FORMAT_VERSION}, got ${JSON.stringify(value['bundle_format_version'])}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const statement = value['statement'];
|
|
74
|
+
if (typeof statement !== 'string') fail('$.statement', 'must be a string');
|
|
75
|
+
if (!HEX_EVEN.test(statement)) {
|
|
76
|
+
fail('$.statement', 'must be lowercase even-length hex (canonical JCS bytes)');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const proofBlob = value['proof_blob'];
|
|
80
|
+
if (typeof proofBlob !== 'string') fail('$.proof_blob', 'must be a string');
|
|
81
|
+
if (!BASE64.test(proofBlob)) {
|
|
82
|
+
fail('$.proof_blob', 'must be standard base64 (A-Z, a-z, 0-9, +, /, = padding)');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rekor = value['rekor'];
|
|
86
|
+
if (!isObject(rekor)) fail('$.rekor', 'must be an object');
|
|
87
|
+
|
|
88
|
+
const logEntryHash = rekor['log_entry_hash'];
|
|
89
|
+
if (typeof logEntryHash !== 'string' || logEntryHash.length === 0) {
|
|
90
|
+
fail('$.rekor.log_entry_hash', 'must be a non-empty string');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!('inclusion_proof' in rekor)) {
|
|
94
|
+
fail('$.rekor.inclusion_proof', 'is required (shape pinned in Day 5)');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const logRootAtSubmission = rekor['log_root_at_submission'];
|
|
98
|
+
if (typeof logRootAtSubmission !== 'string' || logRootAtSubmission.length === 0) {
|
|
99
|
+
fail('$.rekor.log_root_at_submission', 'must be a non-empty string');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// readBundle / writeBundle live in `bundle-fs.ts` (node:fs) so this
|
|
104
|
+
// module stays runtime-neutral; the main index re-exports both.
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What bundle assembly needs from a prepared statement — satisfied by
|
|
108
|
+
* both `PreparedBinding` and `PreparedRevocation`. The statement kind
|
|
109
|
+
* doesn't matter; Rekor sees canonical bytes either way.
|
|
110
|
+
*/
|
|
111
|
+
export interface SubmittableStatement {
|
|
112
|
+
statement_canonical: Uint8Array;
|
|
113
|
+
proof_blob_b64: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Assemble (and validate) the portable bundle from a prepared
|
|
118
|
+
* statement and the Rekor entry it produced. Pure — used by the node
|
|
119
|
+
* `submitBinding` path and by runtimes that sign with
|
|
120
|
+
* `signEnvelopeWeb` and submit through the Rekor client themselves.
|
|
121
|
+
*/
|
|
122
|
+
export function assembleBundle(
|
|
123
|
+
prepared: SubmittableStatement,
|
|
124
|
+
rekorEntry: RekorEntryResponse,
|
|
125
|
+
): PassportsignBundle {
|
|
126
|
+
const bundle: PassportsignBundle = {
|
|
127
|
+
bundle_format_version: BUNDLE_FORMAT_VERSION,
|
|
128
|
+
statement: bytesToHex(prepared.statement_canonical),
|
|
129
|
+
proof_blob: prepared.proof_blob_b64,
|
|
130
|
+
rekor: {
|
|
131
|
+
log_entry_hash: rekorEntry.uuid,
|
|
132
|
+
inclusion_proof: rekorEntry.verification.inclusionProof,
|
|
133
|
+
log_root_at_submission: rekorEntry.verification.inclusionProof.rootHash,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
validateBundle(bundle);
|
|
137
|
+
return bundle;
|
|
138
|
+
}
|
package/src/canonical.ts
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import canonify from '@truestamp/canonify';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
}
|
|
1
|
+
import canonify from '@truestamp/canonify';
|
|
2
|
+
|
|
3
|
+
import { sha256Hex, utf8ToBytes } from './encoding.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RFC 8785 JCS-canonical UTF-8 bytes for a JSON-serializable value.
|
|
7
|
+
*
|
|
8
|
+
* Wraps `@truestamp/canonify` (pinned at exact 1.0.3) and UTF-8 encodes the
|
|
9
|
+
* resulting string. The fixture-pinned drift test in
|
|
10
|
+
* `test/canonical.test.ts` guards against silent behavior changes in the
|
|
11
|
+
* underlying library — JCS implementations have had subtle bugs and this
|
|
12
|
+
* function's output is the most security-critical artifact in the repo.
|
|
13
|
+
*
|
|
14
|
+
* Throws `TypeError` if the value cannot be canonicalized (e.g. undefined,
|
|
15
|
+
* cycles, non-JSON-serializable types).
|
|
16
|
+
*/
|
|
17
|
+
export function canonicalize(value: unknown): Uint8Array {
|
|
18
|
+
const canonical = canonify(value);
|
|
19
|
+
if (canonical === undefined) {
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
'canonicalize: value cannot be JCS-canonicalized (undefined / cycle / non-JSON)',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return utf8ToBytes(canonical);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lowercase-hex SHA-256 of `canonicalize(value)`. Used to derive the
|
|
29
|
+
* Rekor entry hash for the in-toto statement.
|
|
30
|
+
*/
|
|
31
|
+
export function canonicalSha256Hex(value: unknown): string {
|
|
32
|
+
return sha256Hex(canonicalize(value));
|
|
33
|
+
}
|