provenance-protocol 0.2.2 → 0.4.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 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.
package/SPEC.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Provenance Protocol Specification
2
- **Version 0.1**
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 in v0.1.
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
- where `<public_key>` is the same base64 string that appears in
327
- `identity.public_key`. Concatenating the two binds the key to the identity,
328
- so a key lifted from one declaration cannot be replayed under a different
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
- Signing a declaration therefore requires `provenance_id` to be present.
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
- - the declaration was produced by the holder of that private key, and
350
- - not one byte of `provenance_id` or `public_key` has changed since.
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
 
@@ -372,27 +408,49 @@ This is the same division of labour as a machine-readable passport: the
372
408
  document proves its own integrity offline, while identity binding and
373
409
  current standing are looked up.
374
410
 
411
+ ### Every signed payload is domain-separated
412
+
413
+ An agent's key signs several different things. Each payload MUST carry the
414
+ prefix for its purpose, so that a signature obtained for one purpose can never
415
+ be presented as another:
416
+
417
+ | Purpose | Signed payload |
418
+ |---|---|
419
+ | Declaration (0.2) | `provenance-declaration-v1:<canonical JSON>` |
420
+ | Live proof of key control | `provenance-challenge-v1:<provenance_id>:<nonce>` |
421
+ | Revocation | `provenance-revocation-v1:<provenance_id>` |
422
+
423
+ This is not a precaution against something hypothetical. In 0.1 a challenge was
424
+ signed as `<provenance_id>:<nonce>` and a revocation as
425
+ `<provenance_id>:REVOKE` — **the same payload with a chosen nonce.** Any
426
+ publicly reachable endpoint that signs a caller-supplied nonce in the 0.1 form
427
+ therefore hands out valid revocation signatures for its own key, and a stranger
428
+ can revoke the agent. Supplying the public key as the nonce likewise reproduces
429
+ the 0.1 declaration signature.
430
+
431
+ An endpoint that signs a caller-supplied value MUST use the separated challenge
432
+ form, and MUST NOT sign the 0.1 payload. Verifiers SHOULD require the separated
433
+ form from any agent that exposes a public challenge endpoint.
434
+
435
+ The 0.1 payloads remain defined so existing deployments keep working, but they
436
+ are unsafe to expose and are superseded.
437
+
375
438
  ### Live proof of key control
376
439
 
377
440
  A signature on a file proves the file's origin. It does not prove that the
378
- agent running right now controls that key. For that, a receiving system
379
- issues a nonce and the agent returns a signature over:
380
-
381
- ```
382
- <provenance_id>:<nonce>
383
- ```
441
+ agent running right now controls that key. For that, a receiving system issues
442
+ a nonce and the agent returns a signature over
443
+ `provenance-challenge-v1:<provenance_id>:<nonce>`.
384
444
 
385
- Nonces must be single-use and unpredictable. The receiving system verifies
386
- the signature against the public key it already holds for that
387
- `provenance_id`.
445
+ Nonces must be single-use and unpredictable. The receiving system verifies the
446
+ signature against the public key it already holds for that `provenance_id`.
388
447
 
389
448
  ### Revocation
390
449
 
391
- A key holder revokes a `provenance_id` by signing:
392
-
393
- ```
394
- <provenance_id>:REVOKE
395
- ```
450
+ A key holder revokes a `provenance_id` by signing
451
+ `provenance-revocation-v1:<provenance_id>`. The payload contains no
452
+ caller-supplied input, so it cannot be produced by a challenge endpoint however
453
+ that endpoint is called.
396
454
 
397
455
  Revocation is the one operation that cannot be verified offline — a verifier
398
456
  has no way to know a revocation has been issued without asking. Implementations
@@ -405,19 +463,25 @@ they would re-check any other freshness signal.
405
463
 
406
464
  An implementation of this specification is conformant if it:
407
465
 
408
- 1. Accepts every file that validates against `schema/provenance-0.1.json`.
466
+ 1. Accepts every file that validates against the schema for its declared
467
+ version — `schema/provenance-0.1.json` or `schema/provenance-0.2.json`.
409
468
  2. Treats `provenance`, `name` and `description` as required and everything
410
469
  else as optional.
411
470
  3. Ignores unrecognised top-level fields rather than rejecting the file.
412
471
  Future spec versions add fields; a `0.1` reader must not break on them.
413
472
  4. Treats `identity.signature` as optional, and an unsigned `identity`
414
473
  block as advertising a key rather than attesting the file.
