provared 0.2.0 → 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/CHANGELOG.md +85 -0
- package/README.md +116 -653
- package/bin/provared-check.mjs +36 -1
- package/docs/guide.md +736 -0
- package/package.json +25 -2
- package/src/dev.d.ts +54 -0
- package/src/dev.js +123 -0
package/package.json
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "provared",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Proof of what an AI agent was allowed to do, and what it then did. Evidence, not a verdict.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Proof of what an AI agent was allowed to do, and what it then did. A passkey-signed permission with limits a machine can check, a signed receipt for each action, and a free checker. Evidence, not a verdict.",
|
|
5
5
|
"author": "Pavel Izmaylov",
|
|
6
6
|
"license": "Apache-2.0 AND CC-BY-4.0",
|
|
7
|
+
"homepage": "https://prova.red",
|
|
8
|
+
"bugs": "https://github.com/provared/provared/issues",
|
|
7
9
|
"repository": {
|
|
8
10
|
"type": "git",
|
|
9
11
|
"url": "git+https://github.com/provared/provared.git"
|
|
10
12
|
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai-agent",
|
|
15
|
+
"agent",
|
|
16
|
+
"audit-trail",
|
|
17
|
+
"permission",
|
|
18
|
+
"authorization",
|
|
19
|
+
"receipt",
|
|
20
|
+
"tamper-evident",
|
|
21
|
+
"passkey",
|
|
22
|
+
"webauthn",
|
|
23
|
+
"jws",
|
|
24
|
+
"ed25519",
|
|
25
|
+
"ml-dsa",
|
|
26
|
+
"post-quantum",
|
|
27
|
+
"evidence",
|
|
28
|
+
"langchain"
|
|
29
|
+
],
|
|
11
30
|
"files": [
|
|
12
31
|
"src/",
|
|
13
32
|
"bin/",
|
|
@@ -38,6 +57,10 @@
|
|
|
38
57
|
"types": "./src/passkey-browser.d.ts",
|
|
39
58
|
"default": "./src/passkey-browser.js"
|
|
40
59
|
},
|
|
60
|
+
"./dev": {
|
|
61
|
+
"types": "./src/dev.d.ts",
|
|
62
|
+
"default": "./src/dev.js"
|
|
63
|
+
},
|
|
41
64
|
"./package.json": "./package.json"
|
|
42
65
|
},
|
|
43
66
|
"bin": {
|
package/src/dev.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Type declarations for "provared/dev": a development stand-in for a
|
|
2
|
+
// passkey. It is not a passkey, and no person confirms anything through
|
|
3
|
+
// it. Every slip it signs carries " (development)" in its issuer's name.
|
|
4
|
+
|
|
5
|
+
import type { Assertion, KeySet, Limit, Recorder, CheckOptions, Es256Key, Fingerprint, SignedRecord, SlipFields } from './types.js';
|
|
6
|
+
|
|
7
|
+
/** The words every development slip carries in its issuer's name: "(development)". */
|
|
8
|
+
export const DEVELOPMENT_MARK: string;
|
|
9
|
+
|
|
10
|
+
export interface DevelopmentPasskey {
|
|
11
|
+
key: Es256Key;
|
|
12
|
+
rpId: 'localhost';
|
|
13
|
+
origin: 'http://localhost';
|
|
14
|
+
/** The three values a passkey returns, for a challenge from prepareSlip, prepareApproval or prepareCancellation. */
|
|
15
|
+
sign(challenge: Uint8Array): Promise<Assertion>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A development stand-in for a passkey (ES256), signing for http://localhost. */
|
|
19
|
+
export function developmentPasskey(): Promise<DevelopmentPasskey>;
|
|
20
|
+
|
|
21
|
+
export interface DevelopmentSlip {
|
|
22
|
+
slip: SignedRecord;
|
|
23
|
+
slipFingerprint: Fingerprint;
|
|
24
|
+
/** A book holding the slip alone: what openRecorder takes. */
|
|
25
|
+
book: string;
|
|
26
|
+
/** The thumbprint of the stand-in passkey, to hand to a checker as the passkey you trust. */
|
|
27
|
+
issuerKeys: string[];
|
|
28
|
+
agent: { keys: KeySet; privateKeys: CryptoKey[] };
|
|
29
|
+
passkey: DevelopmentPasskey;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The members of a development slip: as for prepareSlip, except that the
|
|
34
|
+
* issuer's key and page address, and the agent's keys, are filled in.
|
|
35
|
+
*/
|
|
36
|
+
export type DevelopmentSlipFields = Omit<SlipFields, 'issuer' | 'agent' | 'validFrom' | 'validUntil' | 'purpose' | 'with' | 'limits'> & {
|
|
37
|
+
issuer?: { name?: string };
|
|
38
|
+
agent?: { name?: string; software?: SlipFields['agent']['software'] };
|
|
39
|
+
/** Defaults to an empty list. */
|
|
40
|
+
limits?: Limit[];
|
|
41
|
+
validFrom?: string;
|
|
42
|
+
validUntil?: string;
|
|
43
|
+
purpose?: string;
|
|
44
|
+
with?: SlipFields['with'];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** A slip signed with a development stand-in for a passkey, with new keys for the agent. */
|
|
48
|
+
export function developmentSlip(fields: DevelopmentSlipFields): Promise<DevelopmentSlip>;
|
|
49
|
+
|
|
50
|
+
/** The stub writer opened under a development slip, in one call. */
|
|
51
|
+
export function developmentRecorder(
|
|
52
|
+
fields: DevelopmentSlipFields,
|
|
53
|
+
more?: { options?: CheckOptions; now?: () => number; countersignWithin?: number },
|
|
54
|
+
): Promise<Omit<DevelopmentSlip, 'book'> & { recorder: Recorder }>;
|
package/src/dev.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// A development stand-in for a passkey, and a stub writer opened under a
|
|
2
|
+
// slip it signed: for trying the library in an evening, and for tests.
|
|
3
|
+
//
|
|
4
|
+
// It is NOT a passkey. Its private key is an ordinary key made in this
|
|
5
|
+
// process's memory, and no person confirmed anything. It returns the same
|
|
6
|
+
// three values a passkey returns (the authenticator data, the client data,
|
|
7
|
+
// the signature), in the forms the W3C Web Authentication standard sets
|
|
8
|
+
// out, so that every record it signs passes the checker exactly as a real
|
|
9
|
+
// one would. To keep such a record from being mistaken for a person's, the
|
|
10
|
+
// name of the issuer in every slip it signs ends with " (development)",
|
|
11
|
+
// and the checker prints that name beside the slip.
|
|
12
|
+
//
|
|
13
|
+
// A real slip is signed by the person, in a browser, with a real passkey:
|
|
14
|
+
// see passkey-browser.js and the checking page. Nothing in this file is
|
|
15
|
+
// part of the library's checker, and nothing here reaches the network.
|
|
16
|
+
|
|
17
|
+
import { concatBytes, fingerprint, formatTime, fromBase64url, sha256, toBase64url, utf8 } from './encoding.js';
|
|
18
|
+
import { thumbprint } from './keys.js';
|
|
19
|
+
import { openRecorder } from './recorder.js';
|
|
20
|
+
import { generateKeySet } from './signatures.js';
|
|
21
|
+
import { assembleSlip, prepareSlip } from './slip.js';
|
|
22
|
+
import { writeBook } from './book.js';
|
|
23
|
+
|
|
24
|
+
const subtle = globalThis.crypto.subtle;
|
|
25
|
+
|
|
26
|
+
/** The words every development slip carries in its issuer's name. */
|
|
27
|
+
export const DEVELOPMENT_MARK = '(development)';
|
|
28
|
+
|
|
29
|
+
// Two numbers of 32 bytes each, side by side, to the ASN.1 DER form a
|
|
30
|
+
// passkey returns for an ECDSA signature.
|
|
31
|
+
function ecdsaRawToDer(raw) {
|
|
32
|
+
const part = (bytes) => {
|
|
33
|
+
let start = 0;
|
|
34
|
+
while (start < bytes.length - 1 && bytes[start] === 0) start++;
|
|
35
|
+
let digits = bytes.subarray(start);
|
|
36
|
+
if (digits[0] & 0x80) digits = concatBytes(new Uint8Array([0]), digits);
|
|
37
|
+
return concatBytes(new Uint8Array([0x02, digits.length]), digits);
|
|
38
|
+
};
|
|
39
|
+
const body = concatBytes(part(raw.subarray(0, 32)), part(raw.subarray(32)));
|
|
40
|
+
return concatBytes(new Uint8Array([0x30, body.length]), body);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A development stand-in for a passkey (ES256, the method most passkeys
|
|
45
|
+
* use). It signs on this computer, for the page address http://localhost,
|
|
46
|
+
* which the format allows for development only.
|
|
47
|
+
*
|
|
48
|
+
* @returns {Promise<{key: object, rpId: string, origin: string, sign: (challenge: Uint8Array) => Promise<{authenticatorData: Uint8Array, clientDataJSON: Uint8Array, signature: Uint8Array}>}>}
|
|
49
|
+
*/
|
|
50
|
+
export async function developmentPasskey() {
|
|
51
|
+
const rpId = 'localhost';
|
|
52
|
+
const origin = 'http://localhost';
|
|
53
|
+
const pair = await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign', 'verify']);
|
|
54
|
+
const j = await subtle.exportKey('jwk', pair.publicKey);
|
|
55
|
+
const key = { alg: 'ES256', crv: 'P-256', kty: 'EC', x: j.x, y: j.y };
|
|
56
|
+
return {
|
|
57
|
+
key,
|
|
58
|
+
rpId,
|
|
59
|
+
origin,
|
|
60
|
+
async sign(challenge) {
|
|
61
|
+
const clientDataJSON = utf8(JSON.stringify({ type: 'webauthn.get', challenge: toBase64url(challenge), origin, crossOrigin: false }));
|
|
62
|
+
// Flags 0x05: "user present" and "user verified". Nobody was: this is a stand-in.
|
|
63
|
+
const authenticatorData = concatBytes(await sha256(utf8(rpId)), new Uint8Array([0x05, 0, 0, 0, 1]));
|
|
64
|
+
const signed = concatBytes(authenticatorData, await sha256(clientDataJSON));
|
|
65
|
+
const signature = ecdsaRawToDer(new Uint8Array(await subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, pair.privateKey, signed)));
|
|
66
|
+
return { authenticatorData, clientDataJSON, signature };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The issuer's name always ends with the development mark, whatever name is given.
|
|
72
|
+
function markedName(name) {
|
|
73
|
+
const given = typeof name === 'string' && name.trim() ? name.trim() : 'Development passkey';
|
|
74
|
+
return given.endsWith(DEVELOPMENT_MARK) ? given : `${given} ${DEVELOPMENT_MARK}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A slip signed with a development stand-in for a passkey, and new keys for
|
|
79
|
+
* the agent. Give the slip's actions, limits, conditions and the rest as for
|
|
80
|
+
* prepareSlip; "issuer" and "agent.keys" are filled in, and the issuer's
|
|
81
|
+
* name is made to end with " (development)". By default the slip runs from
|
|
82
|
+
* a minute ago for one day, and its purpose says that it is for development.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} fields the members of the slip; "actions" is required
|
|
85
|
+
* @returns {Promise<{slip: object, slipFingerprint: string, book: string, issuerKeys: string[], agent: {keys: object[], privateKeys: CryptoKey[]}, passkey: object}>}
|
|
86
|
+
*/
|
|
87
|
+
export async function developmentSlip(fields = {}) {
|
|
88
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) throw new TypeError('developmentSlip: give the members of the slip as an object, with at least "actions".');
|
|
89
|
+
const passkey = await developmentPasskey();
|
|
90
|
+
const agent = await generateKeySet();
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const issuer = fields.issuer && typeof fields.issuer === 'object' ? fields.issuer : {};
|
|
93
|
+
const agentGiven = fields.agent && typeof fields.agent === 'object' ? fields.agent : {};
|
|
94
|
+
const prepared = await prepareSlip({
|
|
95
|
+
validFrom: formatTime(now - 60 * 1000),
|
|
96
|
+
validUntil: formatTime(now + 24 * 3600 * 1000),
|
|
97
|
+
purpose: 'For development: a record made with a stand-in for a passkey, which no person signed.',
|
|
98
|
+
limits: [],
|
|
99
|
+
with: [],
|
|
100
|
+
...fields,
|
|
101
|
+
issuer: { name: markedName(issuer.name), key: passkey.key, rpId: passkey.rpId, origin: passkey.origin },
|
|
102
|
+
agent: { name: typeof agentGiven.name === 'string' ? agentGiven.name : 'Development agent', ...agentGiven, keys: agent.keys },
|
|
103
|
+
});
|
|
104
|
+
const slip = assembleSlip(prepared, await passkey.sign(prepared.challenge));
|
|
105
|
+
const slipFingerprint = await fingerprint(fromBase64url(slip.payload));
|
|
106
|
+
return { slip, slipFingerprint, book: writeBook([{ slip }]), issuerKeys: [await thumbprint(passkey.key)], agent, passkey };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The stub writer, opened under a development slip, in one call: the
|
|
111
|
+
* shortest way to a first record. The same as developmentSlip followed by
|
|
112
|
+
* openRecorder; "more" is passed to openRecorder (options, now,
|
|
113
|
+
* countersignWithin).
|
|
114
|
+
*
|
|
115
|
+
* @param {object} fields the members of the slip; "actions" is required
|
|
116
|
+
* @param {object} [more]
|
|
117
|
+
* @returns {Promise<{recorder: object, slip: object, slipFingerprint: string, issuerKeys: string[], agent: {keys: object[], privateKeys: CryptoKey[]}, passkey: object}>}
|
|
118
|
+
*/
|
|
119
|
+
export async function developmentRecorder(fields = {}, more = {}) {
|
|
120
|
+
const made = await developmentSlip(fields);
|
|
121
|
+
const recorder = await openRecorder({ book: made.book, slip: made.slipFingerprint, privateKeys: made.agent.privateKeys, issuerKeys: made.issuerKeys, ...more });
|
|
122
|
+
return { recorder, slip: made.slip, slipFingerprint: made.slipFingerprint, issuerKeys: made.issuerKeys, agent: made.agent, passkey: made.passkey };
|
|
123
|
+
}
|