provenance-protocol 0.2.1 → 0.3.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/README.md +17 -5
- package/SPEC.md +74 -20
- package/package.json +5 -3
- package/schema/provenance-0.1.json +1 -1
- package/schema/provenance-0.2.json +229 -0
- package/src/canonical.js +98 -0
- package/src/cli.js +249 -151
- package/src/keygen.d.ts +15 -0
- package/src/keygen.js +22 -0
- package/src/verify.d.ts +10 -0
- package/src/verify.js +54 -8
- package/test-vectors/declarations-0.2.json +217 -0
package/README.md
CHANGED
|
@@ -30,11 +30,23 @@ const result = await verifyDeclaration(YAML.parse(fileContents), {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
result.valid // the signature verifies against the key in the file
|
|
33
|
+
result.coverage // 'declaration' (0.2) | 'identity' (0.1)
|
|
33
34
|
result.location // 'match' | 'mismatch' | 'unchecked'
|
|
34
35
|
result.trustworthy // valid AND served from the location it claims
|
|
35
36
|
result.fingerprint // SHA-256 of the key — store it to detect rotation
|
|
36
37
|
```
|
|
37
38
|
|
|
39
|
+
`coverage` matters. Under spec **0.2** the signature covers the whole
|
|
40
|
+
declaration, so deleting a constraint breaks it. Under **0.1** it covered only
|
|
41
|
+
the identity and key — the declared capabilities and constraints were *not*
|
|
42
|
+
protected, and a valid 0.1 signature says nothing about whether they were
|
|
43
|
+
edited. Sign new declarations with 0.2:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { signDeclaration } from 'provenance-protocol/keygen';
|
|
47
|
+
declaration.identity.signature = signDeclaration(privateKey, declaration);
|
|
48
|
+
```
|
|
49
|
+
|
|
38
50
|
A valid signature proves the declaration came from the holder of that private
|
|
39
51
|
key and has not been altered. It does **not** prove who that holder is, that
|
|
40
52
|
the declared capabilities are accurate, or that the declaration is current.
|
|
@@ -264,11 +276,11 @@ await fetch('https://getprovenance.dev/api/agents/revoke', {
|
|
|
264
276
|
## CLI
|
|
265
277
|
|
|
266
278
|
```bash
|
|
267
|
-
npx provenance keygen
|
|
268
|
-
npx provenance register --id provenance:github:your-org/your-agent --url https://github.com/...
|
|
269
|
-
npx provenance status provenance:github:alice/my-agent
|
|
270
|
-
npx provenance validate PROVENANCE.yml
|
|
271
|
-
npx provenance revoke --id provenance:github:your-org/your-agent
|
|
279
|
+
npx provenance-protocol keygen
|
|
280
|
+
npx provenance-protocol register --id provenance:github:your-org/your-agent --url https://github.com/...
|
|
281
|
+
npx provenance-protocol status provenance:github:alice/my-agent
|
|
282
|
+
npx provenance-protocol validate PROVENANCE.yml
|
|
283
|
+
npx provenance-protocol revoke --id provenance:github:your-org/your-agent
|
|
272
284
|
```
|
|
273
285
|
|
|
274
286
|
Full CLI reference: [getprovenance.dev/docs#cli](https://getprovenance.dev/docs#cli)
|
package/SPEC.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Provenance Protocol Specification
|
|
2
|
-
**
|
|
2
|
+
**Versions 0.1 and 0.2**
|
|
3
3
|
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -299,7 +299,7 @@ identity:
|
|
|
299
299
|
```
|
|
300
300
|
|
|
301
301
|
`public_key` is required when `identity` is present. `algorithm` is optional
|
|
302
|
-
and defaults to `ed25519`; no other value is valid
|
|
302
|
+
and defaults to `ed25519`; no other value is valid.
|
|
303
303
|
|
|
304
304
|
`signature` is optional, and its presence changes what the block means:
|
|
305
305
|
|
|
@@ -315,7 +315,35 @@ Prefer the signed form wherever the agent has a public location. A verifier
|
|
|
315
315
|
that finds a key with no signature has learned which key to challenge, and
|
|
316
316
|
nothing about whether the file has been altered.
|
|
317
317
|
|
|
318
|
-
### What gets signed
|
|
318
|
+
### What gets signed — and this differs by version
|
|
319
|
+
|
|
320
|
+
The `provenance` field selects the rule. A verifier MUST use the rule for the
|
|
321
|
+
version the declaration declares, and MUST refuse to guess for a version it does
|
|
322
|
+
not know.
|
|
323
|
+
|
|
324
|
+
#### 0.2 — the whole declaration (use this)
|
|
325
|
+
|
|
326
|
+
The signed message is the UTF-8 encoding of:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
provenance-declaration-v1:<canonical JSON of the declaration>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The canonical JSON is produced from the **parsed** declaration, with
|
|
333
|
+
`identity.signature` removed — it cannot cover itself — and with object keys
|
|
334
|
+
sorted by Unicode code point recursively and no insignificant whitespace.
|
|
335
|
+
|
|
336
|
+
Because canonicalisation applies to the parsed value rather than the file's
|
|
337
|
+
bytes, comments, indentation, quoting style and key order do not affect the
|
|
338
|
+
signature. A declaration can be reformatted without re-signing. Only JSON
|
|
339
|
+
representable values may be signed; a parser that yields dates or other
|
|
340
|
+
non-plain objects must be made to yield strings instead.
|
|
341
|
+
|
|
342
|
+
Every field is covered, so deleting a constraint or adding a capability breaks
|
|
343
|
+
the signature. `provenance_id` is not required to verify a 0.2 signature, though
|
|
344
|
+
the location check still needs it.
|
|
345
|
+
|
|
346
|
+
#### 0.1 — the identity only (legacy)
|
|
319
347
|
|
|
320
348
|
The signed message is the UTF-8 encoding of:
|
|
321
349
|
|
|
@@ -323,12 +351,18 @@ The signed message is the UTF-8 encoding of:
|
|
|
323
351
|
<provenance_id>:<public_key>
|
|
324
352
|
```
|
|
325
353
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
`provenance_id`.
|
|
354
|
+
This binds the key to the identity, so a key lifted from one declaration cannot
|
|
355
|
+
be replayed under a different `provenance_id`, and it requires `provenance_id`
|
|
356
|
+
to be present.
|
|
330
357
|
|
|
331
|
-
|
|
358
|
+
**It does not cover any other field.** A 0.1 declaration's capabilities and
|
|
359
|
+
constraints are not protected by its signature: someone with write access to the
|
|
360
|
+
location can delete a declared constraint and the signature still verifies.
|
|
361
|
+
Location and write access are the only controls there.
|
|
362
|
+
|
|
363
|
+
0.1 declarations remain readable and verifiable — nothing published stops
|
|
364
|
+
working — but new declarations SHOULD use 0.2, and a verifier SHOULD report
|
|
365
|
+
which coverage it checked so a reader is not misled about what was proven.
|
|
332
366
|
|
|
333
367
|
### How to verify
|
|
334
368
|
|
|
@@ -344,10 +378,12 @@ anyone.
|
|
|
344
378
|
|
|
345
379
|
### What verification proves — and what it does not
|
|
346
380
|
|
|
347
|
-
A valid signature proves
|
|
381
|
+
A valid signature proves the declaration was produced by the holder of that
|
|
382
|
+
private key, and:
|
|
348
383
|
|
|
349
|
-
-
|
|
350
|
-
-
|
|
384
|
+
- under **0.2**, that no field of the declaration has changed since;
|
|
385
|
+
- under **0.1**, only that `provenance_id` and `public_key` have not changed —
|
|
386
|
+
everything else is unprotected.
|
|
351
387
|
|
|
352
388
|
A valid signature does **not** prove:
|
|
353
389
|
|
|
@@ -405,19 +441,25 @@ they would re-check any other freshness signal.
|
|
|
405
441
|
|
|
406
442
|
An implementation of this specification is conformant if it:
|
|
407
443
|
|
|
408
|
-
1. Accepts every file that validates against
|
|
444
|
+
1. Accepts every file that validates against the schema for its declared
|
|
445
|
+
version — `schema/provenance-0.1.json` or `schema/provenance-0.2.json`.
|
|
409
446
|
2. Treats `provenance`, `name` and `description` as required and everything
|
|
410
447
|
else as optional.
|
|
411
448
|
3. Ignores unrecognised top-level fields rather than rejecting the file.
|
|
412
449
|
Future spec versions add fields; a `0.1` reader must not break on them.
|
|
413
450
|
4. Treats `identity.signature` as optional, and an unsigned `identity`
|
|
414
451
|
block as advertising a key rather than attesting the file.
|
|
415
|
-
5.
|
|
416
|
-
|
|
417
|
-
|
|
452
|
+
5. Applies the signing rule for the version the declaration declares, refuses
|
|
453
|
+
to guess for an unknown version, and reports which coverage it checked —
|
|
454
|
+
a reader must not be left thinking a 0.1 signature protected the
|
|
455
|
+
declared constraints.
|
|
456
|
+
6. Reproduces every signature in `test-vectors/signatures-0.1.json` and every
|
|
457
|
+
declaration in `test-vectors/declarations-0.2.json` marked `valid`, and
|
|
458
|
+
refuses every one marked `invalid`.
|
|
459
|
+
7. Treats a declaration whose `provenance_id` does not match its retrieval
|
|
418
460
|
location as unverified.
|
|
419
461
|
|
|
420
|
-
Points
|
|
462
|
+
Points 6 and 7 are what make independent implementations agree. An
|
|
421
463
|
implementation that passes the test vectors interoperates with every other
|
|
422
464
|
one that does, with no reference to any particular service.
|
|
423
465
|
|
|
@@ -442,10 +484,22 @@ monitors, attesters — are applications of the standard, not part of it.
|
|
|
442
484
|
|
|
443
485
|
## Versioning
|
|
444
486
|
|
|
445
|
-
The `provenance` field records which spec version you are using
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
487
|
+
The `provenance` field records which spec version you are using, and it is what
|
|
488
|
+
a reader uses to decide how to interpret the file.
|
|
489
|
+
|
|
490
|
+
We commit to backwards compatibility: a `0.1` file will always be readable and
|
|
491
|
+
verifiable, whatever later versions say. Fields are never removed.
|
|
492
|
+
|
|
493
|
+
Usually a new version only adds fields. Version 0.2 is the exception so far — it
|
|
494
|
+
changed what an existing field *covers*, because `identity.signature` in 0.1
|
|
495
|
+
protected only the identity and left the declared capabilities and constraints
|
|
496
|
+
unprotected, which was weaker than readers assumed. Rather than redefine 0.1
|
|
497
|
+
under declarations already published, 0.2 defines the stronger rule and the
|
|
498
|
+
`provenance` field keeps the two apart. A verifier implements both and applies
|
|
499
|
+
the one the declaration asks for.
|
|
500
|
+
|
|
501
|
+
Where a version changes the meaning of an existing field, it will always do so
|
|
502
|
+
by declaring a new version, never by reinterpreting an old one.
|
|
449
503
|
|
|
450
504
|
---
|
|
451
505
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "provenance-protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Provenance Protocol \u2014 open standard for AI agent identity \u2014 and its reference SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"README.md",
|
|
34
34
|
"SPEC.md",
|
|
35
35
|
"schema/provenance-0.1.json",
|
|
36
|
-
"test-vectors/"
|
|
36
|
+
"test-vectors/",
|
|
37
|
+
"src/canonical.js",
|
|
38
|
+
"schema/provenance-0.2.json"
|
|
37
39
|
],
|
|
38
40
|
"keywords": [
|
|
39
41
|
"ai-agent",
|
|
@@ -56,6 +58,6 @@
|
|
|
56
58
|
"node": ">=14.0.0"
|
|
57
59
|
},
|
|
58
60
|
"scripts": {
|
|
59
|
-
"test": "node test/verify.test.mjs"
|
|
61
|
+
"test": "node test/verify.test.mjs && node test/declaration-signature.test.mjs"
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
},
|
|
188
188
|
"signature": {
|
|
189
189
|
"type": "string",
|
|
190
|
-
"description": "Base64 Ed25519 signature of '<provenance_id>:<public_key>' \u2014 proves you control the private key
|
|
190
|
+
"description": "Base64 Ed25519 signature of '<provenance_id>:<public_key>' \u2014 proves you control the private key under that identity. NOTE: in 0.1 it does NOT cover the rest of the declaration, so capabilities and constraints are not protected by it; spec 0.2 signs the whole declaration. Optional: omit it when the key is published only for live challenge-response. Requires provenance_id."
|
|
191
191
|
},
|
|
192
192
|
"algorithm": {
|
|
193
193
|
"type": "string",
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://getprovenance.dev/schema/0.2.json",
|
|
4
|
+
"title": "PROVENANCE.yml",
|
|
5
|
+
"description": "Provenance Protocol v0.2 schema for AI agent identity files",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"provenance",
|
|
9
|
+
"name",
|
|
10
|
+
"description"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"provenance": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Spec version. 0.2 signs the whole declaration.",
|
|
16
|
+
"enum": [
|
|
17
|
+
"0.2"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"name": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Human-readable name for this agent",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"maxLength": 100
|
|
25
|
+
},
|
|
26
|
+
"description": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "What this agent does, in plain language",
|
|
29
|
+
"minLength": 1,
|
|
30
|
+
"maxLength": 1000
|
|
31
|
+
},
|
|
32
|
+
"version": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Semantic version of the agent",
|
|
35
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+.*$"
|
|
36
|
+
},
|
|
37
|
+
"model": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"description": "The LLM powering this agent",
|
|
40
|
+
"properties": {
|
|
41
|
+
"provider": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": [
|
|
44
|
+
"anthropic",
|
|
45
|
+
"openai",
|
|
46
|
+
"google",
|
|
47
|
+
"mistral",
|
|
48
|
+
"meta",
|
|
49
|
+
"local",
|
|
50
|
+
"other"
|
|
51
|
+
],
|
|
52
|
+
"description": "LLM provider"
|
|
53
|
+
},
|
|
54
|
+
"model_id": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Specific model ID e.g. claude-sonnet-4-5"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"additionalProperties": false
|
|
60
|
+
},
|
|
61
|
+
"capabilities": {
|
|
62
|
+
"type": "array",
|
|
63
|
+
"description": "What this agent can do \u2014 standard vocabulary or domain:custom",
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"pattern": "^[a-z][a-z0-9_-]*(:[a-z0-9_:-]+)?$"
|
|
67
|
+
},
|
|
68
|
+
"uniqueItems": true
|
|
69
|
+
},
|
|
70
|
+
"constraints": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"description": "Public commitments \u2014 what this agent will NEVER do",
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"pattern": "^no:[a-z0-9_:-]+$"
|
|
76
|
+
},
|
|
77
|
+
"uniqueItems": true
|
|
78
|
+
},
|
|
79
|
+
"delegates": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"description": "Sub-agents this orchestrator spawns \u2014 required if delegate:agents capability declared",
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"required": [
|
|
85
|
+
"provenance_id"
|
|
86
|
+
],
|
|
87
|
+
"properties": {
|
|
88
|
+
"provenance_id": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"pattern": "^provenance:(github|npm|pypi|huggingface|clawmarket):.+$"
|
|
91
|
+
},
|
|
92
|
+
"purpose": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"maxLength": 200
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"additionalProperties": false
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"skills": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"description": "External skills this agent depends on",
|
|
103
|
+
"items": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"required": [
|
|
106
|
+
"id",
|
|
107
|
+
"source"
|
|
108
|
+
],
|
|
109
|
+
"properties": {
|
|
110
|
+
"id": {
|
|
111
|
+
"type": "string"
|
|
112
|
+
},
|
|
113
|
+
"source": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"enum": [
|
|
116
|
+
"skillsmp",
|
|
117
|
+
"github",
|
|
118
|
+
"npm",
|
|
119
|
+
"custom"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"url": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"format": "uri"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"additionalProperties": false
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"runtime": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"description": "How this agent runs",
|
|
133
|
+
"properties": {
|
|
134
|
+
"type": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"enum": [
|
|
137
|
+
"task",
|
|
138
|
+
"persistent",
|
|
139
|
+
"scheduled"
|
|
140
|
+
],
|
|
141
|
+
"description": "task: runs to completion | persistent: always on | scheduled: cron-like"
|
|
142
|
+
},
|
|
143
|
+
"trigger": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"enum": [
|
|
146
|
+
"api",
|
|
147
|
+
"webhook",
|
|
148
|
+
"schedule",
|
|
149
|
+
"event"
|
|
150
|
+
],
|
|
151
|
+
"description": "What starts this agent"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"additionalProperties": false
|
|
155
|
+
},
|
|
156
|
+
"contact": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"description": "Who is responsible for this agent",
|
|
159
|
+
"properties": {
|
|
160
|
+
"name": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"minLength": 1
|
|
163
|
+
},
|
|
164
|
+
"url": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"format": "uri"
|
|
167
|
+
},
|
|
168
|
+
"email": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"format": "email"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"additionalProperties": false
|
|
174
|
+
},
|
|
175
|
+
"provenance_id": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"description": "Your Provenance identifier \u2014 links this file to your index entry",
|
|
178
|
+
"pattern": "^provenance:(github|npm|pypi|huggingface|clawmarket):.+$"
|
|
179
|
+
},
|
|
180
|
+
"identity": {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"description": "Cryptographic identity. public_key alone advertises the key an agent will use to prove control live. Adding signature makes the entire declaration tamper-evident.",
|
|
183
|
+
"properties": {
|
|
184
|
+
"public_key": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "Base64-encoded Ed25519 SPKI DER public key. Generate with: generateProvenanceKeyPair() from provenance-protocol/keygen"
|
|
187
|
+
},
|
|
188
|
+
"signature": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"description": "Base64 Ed25519 signature over 'provenance-declaration-v1:' followed by the canonical JSON of this parsed declaration with identity.signature removed. Covers every field, so editing capabilities or deleting a constraint breaks it. Generate with signDeclaration() from provenance-protocol/keygen. Optional: omit when the key is published only for live challenge-response."
|
|
191
|
+
},
|
|
192
|
+
"algorithm": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"enum": [
|
|
195
|
+
"ed25519"
|
|
196
|
+
],
|
|
197
|
+
"description": "Signing algorithm. Always ed25519."
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": [
|
|
201
|
+
"public_key"
|
|
202
|
+
],
|
|
203
|
+
"additionalProperties": false
|
|
204
|
+
},
|
|
205
|
+
"ajp": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"description": "Agent Job Protocol endpoint \u2014 enables agent-to-agent job delegation",
|
|
208
|
+
"properties": {
|
|
209
|
+
"endpoint": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"format": "uri",
|
|
212
|
+
"description": "Base URL for your AJP implementation. Must accept POST /jobs, GET /jobs/:id, POST /jobs/:id/ack"
|
|
213
|
+
},
|
|
214
|
+
"version": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"enum": [
|
|
217
|
+
"0.1"
|
|
218
|
+
],
|
|
219
|
+
"description": "AJP spec version"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"required": [
|
|
223
|
+
"endpoint"
|
|
224
|
+
],
|
|
225
|
+
"additionalProperties": false
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
"additionalProperties": false
|
|
229
|
+
}
|
package/src/canonical.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provenance-protocol — canonical form for signing declarations
|
|
3
|
+
*
|
|
4
|
+
* Spec 0.1 signed only "<provenance_id>:<public_key>", so a signature proved
|
|
5
|
+
* key control but left the rest of the declaration unprotected: a constraint
|
|
6
|
+
* could be deleted and the signature would still verify. Spec 0.2 signs the
|
|
7
|
+
* whole declaration, which needs one canonical serialisation that every
|
|
8
|
+
* implementation agrees on byte for byte.
|
|
9
|
+
*
|
|
10
|
+
* Canonicalisation applies to the PARSED value, not the file's bytes. Comments,
|
|
11
|
+
* indentation, quoting style and key order therefore do not affect the
|
|
12
|
+
* signature — a declaration can be reformatted without re-signing, which is
|
|
13
|
+
* what anyone would expect.
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - object keys sorted by Unicode code point, recursively
|
|
17
|
+
* - no insignificant whitespace
|
|
18
|
+
* - `identity.signature` removed before signing (it cannot cover itself)
|
|
19
|
+
* - only JSON-representable values; anything else throws rather than being
|
|
20
|
+
* silently coerced into an ambiguous signature
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DOMAIN = 'provenance-declaration-v1';
|
|
24
|
+
|
|
25
|
+
/** Thrown when a declaration contains something that cannot be canonicalised. */
|
|
26
|
+
export class CanonicalError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'CanonicalError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function canonicalValue(value, path = '$') {
|
|
34
|
+
if (value === null) return 'null';
|
|
35
|
+
|
|
36
|
+
const type = typeof value;
|
|
37
|
+
|
|
38
|
+
if (type === 'string') return JSON.stringify(value);
|
|
39
|
+
if (type === 'boolean') return value ? 'true' : 'false';
|
|
40
|
+
if (type === 'number') {
|
|
41
|
+
if (!Number.isFinite(value)) {
|
|
42
|
+
throw new CanonicalError(`${path}: non-finite numbers cannot be signed`);
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return `[${value.map((item, i) => canonicalValue(item, `${path}[${i}]`)).join(',')}]`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (type === 'object') {
|
|
52
|
+
// A YAML parser may produce Dates, Maps, Buffers and so on. Signing those
|
|
53
|
+
// would depend on whichever serialisation happened to be used, so refuse.
|
|
54
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
55
|
+
throw new CanonicalError(
|
|
56
|
+
`${path}: only plain objects can be signed (got ${value.constructor?.name ?? 'unknown type'}) — quote the value as a string`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
|
|
60
|
+
const parts = keys.map(
|
|
61
|
+
(k) => `${JSON.stringify(k)}:${canonicalValue(value[k], `${path}.${k}`)}`
|
|
62
|
+
);
|
|
63
|
+
return `{${parts.join(',')}}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new CanonicalError(`${path}: values of type ${type} cannot be signed`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The exact string a spec-0.2 declaration signature is computed over.
|
|
71
|
+
*
|
|
72
|
+
* Domain-separated so a declaration signature can never be replayed as a
|
|
73
|
+
* signature over some other kind of message.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} declaration Parsed declaration (its identity.signature is ignored)
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function declarationSigningPayload(declaration) {
|
|
79
|
+
if (declaration === null || typeof declaration !== 'object' || Array.isArray(declaration)) {
|
|
80
|
+
throw new CanonicalError('Declaration must be a parsed object');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { identity, ...rest } = declaration;
|
|
84
|
+
const covered = { ...rest };
|
|
85
|
+
|
|
86
|
+
if (identity !== undefined) {
|
|
87
|
+
if (identity === null || typeof identity !== 'object' || Array.isArray(identity)) {
|
|
88
|
+
throw new CanonicalError('$.identity must be an object when present');
|
|
89
|
+
}
|
|
90
|
+
// public_key and algorithm ARE covered; only the signature is excluded.
|
|
91
|
+
const { signature: _excluded, ...identityRest } = identity;
|
|
92
|
+
covered.identity = identityRest;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return `${DOMAIN}:${canonicalValue(covered)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { DOMAIN as DECLARATION_SIGNING_DOMAIN };
|