415
- 5. Reproduces every signature in `test-vectors/signatures-0.1.json` marked
416
- `valid`, and refuses every one marked `invalid`.
417
- 6. Treats a declaration whose `provenance_id` does not match its retrieval
474
+ 5. Applies the signing rule for the version the declaration declares, refuses
475
+ to guess for an unknown version, and reports which coverage it checked —
476
+ a reader must not be left thinking a 0.1 signature protected the
477
+ declared constraints.
478
+ 6. Reproduces every signature in `test-vectors/signatures-0.1.json` and every
479
+ declaration in `test-vectors/declarations-0.2.json` marked `valid`, and
480
+ refuses every one marked `invalid`.
481
+ 7. Treats a declaration whose `provenance_id` does not match its retrieval
418
482
  location as unverified.
419
483
 
420
- Points 5 and 6 are what make independent implementations agree. An
484
+ Points 6 and 7 are what make independent implementations agree. An
421
485
  implementation that passes the test vectors interoperates with every other
422
486
  one that does, with no reference to any particular service.
423
487
 
@@ -442,10 +506,22 @@ monitors, attesters — are applications of the standard, not part of it.
442
506
 
443
507
  ## Versioning
444
508
 
445
- The `provenance` field records which spec version you are using.
446
- We commit to backwards compatibility a `0.1` file will always be
447
- readable regardless of future spec versions.
448
- New versions add fields. Existing fields are never removed.
509
+ The `provenance` field records which spec version you are using, and it is what
510
+ a reader uses to decide how to interpret the file.
511
+
512
+ We commit to backwards compatibility: a `0.1` file will always be readable and
513
+ verifiable, whatever later versions say. Fields are never removed.
514
+
515
+ Usually a new version only adds fields. Version 0.2 is the exception so far — it
516
+ changed what an existing field *covers*, because `identity.signature` in 0.1
517
+ protected only the identity and left the declared capabilities and constraints
518
+ unprotected, which was weaker than readers assumed. Rather than redefine 0.1
519
+ under declarations already published, 0.2 defines the stronger rule and the
520
+ `provenance` field keeps the two apart. A verifier implements both and applies
521
+ the one the declaration asks for.
522
+
523
+ Where a version changes the meaning of an existing field, it will always do so
524
+ by declaring a new version, never by reinterpreting an old one.
449
525
 
450
526
  ---
