provenance-protocol 0.1.3 → 0.2.1
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 +103 -22
- package/SPEC.md +483 -0
- package/package.json +23 -4
- package/schema/provenance-0.1.json +229 -0
- package/src/cli.js +196 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +54 -6
- package/src/verify.d.ts +85 -0
- package/src/verify.js +277 -0
- package/test-vectors/README.md +52 -0
- package/test-vectors/signatures-0.1.json +80 -0
package/src/verify.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provenance-protocol — offline verification (TypeScript definitions)
|
|
3
|
+
*
|
|
4
|
+
* Verifies a PROVENANCE.yml declaration without contacting any service.
|
|
5
|
+
* Works anywhere the Web Crypto API exists: modern browsers, Node 18+.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Whether a declaration was served from the location its provenance_id names. */
|
|
9
|
+
export type LocationCheck = 'match' | 'mismatch' | 'unchecked';
|
|
10
|
+
|
|
11
|
+
export interface VerificationResult {
|
|
12
|
+
/** An identity.signature was present to check. */
|
|
13
|
+
signed: boolean;
|
|
14
|
+
/** The signature verified against identity.public_key. */
|
|
15
|
+
valid: boolean;
|
|
16
|
+
/** Why the result is not a clean pass, or null when it is. */
|
|
17
|
+
reason: string | null;
|
|
18
|
+
provenanceId: string | null;
|
|
19
|
+
publicKey: string | null;
|
|
20
|
+
/** SHA-256 of the public key, hex. Store it to detect key rotation. */
|
|
21
|
+
fingerprint: string | null;
|
|
22
|
+
location: LocationCheck;
|
|
23
|
+
/**
|
|
24
|
+
* Signature valid AND retrieval location confirmed. Only both together
|
|
25
|
+
* justify treating the declaration as the named project owner's.
|
|
26
|
+
*/
|
|
27
|
+
trustworthy: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VerifyOptions {
|
|
31
|
+
/** URL the declaration was fetched from, for the location check. */
|
|
32
|
+
retrievedFrom?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verify a parsed PROVENANCE.yml declaration offline.
|
|
37
|
+
*
|
|
38
|
+
* Declarations are YAML — parse with your own library and pass the object;
|
|
39
|
+
* this module is dependency-free by design.
|
|
40
|
+
*
|
|
41
|
+
* A valid signature proves the declaration came from the holder of that
|
|
42
|
+
* private key and is unaltered. It does not prove who that holder is, that
|
|
43
|
+
* the declared capabilities are accurate, or that the declaration is current.
|
|
44
|
+
* Revocation and standing cannot be checked offline.
|
|
45
|
+
*/
|
|
46
|
+
export function verifyDeclaration(
|
|
47
|
+
declaration: unknown,
|
|
48
|
+
options?: VerifyOptions
|
|
49
|
+
): Promise<VerificationResult>;
|
|
50
|
+
|
|
51
|
+
/** SHA-256 of the raw public key bytes, hex encoded. Use it to detect key rotation. */
|
|
52
|
+
export function keyFingerprint(publicKeyBase64: string): Promise<string>;
|
|
53
|
+
|
|
54
|
+
/** Split a provenance id into its platform and path, or null if malformed. */
|
|
55
|
+
export function parseProvenanceId(
|
|
56
|
+
provenanceId: string
|
|
57
|
+
): { platform: string; path: string } | null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Does a retrieval location agree with the declaration's own provenance_id?
|
|
61
|
+
* Returns 'unchecked' when the location cannot be interpreted, so an unknown
|
|
62
|
+
* host is never reported as agreement.
|
|
63
|
+
*/
|
|
64
|
+
export function checkLocation(provenanceId: string, retrievedFrom: string): LocationCheck;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Verify a live challenge response against a key you already hold.
|
|
68
|
+
* The nonce must be single-use and unpredictable.
|
|
69
|
+
*/
|
|
70
|
+
export function verifyChallenge(
|
|
71
|
+
publicKeyBase64: string,
|
|
72
|
+
provenanceId: string,
|
|
73
|
+
nonce: string,
|
|
74
|
+
signatureBase64: string
|
|
75
|
+
): Promise<boolean>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Verify an owner-signed revocation. Confirms it came from the key holder;
|
|
79
|
+
* it does not tell you whether a revocation exists.
|
|
80
|
+
*/
|
|
81
|
+
export function verifyRevocation(
|
|
82
|
+
publicKeyBase64: string,
|
|
83
|
+
provenanceId: string,
|
|
84
|
+
signatureBase64: string
|
|
85
|
+
): Promise<boolean>;
|
package/src/verify.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provenance-protocol — offline verification
|
|
3
|
+
*
|
|
4
|
+
* Verifies a PROVENANCE.yml declaration without contacting any service.
|
|
5
|
+
* No network, no account, no API key. See SPEC.md § Signing and Verification.
|
|
6
|
+
*
|
|
7
|
+
* import { verifyDeclaration } from 'provenance-protocol/verify';
|
|
8
|
+
*
|
|
9
|
+
* const result = await verifyDeclaration(parsedYaml, {
|
|
10
|
+
* retrievedFrom: 'https://github.com/alice/research-assistant',
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* Declarations are YAML. Parse them with whatever library you already use and
|
|
14
|
+
* pass the resulting object — this module stays dependency-free on purpose.
|
|
15
|
+
*
|
|
16
|
+
* Uses the Web Crypto API (crypto.subtle): all modern browsers, Node 18+.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Signature algorithm for spec v0.1. No other value is valid. */
|
|
20
|
+
const ALGORITHM = 'ed25519';
|
|
21
|
+
|
|
22
|
+
function subtle() {
|
|
23
|
+
const s = globalThis.crypto?.subtle;
|
|
24
|
+
if (!s) throw new Error('Web Crypto API (crypto.subtle) not available');
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fromBase64(value) {
|
|
29
|
+
if (typeof value !== 'string' || value.length === 0) throw new Error('not base64');
|
|
30
|
+
// atob is available in browsers and Node 18+; avoids depending on Buffer.
|
|
31
|
+
const binary = atob(value.replace(/\s+/g, ''));
|
|
32
|
+
const bytes = new Uint8Array(binary.length);
|
|
33
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
34
|
+
return bytes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toHex(bytes) {
|
|
38
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function verifyEd25519(publicKeyBase64, signatureBase64, message) {
|
|
42
|
+
const key = await subtle().importKey(
|
|
43
|
+
'spki',
|
|
44
|
+
fromBase64(publicKeyBase64),
|
|
45
|
+
{ name: 'Ed25519' },
|
|
46
|
+
false,
|
|
47
|
+
['verify']
|
|
48
|
+
);
|
|
49
|
+
return subtle().verify(
|
|
50
|
+
'Ed25519',
|
|
51
|
+
key,
|
|
52
|
+
fromBase64(signatureBase64),
|
|
53
|
+
new TextEncoder().encode(message)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* SHA-256 of the raw public key bytes, hex encoded.
|
|
59
|
+
*
|
|
60
|
+
* Use it to detect key rotation: store the fingerprint you saw for a
|
|
61
|
+
* provenance_id, and treat a different one later as a material change rather
|
|
62
|
+
* than a silent update. See SPEC.md § Signing and Verification (Continuity).
|
|
63
|
+
*
|
|
64
|
+
* @param {string} publicKeyBase64 Base64 SPKI DER Ed25519 public key
|
|
65
|
+
* @returns {Promise<string>}
|
|
66
|
+
*/
|
|
67
|
+
export async function keyFingerprint(publicKeyBase64) {
|
|
68
|
+
const digest = await subtle().digest('SHA-256', fromBase64(publicKeyBase64));
|
|
69
|
+
return toHex(new Uint8Array(digest));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where a declaration claims to live, parsed out of its provenance_id.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} provenanceId e.g. 'provenance:github:alice/agent'
|
|
76
|
+
* @returns {{ platform: string, path: string } | null}
|
|
77
|
+
*/
|
|
78
|
+
export function parseProvenanceId(provenanceId) {
|
|
79
|
+
if (typeof provenanceId !== 'string') return null;
|
|
80
|
+
const match = /^provenance:([a-z0-9-]+):(.+)$/i.exec(provenanceId.trim());
|
|
81
|
+
if (!match) return null;
|
|
82
|
+
return { platform: match[1].toLowerCase(), path: match[2] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const HOST_PLATFORMS = [
|
|
86
|
+
[/(^|\.)github\.com$/i, 'github'],
|
|
87
|
+
[/(^|\.)githubusercontent\.com$/i, 'github'],
|
|
88
|
+
[/(^|\.)huggingface\.co$/i, 'huggingface'],
|
|
89
|
+
[/(^|\.)npmjs\.com$/i, 'npm'],
|
|
90
|
+
[/(^|\.)registry\.npmjs\.org$/i, 'npm'],
|
|
91
|
+
[/(^|\.)pypi\.org$/i, 'pypi'],
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Does a retrieval location agree with the declaration's own provenance_id?
|
|
96
|
+
*
|
|
97
|
+
* A declaration served from somewhere other than the location it names was
|
|
98
|
+
* placed there by someone who may have no control over the named project —
|
|
99
|
+
* the re-hosting case. A conformant verifier treats that as unverified
|
|
100
|
+
* however good the signature is.
|
|
101
|
+
*
|
|
102
|
+
* Returns 'unchecked' when the location cannot be interpreted, so an unknown
|
|
103
|
+
* host is never reported as agreement.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} provenanceId
|
|
106
|
+
* @param {string} retrievedFrom URL the declaration was fetched from
|
|
107
|
+
* @returns {'match' | 'mismatch' | 'unchecked'}
|
|
108
|
+
*/
|
|
109
|
+
export function checkLocation(provenanceId, retrievedFrom) {
|
|
110
|
+
const id = parseProvenanceId(provenanceId);
|
|
111
|
+
if (!id || typeof retrievedFrom !== 'string') return 'unchecked';
|
|
112
|
+
|
|
113
|
+
let url;
|
|
114
|
+
try {
|
|
115
|
+
url = new URL(retrievedFrom);
|
|
116
|
+
} catch {
|
|
117
|
+
return 'unchecked';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const platform = HOST_PLATFORMS.find(([host]) => host.test(url.hostname))?.[1];
|
|
121
|
+
if (!platform) return 'unchecked';
|
|
122
|
+
if (platform !== id.platform) return 'mismatch';
|
|
123
|
+
|
|
124
|
+
// github/huggingface ids are owner/repo; npm and pypi are package names.
|
|
125
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
126
|
+
const expected = id.path.toLowerCase().split('/').filter(Boolean);
|
|
127
|
+
if (expected.length === 0) return 'unchecked';
|
|
128
|
+
|
|
129
|
+
// The declared path must appear as consecutive segments of the URL path.
|
|
130
|
+
// Covers both https://github.com/owner/repo and raw/blob URLs beneath it.
|
|
131
|
+
const haystack = segments.map((s) => s.toLowerCase());
|
|
132
|
+
for (let i = 0; i + expected.length <= haystack.length; i++) {
|
|
133
|
+
if (expected.every((part, j) => haystack[i + j] === part)) return 'match';
|
|
134
|
+
}
|
|
135
|
+
return 'mismatch';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Verify a parsed PROVENANCE.yml declaration offline.
|
|
140
|
+
*
|
|
141
|
+
* Checks the identity signature against the public key inside the file, and —
|
|
142
|
+
* when `retrievedFrom` is given — whether the file was served from the
|
|
143
|
+
* location it claims.
|
|
144
|
+
*
|
|
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.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} declaration Parsed PROVENANCE.yml
|
|
151
|
+
* @param {object} [options]
|
|
152
|
+
* @param {string} [options.retrievedFrom] URL the declaration was fetched from
|
|
153
|
+
* @returns {Promise<{
|
|
154
|
+
* signed: boolean,
|
|
155
|
+
* valid: boolean,
|
|
156
|
+
* reason: string | null,
|
|
157
|
+
* provenanceId: string | null,
|
|
158
|
+
* publicKey: string | null,
|
|
159
|
+
* fingerprint: string | null,
|
|
160
|
+
* location: 'match' | 'mismatch' | 'unchecked',
|
|
161
|
+
* trustworthy: boolean
|
|
162
|
+
* }>}
|
|
163
|
+
*/
|
|
164
|
+
export async function verifyDeclaration(declaration, options = {}) {
|
|
165
|
+
const base = {
|
|
166
|
+
signed: false,
|
|
167
|
+
valid: false,
|
|
168
|
+
reason: null,
|
|
169
|
+
provenanceId: null,
|
|
170
|
+
publicKey: null,
|
|
171
|
+
fingerprint: null,
|
|
172
|
+
location: 'unchecked',
|
|
173
|
+
trustworthy: false,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (declaration === null || typeof declaration !== 'object') {
|
|
177
|
+
return { ...base, reason: 'Declaration must be a parsed object' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const provenanceId =
|
|
181
|
+
typeof declaration.provenance_id === 'string' ? declaration.provenance_id : null;
|
|
182
|
+
const identity =
|
|
183
|
+
declaration.identity !== null && typeof declaration.identity === 'object'
|
|
184
|
+
? declaration.identity
|
|
185
|
+
: null;
|
|
186
|
+
const publicKey = typeof identity?.public_key === 'string' ? identity.public_key : null;
|
|
187
|
+
const signature = typeof identity?.signature === 'string' ? identity.signature : null;
|
|
188
|
+
|
|
189
|
+
const location = options.retrievedFrom
|
|
190
|
+
? checkLocation(provenanceId, options.retrievedFrom)
|
|
191
|
+
: 'unchecked';
|
|
192
|
+
|
|
193
|
+
const result = { ...base, provenanceId, publicKey, location };
|
|
194
|
+
|
|
195
|
+
if (!identity) return { ...result, reason: 'No identity block' };
|
|
196
|
+
if (!publicKey) return { ...result, reason: 'identity.public_key is missing' };
|
|
197
|
+
|
|
198
|
+
const algorithm = identity.algorithm ?? ALGORITHM;
|
|
199
|
+
if (String(algorithm).toLowerCase() !== ALGORITHM) {
|
|
200
|
+
return { ...result, reason: `Unsupported algorithm: ${algorithm}` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
result.fingerprint = await keyFingerprint(publicKey);
|
|
205
|
+
} catch {
|
|
206
|
+
return { ...result, reason: 'identity.public_key is not a valid Ed25519 key' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// A key with no signature advertises which key to challenge later. It says
|
|
210
|
+
// nothing about whether this file has been altered.
|
|
211
|
+
if (!signature) {
|
|
212
|
+
return { ...result, reason: 'identity.signature is absent — key advertised, file not attested' };
|
|
213
|
+
}
|
|
214
|
+
result.signed = true;
|
|
215
|
+
|
|
216
|
+
if (!provenanceId) {
|
|
217
|
+
return { ...result, reason: 'provenance_id is required to verify a signature' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let valid;
|
|
221
|
+
try {
|
|
222
|
+
valid = await verifyEd25519(publicKey, signature, `${provenanceId}:${publicKey}`);
|
|
223
|
+
} catch {
|
|
224
|
+
return { ...result, reason: 'identity.signature is malformed' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!valid) return { ...result, reason: 'Signature does not verify' };
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
...result,
|
|
231
|
+
valid: true,
|
|
232
|
+
// Signature proves integrity; location binds it to a project someone
|
|
233
|
+
// controls. Only both together justify treating the file as the owner's.
|
|
234
|
+
trustworthy: location === 'match',
|
|
235
|
+
reason: location === 'match' ? null : 'Signature valid, but retrieval location was not confirmed',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Verify a live challenge response offline, against a key you already hold.
|
|
241
|
+
*
|
|
242
|
+
* The network equivalent in the main SDK looks the key up in the index; this
|
|
243
|
+
* takes the key directly, so a system that already stores keys can verify
|
|
244
|
+
* without contacting anyone.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} publicKeyBase64
|
|
247
|
+
* @param {string} provenanceId
|
|
248
|
+
* @param {string} nonce Single-use, unpredictable
|
|
249
|
+
* @param {string} signatureBase64
|
|
250
|
+
* @returns {Promise<boolean>}
|
|
251
|
+
*/
|
|
252
|
+
export async function verifyChallenge(publicKeyBase64, provenanceId, nonce, signatureBase64) {
|
|
253
|
+
try {
|
|
254
|
+
return await verifyEd25519(publicKeyBase64, signatureBase64, `${provenanceId}:${nonce}`);
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Verify an owner-signed revocation offline.
|
|
262
|
+
*
|
|
263
|
+
* Confirms the revocation came from the key holder. It does not tell you
|
|
264
|
+
* whether a revocation exists — that requires asking an index.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} publicKeyBase64
|
|
267
|
+
* @param {string} provenanceId
|
|
268
|
+
* @param {string} signatureBase64
|
|
269
|
+
* @returns {Promise<boolean>}
|
|
270
|
+
*/
|
|
271
|
+
export async function verifyRevocation(publicKeyBase64, provenanceId, signatureBase64) {
|
|
272
|
+
try {
|
|
273
|
+
return await verifyEd25519(publicKeyBase64, signatureBase64, `${provenanceId}:REVOKE`);
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Test vectors
|
|
2
|
+
|
|
3
|
+
Normative test data for Provenance Protocol v0.1. An implementation that
|
|
4
|
+
reproduces these results interoperates with every other implementation that
|
|
5
|
+
does, without reference to any service.
|
|
6
|
+
|
|
7
|
+
## signatures-0.1.json
|
|
8
|
+
|
|
9
|
+
Ed25519 signature vectors for the three signed messages in the protocol:
|
|
10
|
+
|
|
11
|
+
| Message | Used for |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `<provenance_id>:<public_key>` | `identity.signature` in `PROVENANCE.yml` |
|
|
14
|
+
| `<provenance_id>:<nonce>` | live proof that a running agent holds the key |
|
|
15
|
+
| `<provenance_id>:REVOKE` | owner-signed revocation |
|
|
16
|
+
|
|
17
|
+
Each vector carries the exact message, the public key, the signature, and
|
|
18
|
+
whether it must verify. Ed25519 is deterministic (RFC 8032), so signing the
|
|
19
|
+
given message with the given private key must reproduce the given signature
|
|
20
|
+
byte for byte.
|
|
21
|
+
|
|
22
|
+
The four negative vectors are the cases that matter in practice: a signature
|
|
23
|
+
from the wrong key, an altered `provenance_id`, a substituted public key
|
|
24
|
+
(the re-hosting attack), and a malformed signature.
|
|
25
|
+
|
|
26
|
+
The private key in this file is test data. Never use it for anything.
|
|
27
|
+
|
|
28
|
+
## Running them
|
|
29
|
+
|
|
30
|
+
Any Ed25519 implementation works. With Node:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { readFileSync } from 'node:fs';
|
|
34
|
+
|
|
35
|
+
const { vectors } = JSON.parse(readFileSync('signatures-0.1.json', 'utf8'));
|
|
36
|
+
|
|
37
|
+
for (const v of vectors) {
|
|
38
|
+
const key = await crypto.subtle.importKey(
|
|
39
|
+
'spki', Buffer.from(v.public_key, 'base64'), { name: 'Ed25519' }, false, ['verify']
|
|
40
|
+
);
|
|
41
|
+
let got;
|
|
42
|
+
try {
|
|
43
|
+
got = await crypto.subtle.verify(
|
|
44
|
+
'Ed25519', key, Buffer.from(v.signature, 'base64'), new TextEncoder().encode(v.message)
|
|
45
|
+
) ? 'valid' : 'invalid';
|
|
46
|
+
} catch { got = 'invalid'; }
|
|
47
|
+
console.log(got === v.expect ? `PASS ${v.id}` : `FAIL ${v.id}`);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
See [SPEC.md](../SPEC.md#signing-and-verification) for what a valid signature
|
|
52
|
+
does and does not prove.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Normative test vectors for Provenance Protocol v0.1 signatures. Ed25519 is deterministic (RFC 8032): a correct implementation reproduces these signatures byte for byte.",
|
|
3
|
+
"spec_version": "0.1",
|
|
4
|
+
"algorithm": "ed25519",
|
|
5
|
+
"encodings": {
|
|
6
|
+
"public_key": "base64 of SPKI DER",
|
|
7
|
+
"private_key": "base64 of PKCS8 DER",
|
|
8
|
+
"signature": "base64 of raw 64-byte Ed25519 signature",
|
|
9
|
+
"message": "UTF-8 bytes of the message string"
|
|
10
|
+
},
|
|
11
|
+
"keypair": {
|
|
12
|
+
"provenance_id": "provenance:github:example/research-agent",
|
|
13
|
+
"public_key": "MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
14
|
+
"private_key": "MC4CAQAwBQYDK2VwBCIEIMzkaPUByQh5c1/dZ9on5XWDP+0IBFrCyiAXAvhtBAFg",
|
|
15
|
+
"$comment": "Test key only. Never use in production."
|
|
16
|
+
},
|
|
17
|
+
"vectors": [
|
|
18
|
+
{
|
|
19
|
+
"id": "identity-signature-valid",
|
|
20
|
+
"purpose": "identity.signature in PROVENANCE.yml \u2014 binds the declaration to the key holder",
|
|
21
|
+
"message_template": "<provenance_id>:<public_key>",
|
|
22
|
+
"message": "provenance:github:example/research-agent:MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
23
|
+
"public_key": "MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
24
|
+
"signature": "TCnS9o9yiDboy3SFlG65ThO6IDAhv09rlrDWOdvX2xZj3uAI5efbIw8xIXHxErtcaJAtg3BtDvXX+bv5RhLIDQ==",
|
|
25
|
+
"expect": "valid"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "challenge-signature-valid",
|
|
29
|
+
"purpose": "live proof that a running agent controls the declared key",
|
|
30
|
+
"message_template": "<provenance_id>:<nonce>",
|
|
31
|
+
"nonce": "a7f3c1e9b2d84056",
|
|
32
|
+
"message": "provenance:github:example/research-agent:a7f3c1e9b2d84056",
|
|
33
|
+
"public_key": "MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
34
|
+
"signature": "HV/AjVj/53ZIMZkyR5Ov1p8z6t3ZkZoHlr1bgePlQRXYWCTxzKJvHfmQgI78HCmYeMSf5OaW+GcIXOUQcLhGAw==",
|
|
35
|
+
"expect": "valid"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "revocation-signature-valid",
|
|
39
|
+
"purpose": "owner-signed revocation of a provenance id",
|
|
40
|
+
"message_template": "<provenance_id>:REVOKE",
|
|
41
|
+
"message": "provenance:github:example/research-agent:REVOKE",
|
|
42
|
+
"public_key": "MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
43
|
+
"signature": "GPnNOjmVhk4Wxlq3M1KUiyAlI6nkwhq5/SeXQiRkhuLMnuLwJ0qywdlP4CpvgorGLRjP9pUfLSBLOD6Mc5E/Bw==",
|
|
44
|
+
"expect": "valid"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "identity-signature-wrong-key",
|
|
48
|
+
"purpose": "signature made by a different private key must not verify",
|
|
49
|
+
"message": "provenance:github:example/research-agent:MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
50
|
+
"public_key": "MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
51
|
+
"signature": "BqgcrtjZf2KS0p4zQwq/GA/c6jLNyx+PjOEVEZuvMC/1+kHlT4QLv7KuXWCR5vWB6mb+ap79effgYooumTnXCg==",
|
|
52
|
+
"expect": "invalid"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "identity-signature-tampered-id",
|
|
56
|
+
"purpose": "the provenance id was altered after signing",
|
|
57
|
+
"message": "provenance:github:attacker/fork:MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
58
|
+
"public_key": "MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
59
|
+
"signature": "TCnS9o9yiDboy3SFlG65ThO6IDAhv09rlrDWOdvX2xZj3uAI5efbIw8xIXHxErtcaJAtg3BtDvXX+bv5RhLIDQ==",
|
|
60
|
+
"expect": "invalid"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "identity-signature-substituted-key",
|
|
64
|
+
"purpose": "public_key swapped for another key \u2014 re-hosting attack",
|
|
65
|
+
"message": "provenance:github:example/research-agent:MCowBQYDK2VwAyEAm2AoU5BzkoMKTs+tjy8LIPu9FjivA0UbnnhekU5OK48=",
|
|
66
|
+
"public_key": "MCowBQYDK2VwAyEAm2AoU5BzkoMKTs+tjy8LIPu9FjivA0UbnnhekU5OK48=",
|
|
67
|
+
"signature": "TCnS9o9yiDboy3SFlG65ThO6IDAhv09rlrDWOdvX2xZj3uAI5efbIw8xIXHxErtcaJAtg3BtDvXX+bv5RhLIDQ==",
|
|
68
|
+
"expect": "invalid"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "identity-signature-malformed",
|
|
72
|
+
"purpose": "signature is not valid base64 of a 64-byte signature",
|
|
73
|
+
"message": "provenance:github:example/research-agent:MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
74
|
+
"public_key": "MCowBQYDK2VwAyEAfKySY8lFCvOXFoTg1nagyZKUYYvAKW+2GvYh+oD9460=",
|
|
75
|
+
"signature": "not-a-signature",
|
|
76
|
+
"expect": "invalid",
|
|
77
|
+
"note": "Implementations may either return invalid or raise an error while decoding. Both are conformant; the requirement is that it must not verify."
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|