@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.
Files changed (106) hide show
  1. package/dist/badge.d.ts +5 -0
  2. package/dist/badge.d.ts.map +1 -1
  3. package/dist/badge.js +8 -2
  4. package/dist/badge.js.map +1 -1
  5. package/dist/bind.d.ts.map +1 -1
  6. package/dist/bind.js +2 -8
  7. package/dist/bind.js.map +1 -1
  8. package/dist/bundle-fs.d.ts +16 -0
  9. package/dist/bundle-fs.d.ts.map +1 -0
  10. package/dist/bundle-fs.js +31 -0
  11. package/dist/bundle-fs.js.map +1 -0
  12. package/dist/bundle.d.ts +13 -5
  13. package/dist/bundle.d.ts.map +1 -1
  14. package/dist/bundle.js +18 -20
  15. package/dist/bundle.js.map +1 -1
  16. package/dist/canonical.d.ts.map +1 -1
  17. package/dist/canonical.js +3 -4
  18. package/dist/canonical.js.map +1 -1
  19. package/dist/classify.d.ts +68 -0
  20. package/dist/classify.d.ts.map +1 -0
  21. package/dist/classify.js +117 -0
  22. package/dist/classify.js.map +1 -0
  23. package/dist/dsse-common.d.ts +32 -0
  24. package/dist/dsse-common.d.ts.map +1 -0
  25. package/dist/dsse-common.js +26 -0
  26. package/dist/dsse-common.js.map +1 -0
  27. package/dist/dsse-web.d.ts +28 -0
  28. package/dist/dsse-web.d.ts.map +1 -0
  29. package/dist/dsse-web.js +81 -0
  30. package/dist/dsse-web.js.map +1 -0
  31. package/dist/dsse.d.ts +2 -26
  32. package/dist/dsse.d.ts.map +1 -1
  33. package/dist/dsse.js +2 -19
  34. package/dist/dsse.js.map +1 -1
  35. package/dist/encoding.d.ts +20 -0
  36. package/dist/encoding.d.ts.map +1 -0
  37. package/dist/encoding.js +88 -0
  38. package/dist/encoding.js.map +1 -0
  39. package/dist/github.js +2 -2
  40. package/dist/github.js.map +1 -1
  41. package/dist/index.d.ts +9 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/log/rekor.d.ts +1 -1
  46. package/dist/log/rekor.d.ts.map +1 -1
  47. package/dist/log/rekor.js +7 -10
  48. package/dist/log/rekor.js.map +1 -1
  49. package/dist/lookup.d.ts +46 -0
  50. package/dist/lookup.d.ts.map +1 -0
  51. package/dist/lookup.js +101 -0
  52. package/dist/lookup.js.map +1 -0
  53. package/dist/merkle.js +3 -3
  54. package/dist/merkle.js.map +1 -1
  55. package/dist/nonce.js +1 -1
  56. package/dist/nonce.js.map +1 -1
  57. package/dist/profile-index.d.ts +64 -0
  58. package/dist/profile-index.d.ts.map +1 -0
  59. package/dist/profile-index.js +161 -0
  60. package/dist/profile-index.js.map +1 -0
  61. package/dist/revoke.d.ts +30 -0
  62. package/dist/revoke.d.ts.map +1 -0
  63. package/dist/revoke.js +42 -0
  64. package/dist/revoke.js.map +1 -0
  65. package/dist/sdk-payload.d.ts.map +1 -1
  66. package/dist/sdk-payload.js +4 -6
  67. package/dist/sdk-payload.js.map +1 -1
  68. package/dist/statement.d.ts +41 -0
  69. package/dist/statement.d.ts.map +1 -1
  70. package/dist/statement.js +43 -0
  71. package/dist/statement.js.map +1 -1
  72. package/dist/submit.d.ts +3 -3
  73. package/dist/submit.d.ts.map +1 -1
  74. package/dist/submit.js +3 -14
  75. package/dist/submit.js.map +1 -1
  76. package/dist/verifier.d.ts.map +1 -1
  77. package/dist/verifier.js +4 -14
  78. package/dist/verifier.js.map +1 -1
  79. package/dist/web.d.ts +35 -0
  80. package/dist/web.d.ts.map +1 -0
  81. package/dist/web.js +35 -0
  82. package/dist/web.js.map +1 -0
  83. package/package.json +6 -2
  84. package/src/badge.ts +124 -113
  85. package/src/bind.ts +128 -137
  86. package/src/bundle-fs.ts +40 -0
  87. package/src/bundle.ts +138 -127
  88. package/src/canonical.ts +33 -33
  89. package/src/classify.ts +165 -0
  90. package/src/dsse-common.ts +45 -0
  91. package/src/dsse-web.ts +97 -0
  92. package/src/dsse.ts +63 -91
  93. package/src/encoding.ts +96 -0
  94. package/src/github.ts +196 -196
  95. package/src/index.ts +59 -2
  96. package/src/log/rekor.ts +330 -334
  97. package/src/lookup.ts +175 -0
  98. package/src/merkle.ts +187 -187
  99. package/src/nonce.ts +53 -53
  100. package/src/profile-index.ts +222 -0
  101. package/src/revoke.ts +67 -0
  102. package/src/sdk-payload.ts +60 -62
  103. package/src/statement.ts +203 -119
  104. package/src/submit.ts +38 -54
  105. package/src/verifier.ts +304 -317
  106. package/src/web.ts +175 -0