451
527
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "provenance-protocol",
3
- "version": "0.2.2",
3
+ "version": "0.4.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 && node test/payload-separation.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 and that the declaration is unaltered. Optional: omit it when the key is published only for live challenge-response. Requires provenance_id."
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
+ }
@@ -0,0 +1,134 @@
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
+ /** Thrown when a declaration contains something that cannot be canonicalised. */
24
+ export class CanonicalError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = 'CanonicalError';
28
+ }
29
+ }
30
+
31
+ const DOMAIN = 'provenance-declaration-v1';
32
+
33
+ /**
34
+ * Every distinct thing an agent key signs gets its own prefix, so a signature
35
+ * obtained for one purpose can never be presented as another.
36
+ *
37
+ * This is not theoretical. Spec 0.1 signed a challenge as "<id>:<nonce>" and a
38
+ * revocation as "<id>:REVOKE" — the same payload with a chosen nonce. Any
39
+ * publicly reachable endpoint that signs a caller-supplied nonce therefore
40
+ * hands out valid revocation signatures for its own key, letting a stranger
41
+ * revoke the agent. A key lifted into the nonce likewise reproduces the 0.1
42
+ * declaration payload.
43
+ *
44
+ * The separated forms below cannot be confused with each other whatever the
45
+ * caller supplies.
46
+ */
47
+ export const CHALLENGE_DOMAIN = 'provenance-challenge-v1';
48
+ export const REVOCATION_DOMAIN = 'provenance-revocation-v1';
49
+
50
+ /** Payload for proving live control of a key. Nonce must be single-use. */
51
+ export function challengePayload(provenanceId, nonce) {
52
+ if (typeof provenanceId !== 'string' || provenanceId.length === 0) {
53
+ throw new CanonicalError('provenanceId is required');
54
+ }
55
+ if (typeof nonce !== 'string' || nonce.length === 0) {
56
+ throw new CanonicalError('nonce is required');
57
+ }
58
+ return `${CHALLENGE_DOMAIN}:${provenanceId}:${nonce}`;
59
+ }
60
+
61
+ /** Payload for revoking a provenance id. Carries no caller-supplied input. */
62
+ export function revocationPayload(provenanceId) {
63
+ if (typeof provenanceId !== 'string' || provenanceId.length === 0) {
64
+ throw new CanonicalError('provenanceId is required');
65
+ }
66
+ return `${REVOCATION_DOMAIN}:${provenanceId}`;
67
+ }
68
+
69
+ function canonicalValue(value, path = '$') {
70
+ if (value === null) return 'null';
71
+
72
+ const type = typeof value;
73
+
74
+ if (type === 'string') return JSON.stringify(value);
75
+ if (type === 'boolean') return value ? 'true' : 'false';
76
+ if (type === 'number') {
77
+ if (!Number.isFinite(value)) {
78
+ throw new CanonicalError(`${path}: non-finite numbers cannot be signed`);
79
+ }
80
+ return JSON.stringify(value);
81
+ }
82
+
83
+ if (Array.isArray(value)) {
84
+ return `[${value.map((item, i) => canonicalValue(item, `${path}[${i}]`)).join(',')}]`;
85
+ }
86
+
87
+ if (type === 'object') {
88
+ // A YAML parser may produce Dates, Maps, Buffers and so on. Signing those
89
+ // would depend on whichever serialisation happened to be used, so refuse.
90
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
91
+ throw new CanonicalError(
92
+ `${path}: only plain objects can be signed (got ${value.constructor?.name ?? 'unknown type'}) — quote the value as a string`
93
+ );
94
+ }
95
+ const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
96
+ const parts = keys.map(
97
+ (k) => `${JSON.stringify(k)}:${canonicalValue(value[k], `${path}.${k}`)}`
98
+ );
99
+ return `{${parts.join(',')}}`;
100
+ }
101
+
102
+ throw new CanonicalError(`${path}: values of type ${type} cannot be signed`);
103
+ }
104
+
105
+ /**
106
+ * The exact string a spec-0.2 declaration signature is computed over.
107
+ *
108
+ * Domain-separated so a declaration signature can never be replayed as a
109
+ * signature over some other kind of message.
110
+ *
111
+ * @param {object} declaration Parsed declaration (its identity.signature is ignored)
112
+ * @returns {string}
113
+ */
114
+ export function declarationSigningPayload(declaration) {
115
+ if (declaration === null || typeof declaration !== 'object' || Array.isArray(declaration)) {
116
+ throw new CanonicalError('Declaration must be a parsed object');
117
+ }
118
+
119
+ const { identity, ...rest } = declaration;
120
+ const covered = { ...rest };
121
+
122
+ if (identity !== undefined) {
123
+ if (identity === null || typeof identity !== 'object' || Array.isArray(identity)) {
124
+ throw new CanonicalError('$.identity must be an object when present');
125
+ }
126
+ // public_key and algorithm ARE covered; only the signature is excluded.
127
+ const { signature: _excluded, ...identityRest } = identity;
128
+ covered.identity = identityRest;
129
+ }
130
+
131
+ return `${DOMAIN}:${canonicalValue(covered)}`;
132
+ }
133
+
134
+ export { DOMAIN as DECLARATION_SIGNING_DOMAIN };
package/src/keygen.d.ts CHANGED
@@ -46,3 +46,30 @@ export function signForProvenance(privateKeyBase64: string, provenanceId: string
46
46
  * Send the result as signed_challenge to POST /api/agents/revoke.
47
47
  */
48
48
  export function signRevocation(privateKeyBase64: string, provenanceId: string): string;
49
+
50
+ /**
51
+ * Sign a whole declaration — spec 0.2.
52
+ *
53
+ * Covers every field, so deleting a constraint or adding a capability breaks the
54
+ * signature. `signForProvenance` (spec 0.1) covers only the identity and key.
55
+ *
56
+ * Signs the canonical form of the PARSED declaration, so reformatting the file
57
+ * does not invalidate the signature.
58
+ *
59
+ * @param privateKeyBase64 Your PROVENANCE_PRIVATE_KEY (base64 PKCS8 DER)
60
+ * @param declaration Parsed declaration; identity.signature is ignored
61
+ * @returns Base64 signature — put it in identity.signature
62
+ */
63
+ export function signDeclaration(privateKeyBase64: string, declaration: object): string;
64
+
65
+ /**
66
+ * Prove live control of a key against a nonce — domain-separated form.
67
+ *
68
+ * Use this for any endpoint a stranger can call. The legacy `signChallenge`
69
+ * signs the same payload shape as a revocation, so exposing that publicly lets
70
+ * a caller obtain a valid revocation signature for your own key.
71
+ */
72
+ export function signAgentChallenge(privateKeyBase64: string, provenanceId: string, nonce: string): string;
73
+
74
+ /** Revoke a provenance id — domain-separated form. Takes no caller-supplied input. */
75
+ export function signAgentRevocation(privateKeyBase64: string, provenanceId: string): string;
package/src/keygen.js CHANGED
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { generateKeyPairSync, sign, createPrivateKey } from 'crypto';
28
+ import { declarationSigningPayload, challengePayload, revocationPayload } from './canonical.js';
28
29
 
29
30
  /**
30
31
  * Generate a new Ed25519 keypair for use with Provenance identity.
@@ -57,7 +58,12 @@ export function generateProvenanceKeyPair() {
57
58
  }
58
59
 
59
60
  /**
60
- * Sign a challenge from a receiving system.
61
+ * Sign a challenge from a receiving system — LEGACY, spec 0.1 form.
62
+ *
63
+ * Signs "<provenanceId>:<nonce>", which is indistinguishable from a revocation
64
+ * when the nonce is "REVOKE". NEVER expose an endpoint that calls this with a
65
+ * caller-supplied nonce: a stranger can use it to revoke your key. Use
66
+ * `signAgentChallenge` instead.
61
67
  *
62
68
  * Call this when a receiving system sends you a nonce to prove your identity.
63
69
  * The signed message is always `${provenanceId}:${nonce}` — this binds the
@@ -134,6 +140,68 @@ export function signRevocation(privateKeyBase64, provenanceId) {
134
140
  return signChallenge(privateKeyBase64, provenanceId, 'REVOKE');
135
141
  }
136
142
 
143
+ /**
144
+ * Sign a whole declaration — spec 0.2.
145
+ *
146
+ * Covers every field, so deleting a constraint or adding a capability breaks
147
+ * the signature. `signForProvenance` (spec 0.1) covers only the identity and
148
+ * key, which leaves the rest of the declaration unprotected; prefer this.
149
+ *
150
+ * Signs the canonical form of the PARSED declaration, so reformatting the file
151
+ * does not invalidate the signature.
152
+ *
153
+ * @param {string} privateKeyBase64 Base64 PKCS8 DER private key
154
+ * @param {object} declaration Parsed declaration; identity.signature is ignored
155
+ * @returns {string} Base64 signature — put it in identity.signature
156
+ */
157
+ function _sign(privateKeyBase64, message) {
158
+ const privateKey = createPrivateKey({
159
+ key: Buffer.from(privateKeyBase64, 'base64'),
160
+ format: 'der',
161
+ type: 'pkcs8',
162
+ });
163
+ return sign(null, Buffer.from(message, 'utf8'), privateKey).toString('base64');
164
+ }
165
+
166
+ /**
167
+ * Prove live control of a key against a nonce — domain-separated form.
168
+ *
169
+ * Use this for any endpoint a stranger can call. The legacy `signChallenge`
170
+ * signs "<provenanceId>:<nonce>", which is the same shape as a revocation with
171
+ * nonce "REVOKE" — so exposing that publicly lets a caller obtain a valid
172
+ * revocation signature for your own key and revoke you. This form cannot be
173
+ * confused with a revocation or a declaration whatever nonce is supplied.
174
+ *
175
+ * @param {string} privateKeyBase64
176
+ * @param {string} provenanceId
177
+ * @param {string} nonce Single-use and unpredictable
178
+ * @returns {string} Base64 signature
179
+ */
180
+ export function signAgentChallenge(privateKeyBase64, provenanceId, nonce) {
181
+ return _sign(privateKeyBase64, challengePayload(provenanceId, nonce));
182
+ }
183
+
184
+ /**
185
+ * Revoke a provenance id — domain-separated form.
186
+ *
187
+ * Takes no caller-supplied input, so it cannot be produced by a challenge
188
+ * endpoint however it is called.
189
+ *
190
+ * @param {string} privateKeyBase64
191
+ * @param {string} provenanceId
192
+ * @returns {string} Base64 signature
193
+ */
194
+ export function signAgentRevocation(privateKeyBase64, provenanceId) {
195
+ return _sign(privateKeyBase64, revocationPayload(provenanceId));
196
+ }
197
+
198
+ export function signDeclaration(privateKeyBase64, declaration) {
199
+ const keyBuffer = Buffer.from(privateKeyBase64, 'base64');
200
+ const privateKey = createPrivateKey({ key: keyBuffer, format: 'der', type: 'pkcs8' });
201
+ const message = Buffer.from(declarationSigningPayload(declaration), 'utf8');
202
+ return sign(null, message, privateKey).toString('base64');
203
+ }
204
+
137
205
  export function signForProvenance(privateKeyBase64, provenanceId, publicKeyBase64) {
138
206
  const keyBuffer = Buffer.from(privateKeyBase64, 'base64');
139
207
  const privateKey = createPrivateKey({ key: keyBuffer, format: 'der', type: 'pkcs8' });
package/src/verify.d.ts CHANGED
@@ -8,6 +8,9 @@
8
8
  /** Whether a declaration was served from the location its provenance_id names. */
9
9
  export type LocationCheck = 'match' | 'mismatch' | 'unchecked';
10
10
 
11
+ /** What a signature was found to cover, per the declaration's spec version. */
12
+ export type SignatureCoverage = 'declaration' | 'identity';
13
+
11
14
  export interface VerificationResult {
12
15
  /** An identity.signature was present to check. */
13
16
  signed: boolean;
@@ -20,6 +23,13 @@ export interface VerificationResult {
20
23
  /** SHA-256 of the public key, hex. Store it to detect key rotation. */
21
24
  fingerprint: string | null;
22
25
  location: LocationCheck;
26
+ /**
27
+ * 'declaration' (spec 0.2) — every field is covered; any edit breaks it.
28
+ * 'identity' (spec 0.1) — only the identity and key are covered, so the
29
+ * declared capabilities and constraints are NOT protected by the signature.
30
+ * null when no signature was checked.
31
+ */
32
+ coverage: SignatureCoverage | null;
23
33
  /**
24
34
  * Signature valid AND retrieval location confirmed. Only both together
25
35
  * justify treating the declaration as the named project owner's.
@@ -83,3 +93,16 @@ export function verifyRevocation(
83
93
  provenanceId: string,
84
94
  signatureBase64: string
85
95
  ): Promise<boolean>;
96
+
97
+ /**
98
+ * Verify a live challenge response — domain-separated form. Prefer this over
99
+ * `verifyChallenge`, whose payload is indistinguishable from a revocation.
100
+ */
101
+ export function verifyAgentChallenge(
102
+ publicKeyBase64: string, provenanceId: string, nonce: string, signatureBase64: string
103
+ ): Promise<boolean>;
104
+
105
+ /** Verify a revocation — domain-separated form. */
106
+ export function verifyAgentRevocation(
107
+ publicKeyBase64: string, provenanceId: string, signatureBase64: string
108
+ ): Promise<boolean>;
package/src/verify.js CHANGED
@@ -16,9 +16,23 @@
16
16
  * Uses the Web Crypto API (crypto.subtle): all modern browsers, Node 18+.
17
17
  */
18
18
 
19
- /** Signature algorithm for spec v0.1. No other value is valid. */
19
+ import { declarationSigningPayload, challengePayload, revocationPayload } from './canonical.js';
20
+
21
+ /** Signature algorithm. Ed25519 in every spec version so far. */
20
22
  const ALGORITHM = 'ed25519';
21
23
 
24
+ /**
25
+ * Which spec versions this module knows how to verify a signature for, and what
26
+ * the signature covers in each.
27
+ *
28
+ * 0.1 — signs "<provenance_id>:<public_key>". Proves key control under that
29
+ * identity. Does NOT cover the rest of the declaration: a constraint can
30
+ * be deleted and the signature still verifies.
31
+ * 0.2 — signs the canonical form of the whole declaration. Any change to any
32
+ * field breaks it.
33
+ */
34
+ const SIGNATURE_COVERAGE = { '0.1': 'identity', '0.2': 'declaration' };
35
+
22
36
  function subtle() {
23
37
  const s = globalThis.crypto?.subtle;
24
38
  if (!s) throw new Error('Web Crypto API (crypto.subtle) not available');
@@ -142,10 +156,14 @@ export function checkLocation(provenanceId, retrievedFrom) {
142
156
  * when `retrievedFrom` is given — whether the file was served from the
143
157
  * location it claims.
144
158
  *
145
- * A valid signature proves the declaration was produced by the holder of that
146
- * private key and has not been altered. It does NOT prove who that holder is,
147
- * that the declared capabilities are accurate, or that the declaration is
148
- * current. Revocation and standing cannot be checked offline.
159
+ * What a valid signature proves depends on the spec version, and `coverage`
160
+ * reports which: 'declaration' (0.2) means every field is covered, so any edit
161
+ * breaks it; 'identity' (0.1) means only the identity and key are covered, so
162
+ * the declared capabilities and constraints are NOT protected by it.
163
+ *
164
+ * In neither case does a signature prove who the key holder is, that the
165
+ * declared capabilities are accurate, or that the declaration is current.
166
+ * Revocation and standing cannot be checked offline.
149
167
  *
150
168
  * @param {object} declaration Parsed PROVENANCE.yml
151
169
  * @param {object} [options]
@@ -158,6 +176,7 @@ export function checkLocation(provenanceId, retrievedFrom) {
158
176
  * publicKey: string | null,
159
177
  * fingerprint: string | null,
160
178
  * location: 'match' | 'mismatch' | 'unchecked',
179
+ * coverage: 'declaration' | 'identity' | null,
161
180
  * trustworthy: boolean
162
181
  * }>}
163
182
  */
@@ -170,6 +189,7 @@ export async function verifyDeclaration(declaration, options = {}) {
170
189
  publicKey: null,
171
190
  fingerprint: null,
172
191
  location: 'unchecked',
192
+ coverage: null,
173
193
  trustworthy: false,
174
194
  };
175
195
 
@@ -213,13 +233,39 @@ export async function verifyDeclaration(declaration, options = {}) {
213
233
  }
214
234
  result.signed = true;
215
235
 
216
- if (!provenanceId) {
217
- return { ...result, reason: 'provenance_id is required to verify a signature' };
236
+ const declaredVersion = typeof declaration.provenance === 'string' ? declaration.provenance : '0.1';
237
+ if (!provenanceId && declaredVersion === '0.1') {
238
+ // The 0.1 payload is built from provenance_id, so without it there is
239
+ // nothing to verify. A 0.2 signature covers the whole declaration and does
240
+ // not need it (though the location check still does).
241
+ return { ...result, reason: 'provenance_id is required to verify a 0.1 signature' };
242
+ }
243
+
244
+ // What the signature covers depends on the spec version the declaration
245
+ // declares, so the payload is built differently for each.
246
+ const specVersion = typeof declaration.provenance === 'string' ? declaration.provenance : '0.1';
247
+ const coverage = SIGNATURE_COVERAGE[specVersion];
248
+ if (!coverage) {
249
+ return {
250
+ ...result,
251
+ reason: `Spec version ${specVersion} is not known to this verifier — cannot check its signature`,
252
+ };
253
+ }
254
+ result.coverage = coverage;
255
+
256
+ let payload;
257
+ try {
258
+ payload =
259
+ coverage === 'declaration'
260
+ ? declarationSigningPayload(declaration)
261
+ : `${provenanceId}:${publicKey}`;
262
+ } catch (e) {
263
+ return { ...result, reason: `Declaration cannot be canonicalised: ${e.message}` };
218
264
  }
219
265
 
220
266
  let valid;
221
267
  try {
222
- valid = await verifyEd25519(publicKey, signature, `${provenanceId}:${publicKey}`);
268
+ valid = await verifyEd25519(publicKey, signature, payload);
223
269
  } catch {
224
270
  return { ...result, reason: 'identity.signature is malformed' };
225
271
  }
@@ -236,9 +282,53 @@ export async function verifyDeclaration(declaration, options = {}) {
236
282
  };
237
283
  }
238
284
 
285
+ /**
286
+ * Verify a live challenge response — domain-separated form.
287
+ *
288
+ * Pair with `signAgentChallenge`. Prefer this over `verifyChallenge`: the legacy
289
+ * payload is the same shape as a revocation, so any public endpoint signing it
290
+ * is a way to revoke the agent's own key.
291
+ *
292
+ * @param {string} publicKeyBase64
293
+ * @param {string} provenanceId
294
+ * @param {string} nonce Single-use, unpredictable
295
+ * @param {string} signatureBase64
296
+ * @returns {Promise<boolean>}
297
+ */
298
+ export async function verifyAgentChallenge(publicKeyBase64, provenanceId, nonce, signatureBase64) {
299
+ try {
300
+ return await verifyEd25519(publicKeyBase64, signatureBase64, challengePayload(provenanceId, nonce));
301
+ } catch {
302
+ return false;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Verify a revocation — domain-separated form.
308
+ *
309
+ * Confirms it came from the key holder. It does not tell you whether a
310
+ * revocation exists; that requires asking an index.
311
+ *
312
+ * @param {string} publicKeyBase64
313
+ * @param {string} provenanceId
314
+ * @param {string} signatureBase64
315
+ * @returns {Promise<boolean>}
316
+ */
317
+ export async function verifyAgentRevocation(publicKeyBase64, provenanceId, signatureBase64) {
318
+ try {
319
+ return await verifyEd25519(publicKeyBase64, signatureBase64, revocationPayload(provenanceId));
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+
239
325
  /**
240
326
  * Verify a live challenge response offline, against a key you already hold.
241
327
  *
328
+ * LEGACY (spec 0.1 payload). Accepts "<provenanceId>:<nonce>", which is the same
329
+ * shape as a revocation with nonce "REVOKE" — so never verify against a peer
330
+ * that exposes this form publicly. Use `verifyAgentChallenge`.
331
+ *
242
332
  * The network equivalent in the main SDK looks the key up in the index; this
243
333
  * takes the key directly, so a system that already stores keys can verify
244
334
  * without contacting anyone.
@@ -0,0 +1,217 @@
1
+ {
2
+ "$comment": "Normative test vectors for Provenance Protocol v0.2 declaration signatures. The signature covers the canonical form of the entire parsed declaration, excluding identity.signature itself.",
3
+ "spec_version": "0.2",
4
+ "algorithm": "ed25519",
5
+ "signing": {
6
+ "payload": "the string \"provenance-declaration-v1:\" followed by the canonical JSON of the parsed declaration with identity.signature removed",
7
+ "canonicalisation": "object keys sorted by code point recursively, no insignificant whitespace, applied to the PARSED value so formatting and key order do not matter"
8
+ },
9
+ "keypair": {
10
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=",
11
+ "private_key": "MC4CAQAwBQYDK2VwBCIEICnsl5v8baf3DyJApejP5DG3cmx1xS4S5qewS1A28IMN",
12
+ "$comment": "Test key only. Never use in production."
13
+ },
14
+ "reference_payload": "provenance-declaration-v1:{\"capabilities\":[\"read:web\",\"write:summaries\"],\"constraints\":[\"no:pii\",\"no:financial:transact\"],\"description\":\"A bounded fixture declaration for the signing vectors.\",\"identity\":{\"algorithm\":\"ed25519\",\"public_key\":\"MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=\"},\"name\":\"Fixture Research Agent\",\"provenance\":\"0.2\",\"provenance_id\":\"provenance:github:example/research-agent\",\"version\":\"1.2.0\"}",
15
+ "vectors": [
16
+ {
17
+ "id": "declaration-signature-valid",
18
+ "expect": "valid",
19
+ "purpose": "a correctly signed 0.2 declaration",
20
+ "declaration": {
21
+ "provenance": "0.2",
22
+ "name": "Fixture Research Agent",
23
+ "description": "A bounded fixture declaration for the signing vectors.",
24
+ "version": "1.2.0",
25
+ "capabilities": [
26
+ "read:web",
27
+ "write:summaries"
28
+ ],
29
+ "constraints": [
30
+ "no:pii",
31
+ "no:financial:transact"
32
+ ],
33
+ "provenance_id": "provenance:github:example/research-agent",
34
+ "identity": {
35
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=",
36
+ "algorithm": "ed25519",
37
+ "signature": "LqPe4bIXfa4qRhblNfp3VXElk2gG0x5UfTaHNKJGUDNbx4tRNXnJyXZZuu3BiC58Eur2JF719MieXU+EJe2pDg=="
38
+ }
39
+ }
40
+ },
41
+ {
42
+ "id": "declaration-key-order-irrelevant",
43
+ "expect": "valid",
44
+ "purpose": "the same declaration with keys in a different order must still verify",
45
+ "declaration": {
46
+ "identity": {
47
+ "signature": "LqPe4bIXfa4qRhblNfp3VXElk2gG0x5UfTaHNKJGUDNbx4tRNXnJyXZZuu3BiC58Eur2JF719MieXU+EJe2pDg==",
48
+ "algorithm": "ed25519",
49
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98="
50
+ },
51
+ "constraints": [
52
+ "no:pii",
53
+ "no:financial:transact"
54
+ ],
55
+ "capabilities": [
56
+ "read:web",
57
+ "write:summaries"
58
+ ],
59
+ "provenance_id": "provenance:github:example/research-agent",
60
+ "version": "1.2.0",
61
+ "description": "A bounded fixture declaration for the signing vectors.",
62
+ "name": "Fixture Research Agent",
63
+ "provenance": "0.2"
64
+ }
65
+ },
66
+ {
67
+ "id": "declaration-constraint-removed",
68
+ "expect": "invalid",
69
+ "purpose": "THE CASE 0.1 MISSED — deleting a declared constraint must break the signature",
70
+ "declaration": {
71
+ "provenance": "0.2",
72
+ "name": "Fixture Research Agent",
73
+ "description": "A bounded fixture declaration for the signing vectors.",
74
+ "version": "1.2.0",
75
+ "capabilities": [
76
+ "read:web",
77
+ "write:summaries"
78
+ ],
79
+ "constraints": [
80
+ "no:pii"
81
+ ],
82
+ "provenance_id": "provenance:github:example/research-agent",
83
+ "identity": {
84
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=",
85
+ "algorithm": "ed25519",
86
+ "signature": "LqPe4bIXfa4qRhblNfp3VXElk2gG0x5UfTaHNKJGUDNbx4tRNXnJyXZZuu3BiC58Eur2JF719MieXU+EJe2pDg=="
87
+ }
88
+ }
89
+ },
90
+ {
91
+ "id": "declaration-capability-added",
92
+ "expect": "invalid",
93
+ "purpose": "granting the agent a new declared power must break the signature",
94
+ "declaration": {
95
+ "provenance": "0.2",
96
+ "name": "Fixture Research Agent",
97
+ "description": "A bounded fixture declaration for the signing vectors.",
98
+ "version": "1.2.0",
99
+ "capabilities": [
100
+ "read:web",
101
+ "write:summaries",
102
+ "financial:transact"
103
+ ],
104
+ "constraints": [
105
+ "no:pii",
106
+ "no:financial:transact"
107
+ ],
108
+ "provenance_id": "provenance:github:example/research-agent",
109
+ "identity": {
110
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=",
111
+ "algorithm": "ed25519",
112
+ "signature": "LqPe4bIXfa4qRhblNfp3VXElk2gG0x5UfTaHNKJGUDNbx4tRNXnJyXZZuu3BiC58Eur2JF719MieXU+EJe2pDg=="
113
+ }
114
+ }
115
+ },
116
+ {
117
+ "id": "declaration-key-swapped",
118
+ "expect": "invalid",
119
+ "purpose": "substituting another public key must break the signature",
120
+ "declaration": {
121
+ "provenance": "0.2",
122
+ "name": "Fixture Research Agent",
123
+ "description": "A bounded fixture declaration for the signing vectors.",
124
+ "version": "1.2.0",
125
+ "capabilities": [
126
+ "read:web",
127
+ "write:summaries"
128
+ ],
129
+ "constraints": [
130
+ "no:pii",
131
+ "no:financial:transact"
132
+ ],
133
+ "provenance_id": "provenance:github:example/research-agent",
134
+ "identity": {
135
+ "public_key": "MCowBQYDK2VwAyEAiuHMTpGGFwJ8XGYRofeYY0s6rV1ywTl8nmLgHj08lew=",
136
+ "algorithm": "ed25519",
137
+ "signature": "LqPe4bIXfa4qRhblNfp3VXElk2gG0x5UfTaHNKJGUDNbx4tRNXnJyXZZuu3BiC58Eur2JF719MieXU+EJe2pDg=="
138
+ }
139
+ }
140
+ },
141
+ {
142
+ "id": "declaration-signed-with-wrong-key",
143
+ "expect": "invalid",
144
+ "purpose": "a signature made by a different private key must not verify",
145
+ "declaration": {
146
+ "provenance": "0.2",
147
+ "name": "Fixture Research Agent",
148
+ "description": "A bounded fixture declaration for the signing vectors.",
149
+ "version": "1.2.0",
150
+ "capabilities": [
151
+ "read:web",
152
+ "write:summaries"
153
+ ],
154
+ "constraints": [
155
+ "no:pii",
156
+ "no:financial:transact"
157
+ ],
158
+ "provenance_id": "provenance:github:example/research-agent",
159
+ "identity": {
160
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=",
161
+ "algorithm": "ed25519",
162
+ "signature": "SgK3ou1XNPOTQDP+gbwvbHPcs8+21SQaxChiHT14c5mHvZ3XzNH8secDlS/fDkog7iBN0oKQrw5O7RjLr61LCw=="
163
+ }
164
+ }
165
+ },
166
+ {
167
+ "id": "declaration-0.1-signature-in-0.2-file",
168
+ "expect": "invalid",
169
+ "purpose": "an old identity-only signature must not satisfy 0.2",
170
+ "declaration": {
171
+ "provenance": "0.2",
172
+ "name": "Fixture Research Agent",
173
+ "description": "A bounded fixture declaration for the signing vectors.",
174
+ "version": "1.2.0",
175
+ "capabilities": [
176
+ "read:web",
177
+ "write:summaries"
178
+ ],
179
+ "constraints": [
180
+ "no:pii",
181
+ "no:financial:transact"
182
+ ],
183
+ "provenance_id": "provenance:github:example/research-agent",
184
+ "identity": {
185
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=",
186
+ "algorithm": "ed25519",
187
+ "signature": "d4/wFYKJD8AQlKVDnC+JloeW7EnYUZ1pJoSG+HkM2wH/KMKYm8Dc6bdnCCNjtEIB/EiDei4+45DMdODPd9F0Cg=="
188
+ }
189
+ }
190
+ },
191
+ {
192
+ "id": "declaration-unknown-spec-version",
193
+ "expect": "invalid",
194
+ "purpose": "a verifier must refuse to guess the signing rule for a version it does not know",
195
+ "declaration": {
196
+ "provenance": "9.9",
197
+ "name": "Fixture Research Agent",
198
+ "description": "A bounded fixture declaration for the signing vectors.",
199
+ "version": "1.2.0",
200
+ "capabilities": [
201
+ "read:web",
202
+ "write:summaries"
203
+ ],
204
+ "constraints": [
205
+ "no:pii",
206
+ "no:financial:transact"
207
+ ],
208
+ "provenance_id": "provenance:github:example/research-agent",
209
+ "identity": {
210
+ "public_key": "MCowBQYDK2VwAyEAyuDQn/j1dbeAd288fU7lRV5cFcm7Zsm42jwCKy07N98=",
211
+ "algorithm": "ed25519",
212
+ "signature": "LqPe4bIXfa4qRhblNfp3VXElk2gG0x5UfTaHNKJGUDNbx4tRNXnJyXZZuu3BiC58Eur2JF719MieXU+EJe2pDg=="
213
+ }
214
+ }
215
+ }
216
+ ]
217
+ }