provenance-protocol 0.3.0 → 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/SPEC.md +36 -14
- package/package.json +2 -2
- package/src/canonical.js +38 -2
- package/src/keygen.d.ts +12 -0
- package/src/keygen.js +48 -2
- package/src/verify.d.ts +13 -0
- package/src/verify.js +45 -1
package/SPEC.md
CHANGED
|
@@ -408,27 +408,49 @@ This is the same division of labour as a machine-readable passport: the
|
|
|
408
408
|
document proves its own integrity offline, while identity binding and
|
|
409
409
|
current standing are looked up.
|
|
410
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
|
+
|
|
411
438
|
### Live proof of key control
|
|
412
439
|
|
|
413
440
|
A signature on a file proves the file's origin. It does not prove that the
|
|
414
|
-
agent running right now controls that key. For that, a receiving system
|
|
415
|
-
|
|
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>`.
|
|
416
444
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
Nonces must be single-use and unpredictable. The receiving system verifies
|
|
422
|
-
the signature against the public key it already holds for that
|
|
423
|
-
`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`.
|
|
424
447
|
|
|
425
448
|
### Revocation
|
|
426
449
|
|
|
427
|
-
A key holder revokes a `provenance_id` by signing
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
```
|
|
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.
|
|
432
454
|
|
|
433
455
|
Revocation is the one operation that cannot be verified offline — a verifier
|
|
434
456
|
has no way to know a revocation has been issued without asking. Implementations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "provenance-protocol",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
@@ -58,6 +58,6 @@
|
|
|
58
58
|
"node": ">=14.0.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
|
-
"test": "node test/verify.test.mjs && node test/declaration-signature.test.mjs"
|
|
61
|
+
"test": "node test/verify.test.mjs && node test/declaration-signature.test.mjs && node test/payload-separation.test.mjs"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/canonical.js
CHANGED
|
@@ -20,8 +20,6 @@
|
|
|
20
20
|
* silently coerced into an ambiguous signature
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
const DOMAIN = 'provenance-declaration-v1';
|
|
24
|
-
|
|
25
23
|
/** Thrown when a declaration contains something that cannot be canonicalised. */
|
|
26
24
|
export class CanonicalError extends Error {
|
|
27
25
|
constructor(message) {
|
|
@@ -30,6 +28,44 @@ export class CanonicalError extends Error {
|
|
|
30
28
|
}
|
|
31
29
|
}
|
|
32
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
|
+
|
|
33
69
|
function canonicalValue(value, path = '$') {
|
|
34
70
|
if (value === null) return 'null';
|
|
35
71
|
|
package/src/keygen.d.ts
CHANGED
|
@@ -61,3 +61,15 @@ export function signRevocation(privateKeyBase64: string, provenanceId: string):
|
|
|
61
61
|
* @returns Base64 signature — put it in identity.signature
|
|
62
62
|
*/
|
|
63
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,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { generateKeyPairSync, sign, createPrivateKey } from 'crypto';
|
|
28
|
-
import { declarationSigningPayload } from './canonical.js';
|
|
28
|
+
import { declarationSigningPayload, challengePayload, revocationPayload } from './canonical.js';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Generate a new Ed25519 keypair for use with Provenance identity.
|
|
@@ -58,7 +58,12 @@ export function generateProvenanceKeyPair() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* 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.
|
|
62
67
|
*
|
|
63
68
|
* Call this when a receiving system sends you a nonce to prove your identity.
|
|
64
69
|
* The signed message is always `${provenanceId}:${nonce}` — this binds the
|
|
@@ -149,6 +154,47 @@ export function signRevocation(privateKeyBase64, provenanceId) {
|
|
|
149
154
|
* @param {object} declaration Parsed declaration; identity.signature is ignored
|
|
150
155
|
* @returns {string} Base64 signature — put it in identity.signature
|
|
151
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
|
+
|
|
152
198
|
export function signDeclaration(privateKeyBase64, declaration) {
|
|
153
199
|
const keyBuffer = Buffer.from(privateKeyBase64, 'base64');
|
|
154
200
|
const privateKey = createPrivateKey({ key: keyBuffer, format: 'der', type: 'pkcs8' });
|
package/src/verify.d.ts
CHANGED
|
@@ -93,3 +93,16 @@ export function verifyRevocation(
|
|
|
93
93
|
provenanceId: string,
|
|
94
94
|
signatureBase64: string
|
|
95
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,7 +16,7 @@
|
|
|
16
16
|
* Uses the Web Crypto API (crypto.subtle): all modern browsers, Node 18+.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { declarationSigningPayload } from './canonical.js';
|
|
19
|
+
import { declarationSigningPayload, challengePayload, revocationPayload } from './canonical.js';
|
|
20
20
|
|
|
21
21
|
/** Signature algorithm. Ed25519 in every spec version so far. */
|
|
22
22
|
const ALGORITHM = 'ed25519';
|
|
@@ -282,9 +282,53 @@ export async function verifyDeclaration(declaration, options = {}) {
|
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
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
|
+
|
|
285
325
|
/**
|
|
286
326
|
* Verify a live challenge response offline, against a key you already hold.
|
|
287
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
|
+
*
|
|
288
332
|
* The network equivalent in the main SDK looks the key up in the index; this
|
|
289
333
|
* takes the key directly, so a system that already stores keys can verify
|
|
290
334
|
* without contacting anyone.
|