package/src/statement.ts CHANGED
@@ -1,119 +1,203 @@
1
- /**
2
- * in-toto Statement v1 builder for passportsign attestations.
3
- *
4
- * The statement's canonical JCS bytes are what gets hashed into the
5
- * Rekor entry, so this module is the authoritative source for the
6
- * statement shape. Test vectors in
7
- * `test/fixtures/canonical-vectors.json` pin the canonicalization
8
- * output for representative statements built here.
9
- */
10
-
11
- export const IN_TOTO_STATEMENT_TYPE = 'https://in-toto.io/Statement/v1' as const;
12
- export const PASSPORTSIGN_PREDICATE_TYPE =
13
- 'https://passportsign.dev/personhood/v1' as const;
14
-
15
- export type DisclosureLevel = 'personhood' | 'personhood+country';
16
-
17
- export interface PassportsignPredicate {
18
- /** From the zkPassport SDK — deterministic for (passport, domain, scope). */
19
- unique_identifier: string;
20
- /** ICAO 3-letter code if disclosed, else null. Pass through as-returned by SDK. */
21
- issuing_country: string | null;
22
- /** Derived from issuing_country (null personhood, set → personhood+country). */
23
- disclosure_level: DisclosureLevel;
24
- /** Lowercase hex SHA-256 of the proof blob bytes. */
25
- proof_blob_sha256: string;
26
- /** Public gist URL captured at binding time. */
27
- gist_url: string;
28
- /** Lowercase hex SHA-256 of the gist's content bytes. Also the subject digest. */
29
- gist_content_sha256: string;
30
- /** zkPassport scope (e.g. "passportsign.dev:nationality-disclose:1"). */
31
- scope: string;
32
- /** Version string from the zkPassport SDK that produced the proof. */
33
- zkpassport_sdk_version: string;
34
- }
35
-
36
- export interface PassportsignStatement {
37
- _type: typeof IN_TOTO_STATEMENT_TYPE;
38
- subject: Array<{
39
- name: string;
40
- digest: { sha256: string };
41
- }>;
42
- predicateType: typeof PASSPORTSIGN_PREDICATE_TYPE;
43
- predicate: PassportsignPredicate;
44
- }
45
-
46
- export interface BuildStatementInput {
47
- github_username: string;
48
- unique_identifier: string;
49
- issuing_country: string | null;
50
- proof_blob_sha256: string;
51
- gist_url: string;
52
- gist_content_sha256: string;
53
- scope: string;
54
- zkpassport_sdk_version: string;
55
- }
56
-
57
- const SHA256_HEX = /^[0-9a-f]{64}$/;
58
-
59
- function assertSha256Hex(value: string, field: string): void {
60
- if (!SHA256_HEX.test(value)) {
61
- throw new TypeError(
62
- `${field}: expected lowercase 64-char hex SHA-256, got ${JSON.stringify(value)}`,
63
- );
64
- }
65
- }
66
-
67
- function assertNonEmpty(value: string, field: string): void {
68
- if (value.length === 0) {
69
- throw new TypeError(`${field}: must be non-empty`);
70
- }
71
- }
72
-
73
- /**
74
- * Build a passportsign in-toto Statement v1.
75
- *
76
- * Invariants enforced here (so the canonical bytes are always well-formed):
77
- * - `proof_blob_sha256` and `gist_content_sha256` are lowercase 64-char hex.
78
- * - `github_username`, `unique_identifier`, `gist_url`, `scope`,
79
- * `zkpassport_sdk_version` are non-empty.
80
- * - `subject[0].digest.sha256 === gist_content_sha256` — the subject digest
81
- * is the artifact whose control was demonstrated (the gist content).
82
- * - `disclosure_level` is derived from `issuing_country` and is never
83
- * accepted from the caller.
84
- * - There is no `bound_at` field — the Rekor inclusion timestamp is the
85
- * authoritative time of binding.
86
- */
87
- export function buildStatement(input: BuildStatementInput): PassportsignStatement {
88
- assertSha256Hex(input.proof_blob_sha256, 'proof_blob_sha256');
89
- assertSha256Hex(input.gist_content_sha256, 'gist_content_sha256');
90
- assertNonEmpty(input.github_username, 'github_username');
91
- assertNonEmpty(input.unique_identifier, 'unique_identifier');
92
- assertNonEmpty(input.gist_url, 'gist_url');
93
- assertNonEmpty(input.scope, 'scope');
94
- assertNonEmpty(input.zkpassport_sdk_version, 'zkpassport_sdk_version');
95
-
96
- const disclosure_level: DisclosureLevel =
97
- input.issuing_country === null ? 'personhood' : 'personhood+country';
98
-
99
- return {
100
- _type: IN_TOTO_STATEMENT_TYPE,
101
- subject: [
102
- {
103
- name: `github.com/${input.github_username}`,
104
- digest: { sha256: input.gist_content_sha256 },
105
- },
106
- ],
107
- predicateType: PASSPORTSIGN_PREDICATE_TYPE,
108
- predicate: {
109
- unique_identifier: input.unique_identifier,
110
- issuing_country: input.issuing_country,
111
- disclosure_level,
112
- proof_blob_sha256: input.proof_blob_sha256,
113
- gist_url: input.gist_url,
114
- gist_content_sha256: input.gist_content_sha256,
115
- scope: input.scope,
116
- zkpassport_sdk_version: input.zkpassport_sdk_version,
117
- },
118
- };
119
- }
1
+ /**
2
+ * in-toto Statement v1 builder for passportsign attestations.
3
+ *
4
+ * The statement's canonical JCS bytes are what gets hashed into the
5
+ * Rekor entry, so this module is the authoritative source for the
6
+ * statement shape. Test vectors in
7
+ * `test/fixtures/canonical-vectors.json` pin the canonicalization
8
+ * output for representative statements built here.
9
+ */
10
+
11
+ import { sha256Hex, utf8ToBytes } from './encoding.js';
12
+
13
+ export const IN_TOTO_STATEMENT_TYPE = 'https://in-toto.io/Statement/v1' as const;
14
+ export const PASSPORTSIGN_PREDICATE_TYPE =
15
+ 'https://passportsign.dev/personhood/v1' as const;
16
+ export const PASSPORTSIGN_REVOCATION_PREDICATE_TYPE =
17
+ 'https://passportsign.dev/personhood/v1#revocation' as const;
18
+
19
+ export type DisclosureLevel = 'personhood' | 'personhood+country';
20
+
21
+ export interface PassportsignPredicate {
22
+ /** From the zkPassport SDK deterministic for (passport, domain, scope). */
23
+ unique_identifier: string;
24
+ /** ICAO 3-letter code if disclosed, else null. Pass through as-returned by SDK. */
25
+ issuing_country: string | null;
26
+ /** Derived from issuing_country (null personhood, set → personhood+country). */
27
+ disclosure_level: DisclosureLevel;
28
+ /** Lowercase hex SHA-256 of the proof blob bytes. */
29
+ proof_blob_sha256: string;
30
+ /** Public gist URL captured at binding time. */
31
+ gist_url: string;
32
+ /** Lowercase hex SHA-256 of the gist's content bytes. Also the subject digest. */
33
+ gist_content_sha256: string;
34
+ /** zkPassport scope (e.g. "passportsign.dev:nationality-disclose:1"). */
35
+ scope: string;
36
+ /** Version string from the zkPassport SDK that produced the proof. */
37
+ zkpassport_sdk_version: string;
38
+ }
39
+
40
+ export interface PassportsignStatement {
41
+ _type: typeof IN_TOTO_STATEMENT_TYPE;
42
+ subject: Array<{
43
+ name: string;
44
+ digest: { sha256: string };
45
+ }>;
46
+ predicateType: typeof PASSPORTSIGN_PREDICATE_TYPE;
47
+ predicate: PassportsignPredicate;
48
+ }
49
+
50
+ export interface BuildStatementInput {
51
+ github_username: string;
52
+ unique_identifier: string;
53
+ issuing_country: string | null;
54
+ proof_blob_sha256: string;
55
+ gist_url: string;
56
+ gist_content_sha256: string;
57
+ scope: string;
58
+ zkpassport_sdk_version: string;
59
+ }
60
+
61
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
62
+ const REKOR_UUID = /^[0-9a-f]{80}$/;
63
+
64
+ function assertSha256Hex(value: string, field: string): void {
65
+ if (!SHA256_HEX.test(value)) {
66
+ throw new TypeError(
67
+ `${field}: expected lowercase 64-char hex SHA-256, got ${JSON.stringify(value)}`,
68
+ );
69
+ }
70
+ }
71
+
72
+ function assertRekorUuid(value: string, field: string): void {
73
+ if (!REKOR_UUID.test(value)) {
74
+ throw new TypeError(
75
+ `${field}: expected 80-char lowercase hex Rekor entry UUID, got ${JSON.stringify(value)}`,
76
+ );
77
+ }
78
+ }
79
+
80
+ function assertNonEmpty(value: string, field: string): void {
81
+ if (value.length === 0) {
82
+ throw new TypeError(`${field}: must be non-empty`);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Build a passportsign in-toto Statement v1.
88
+ *
89
+ * Invariants enforced here (so the canonical bytes are always well-formed):
90
+ * - `proof_blob_sha256` and `gist_content_sha256` are lowercase 64-char hex.
91
+ * - `github_username`, `unique_identifier`, `gist_url`, `scope`,
92
+ * `zkpassport_sdk_version` are non-empty.
93
+ * - `subject[0].digest.sha256 === gist_content_sha256` — the subject digest
94
+ * is the artifact whose control was demonstrated (the gist content).
95
+ * - `disclosure_level` is derived from `issuing_country` and is never
96
+ * accepted from the caller.
97
+ * - There is no `bound_at` field — the Rekor inclusion timestamp is the
98
+ * authoritative time of binding.
99
+ */
100
+ export function buildStatement(input: BuildStatementInput): PassportsignStatement {
101
+ assertSha256Hex(input.proof_blob_sha256, 'proof_blob_sha256');
102
+ assertSha256Hex(input.gist_content_sha256, 'gist_content_sha256');
103
+ assertNonEmpty(input.github_username, 'github_username');
104
+ assertNonEmpty(input.unique_identifier, 'unique_identifier');
105
+ assertNonEmpty(input.gist_url, 'gist_url');
106
+ assertNonEmpty(input.scope, 'scope');
107
+ assertNonEmpty(input.zkpassport_sdk_version, 'zkpassport_sdk_version');
108
+
109
+ const disclosure_level: DisclosureLevel =
110
+ input.issuing_country === null ? 'personhood' : 'personhood+country';
111
+
112
+ return {
113
+ _type: IN_TOTO_STATEMENT_TYPE,
114
+ subject: [
115
+ {
116
+ name: `github.com/${input.github_username}`,
117
+ digest: { sha256: input.gist_content_sha256 },
118
+ },
119
+ ],
120
+ predicateType: PASSPORTSIGN_PREDICATE_TYPE,
121
+ predicate: {
122
+ unique_identifier: input.unique_identifier,
123
+ issuing_country: input.issuing_country,
124
+ disclosure_level,
125
+ proof_blob_sha256: input.proof_blob_sha256,
126
+ gist_url: input.gist_url,
127
+ gist_content_sha256: input.gist_content_sha256,
128
+ scope: input.scope,
129
+ zkpassport_sdk_version: input.zkpassport_sdk_version,
130
+ },
131
+ };
132
+ }
133
+
134
+ export interface PassportsignRevocationPredicate {
135
+ /** Must match the revoked binding's `unique_identifier` — same passport, same scope. */
136
+ unique_identifier: string;
137
+ /** Rekor entry UUID of the binding being revoked. */
138
+ revokes_rekor_entry_hash: string;
139
+ /** Lowercase hex SHA-256 of the fresh proof blob backing this revocation. */
140
+ proof_blob_sha256: string;
141
+ /** zkPassport scope — must equal the binding scope or the identifiers won't match. */
142
+ scope: string;
143
+ zkpassport_sdk_version: string;
144
+ }
145
+
146
+ export interface PassportsignRevocationStatement {
147
+ _type: typeof IN_TOTO_STATEMENT_TYPE;
148
+ subject: Array<{
149
+ name: string;
150
+ digest: { sha256: string };
151
+ }>;
152
+ predicateType: typeof PASSPORTSIGN_REVOCATION_PREDICATE_TYPE;
153
+ predicate: PassportsignRevocationPredicate;
154
+ }
155
+
156
+ export interface BuildRevocationStatementInput {
157
+ github_username: string;
158
+ unique_identifier: string;
159
+ revokes_rekor_entry_hash: string;
160
+ proof_blob_sha256: string;
161
+ scope: string;
162
+ zkpassport_sdk_version: string;
163
+ }
164
+
165
+ /**
166
+ * Build a passportsign revocation statement (spec §7, roadmap v0.5.2).
167
+ *
168
+ * Revocation requires only a fresh proof from the same passport — there
169
+ * are deliberately no gist fields (no GitHub control needed; that's the
170
+ * recovery property). The revocation always targets one concrete
171
+ * binding entry; the subject digest is the sha256 of that entry's UUID
172
+ * string, tying the statement to the artifact it acts on.
173
+ */
174
+ export function buildRevocationStatement(
175
+ input: BuildRevocationStatementInput,
176
+ ): PassportsignRevocationStatement {
177
+ assertRekorUuid(input.revokes_rekor_entry_hash, 'revokes_rekor_entry_hash');
178
+ assertSha256Hex(input.proof_blob_sha256, 'proof_blob_sha256');
179
+ assertNonEmpty(input.github_username, 'github_username');
180
+ assertNonEmpty(input.unique_identifier, 'unique_identifier');
181
+ assertNonEmpty(input.scope, 'scope');
182
+ assertNonEmpty(input.zkpassport_sdk_version, 'zkpassport_sdk_version');
183
+
184
+ const subjectDigest = sha256Hex(utf8ToBytes(input.revokes_rekor_entry_hash));
185
+
186
+ return {
187
+ _type: IN_TOTO_STATEMENT_TYPE,
188
+ subject: [
189
+ {
190
+ name: `github.com/${input.github_username}`,
191
+ digest: { sha256: subjectDigest },
192
+ },
193
+ ],
194
+ predicateType: PASSPORTSIGN_REVOCATION_PREDICATE_TYPE,
195
+ predicate: {
196
+ unique_identifier: input.unique_identifier,
197
+ revokes_rekor_entry_hash: input.revokes_rekor_entry_hash,
198
+ proof_blob_sha256: input.proof_blob_sha256,
199
+ scope: input.scope,
200
+ zkpassport_sdk_version: input.zkpassport_sdk_version,
201
+ },
202
+ };
203
+ }
package/src/submit.ts CHANGED
@@ -1,54 +1,38 @@
1
- /**
2
- * Submit a {@link PreparedBinding} to a Rekor log and assemble the
3
- * resulting {@link PassportsignBundle}.
4
- *
5
- * Composes the DSSE envelope step (with ephemeral ECDSA P-256 key)
6
- * with a {@link RekorClient}. Day 7 calls this to turn a real-passport
7
- * bind into a public-log entry plus a portable bundle.
8
- */
9
-
10
- import { type PreparedBinding } from './bind.js';
11
- import {
12
- BUNDLE_FORMAT_VERSION,
13
- type PassportsignBundle,
14
- validateBundle,
15
- } from './bundle.js';
16
- import { IN_TOTO_PAYLOAD_TYPE, signEnvelope } from './dsse.js';
17
- import { type RekorClient, type RekorEntryResponse } from './log/rekor.js';
18
-
19
- export interface SubmitBindingDeps {
20
- rekor: RekorClient;
21
- }
22
-
23
- export interface SubmitBindingResult {
24
- bundle: PassportsignBundle;
25
- rekorEntry: RekorEntryResponse;
26
- }
27
-
28
- /**
29
- * Sign the canonical statement bytes with an ephemeral ECDSA P-256 key,
30
- * submit the in-toto entry to Rekor, and assemble the bundle. Throws
31
- * `PassportsignError('log_submission_failed', …)` (from the client) on
32
- * any Rekor failure.
33
- */
34
- export async function submitBinding(
35
- prepared: PreparedBinding,
36
- deps: SubmitBindingDeps,
37
- ): Promise<SubmitBindingResult> {
38
- const { envelope } = signEnvelope(prepared.statement_canonical, IN_TOTO_PAYLOAD_TYPE);
39
- const rekorEntry = await deps.rekor.submitIntoto(envelope);
40
-
41
- const bundle: PassportsignBundle = {
42
- bundle_format_version: BUNDLE_FORMAT_VERSION,
43
- statement: Buffer.from(prepared.statement_canonical).toString('hex'),
44
- proof_blob: prepared.proof_blob_b64,
45
- rekor: {
46
- log_entry_hash: rekorEntry.uuid,
47
- inclusion_proof: rekorEntry.verification.inclusionProof,
48
- log_root_at_submission: rekorEntry.verification.inclusionProof.rootHash,
49
- },
50
- };
51
- validateBundle(bundle);
52
-
53
- return { bundle, rekorEntry };
54
- }
1
+ /**
2
+ * Submit a {@link PreparedBinding} to a Rekor log and assemble the
3
+ * resulting {@link PassportsignBundle}.
4
+ *
5
+ * Composes the DSSE envelope step (with ephemeral ECDSA P-256 key)
6
+ * with a {@link RekorClient}. Day 7 calls this to turn a real-passport
7
+ * bind into a public-log entry plus a portable bundle.
8
+ */
9
+
10
+ import { assembleBundle, type PassportsignBundle, type SubmittableStatement } from './bundle.js';
11
+ import { IN_TOTO_PAYLOAD_TYPE, signEnvelope } from './dsse.js';
12
+ import { type RekorClient, type RekorEntryResponse } from './log/rekor.js';
13
+
14
+ export { type SubmittableStatement } from './bundle.js';
15
+
16
+ export interface SubmitBindingDeps {
17
+ rekor: RekorClient;
18
+ }
19
+
20
+ export interface SubmitBindingResult {
21
+ bundle: PassportsignBundle;
22
+ rekorEntry: RekorEntryResponse;
23
+ }
24
+
25
+ /**
26
+ * Sign the canonical statement bytes with an ephemeral ECDSA P-256 key,
27
+ * submit the in-toto entry to Rekor, and assemble the bundle. Throws
28
+ * `PassportsignError('log_submission_failed', …)` (from the client) on
29
+ * any Rekor failure.
30
+ */
31
+ export async function submitBinding(
32
+ prepared: SubmittableStatement,
33
+ deps: SubmitBindingDeps,
34
+ ): Promise<SubmitBindingResult> {
35
+ const { envelope } = signEnvelope(prepared.statement_canonical, IN_TOTO_PAYLOAD_TYPE);
36
+ const rekorEntry = await deps.rekor.submitIntoto(envelope);
37
+ return { bundle: assembleBundle(prepared, rekorEntry), rekorEntry };
38
+ }