provared 0.1.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 +1062 -0
- package/LICENSE +202 -0
- package/LICENSE-CC-BY-4.0.txt +396 -0
- package/README.md +689 -0
- package/bin/provared-check.mjs +565 -0
- package/bin/provared-headers.mjs +110 -0
- package/docs/threat-model.md +997 -0
- package/net/headers.js +403 -0
- package/package.json +57 -0
- package/page/check-page.js +162 -0
- package/page/index.html +47 -0
- package/page/render.js +530 -0
- package/page/style.css +150 -0
- package/samples/office-supplies.expected.json +82 -0
- package/samples/office-supplies.jsonl +10 -0
- package/samples/office-supplies.show.json +59 -0
- package/spec/provared-format.md +1672 -0
- package/src/actions.js +90 -0
- package/src/approval.js +73 -0
- package/src/blockstamp.js +198 -0
- package/src/book.js +2232 -0
- package/src/check.d.ts +21 -0
- package/src/check.js +15 -0
- package/src/compare.js +276 -0
- package/src/cover.js +198 -0
- package/src/der.js +192 -0
- package/src/encoding.js +286 -0
- package/src/fields.js +94 -0
- package/src/guard.js +171 -0
- package/src/headers.js +380 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +37 -0
- package/src/jws.js +235 -0
- package/src/keys.js +149 -0
- package/src/pass.js +67 -0
- package/src/passkey-browser.d.ts +20 -0
- package/src/passkey-browser.js +171 -0
- package/src/recorder.js +1115 -0
- package/src/seal.js +133 -0
- package/src/service.js +124 -0
- package/src/signatures.js +147 -0
- package/src/slip.js +401 -0
- package/src/standing.js +181 -0
- package/src/stub.js +140 -0
- package/src/timestamp.js +351 -0
- package/src/tools.js +192 -0
- package/src/tree.js +143 -0
- package/src/types.d.ts +628 -0
- package/src/webauthn.js +119 -0
package/src/seal.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The Seal: whoever keeps a book signs its top fingerprint and its number of
|
|
2
|
+
// entries, and an outside time-stamp service states when that was.
|
|
3
|
+
// Format description, section 21.
|
|
4
|
+
|
|
5
|
+
import { Refusal, fingerprint, formatTime, fromBase64url, randomId, toBase64url, utf8 } from './encoding.js';
|
|
6
|
+
import * as f from './fields.js';
|
|
7
|
+
import { KINDS, encodeContent, protectedHeaders, signingInput, withinSize } from './jws.js';
|
|
8
|
+
import { SEAL_METHODS, checkSealKeySet } from './keys.js';
|
|
9
|
+
import { sign } from './signatures.js';
|
|
10
|
+
import { MAX_STAMP_BYTES } from './timestamp.js';
|
|
11
|
+
import { treeRoot } from './tree.js';
|
|
12
|
+
|
|
13
|
+
/** The most time-stamps one seal may carry. */
|
|
14
|
+
export const MAX_STAMPS = 4;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* How far a clock may differ, in milliseconds. A seal may be dated up to this
|
|
18
|
+
* much after its time-stamp, and an entry up to this much after the
|
|
19
|
+
* time-stamp that covers it, before the checker reports it.
|
|
20
|
+
*/
|
|
21
|
+
export const CLOCK_ALLOWANCE_MS = 300 * 1000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Confirm every member of a seal's content.
|
|
25
|
+
* @param {any} c
|
|
26
|
+
*/
|
|
27
|
+
export function validateSealContent(c) {
|
|
28
|
+
f.members(c, ['by', 'id', 'root', 'size', 'type', 'when'], ['previous'], 'seal');
|
|
29
|
+
f.id(c.id, 'id');
|
|
30
|
+
f.members(c.by, ['keys', 'name'], [], 'by');
|
|
31
|
+
f.label(c.by.name, 'by.name');
|
|
32
|
+
checkSealKeySet(c.by.keys, 'by.keys');
|
|
33
|
+
f.wholeNumber(c.size, 'size');
|
|
34
|
+
if (c.size < 1) throw f.fail('size', 'must be 1 or more.');
|
|
35
|
+
f.fingerprintText(c.root, 'root');
|
|
36
|
+
if (Object.hasOwn(c, 'previous')) f.fingerprintText(c.previous, 'previous');
|
|
37
|
+
f.time(c.when, 'when');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Confirm the time-stamps that sit beside a seal or a cancellation in an
|
|
42
|
+
* entry, and decode them. A time-stamp from a service is text: the
|
|
43
|
+
* service's answer in base64url. A block time-stamp is an object: the
|
|
44
|
+
* proof and the header of its block, each in base64url.
|
|
45
|
+
* @param {any} stamps
|
|
46
|
+
* @returns {({kind: 'service', token: Uint8Array}|{kind: 'block', proof: Uint8Array, header: Uint8Array})[]}
|
|
47
|
+
*/
|
|
48
|
+
export function decodeStamps(stamps) {
|
|
49
|
+
f.list(stamps, 1, MAX_STAMPS, 'stamps');
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const items = [];
|
|
52
|
+
// Every place in the list is read by its number, a gap included, so that a gap is refused as an empty
|
|
53
|
+
// place, not skipped; a list's own way of walking through itself is not used.
|
|
54
|
+
for (let i = 0; i < stamps.length; i++) {
|
|
55
|
+
const s = stamps[i];
|
|
56
|
+
const path = `stamps[${i}]`;
|
|
57
|
+
const decode = (text, most, what) => {
|
|
58
|
+
if (typeof text !== 'string' || text.length === 0 || text.length > Math.ceil((most * 4) / 3)) throw f.fail(path, what);
|
|
59
|
+
try {
|
|
60
|
+
return fromBase64url(text);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Refusal('bad-base64url', 'A time-stamp is not base64url.');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
let item;
|
|
66
|
+
let key;
|
|
67
|
+
if (s !== null && typeof s === 'object' && !Array.isArray(s)) {
|
|
68
|
+
f.members(s, ['block', 'proof'], [], path);
|
|
69
|
+
item = {
|
|
70
|
+
kind: 'block',
|
|
71
|
+
header: decode(s.block, 1024, 'must hold a block header of 80 bytes, in base64url.'),
|
|
72
|
+
proof: decode(s.proof, MAX_STAMP_BYTES, 'must hold a proof in base64url, of at most 12,288 bytes.'),
|
|
73
|
+
};
|
|
74
|
+
key = `${s.block} ${s.proof}`;
|
|
75
|
+
} else {
|
|
76
|
+
item = { kind: 'service', token: decode(s, MAX_STAMP_BYTES, 'must be a time-stamp in base64url, of at most 12,288 bytes.') };
|
|
77
|
+
key = s;
|
|
78
|
+
}
|
|
79
|
+
if (seen.has(key)) throw f.fail('stamps', 'the same time-stamp is given twice.');
|
|
80
|
+
seen.add(key);
|
|
81
|
+
items.push(item);
|
|
82
|
+
}
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The fingerprint of a recorder's key set: how a person checking says which
|
|
88
|
+
* recorder they expect. It is the fingerprint of the list of keys, written
|
|
89
|
+
* in the canonical form.
|
|
90
|
+
* @param {object[]} keys a key set that has passed its check
|
|
91
|
+
* @returns {Promise<string>}
|
|
92
|
+
*/
|
|
93
|
+
export async function keySetFingerprint(keys) {
|
|
94
|
+
return fingerprint(encodeContent(keys).contentBytes);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Seal a book as it stands: sign its top fingerprint and its number of
|
|
99
|
+
* entries. The seal is then added to the book as its next entry, with any
|
|
100
|
+
* time-stamps beside it.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} book the book so far
|
|
103
|
+
* @param {object} fields
|
|
104
|
+
* @param {{keys: object[], name: string}} fields.by the recorder's public key set and its name (a label)
|
|
105
|
+
* @param {string} [fields.previous] the fingerprint of the seal before this one in the book, if there is one
|
|
106
|
+
* @param {number|Date} [fields.when] defaults to now
|
|
107
|
+
* @param {any[]} privateKeys the recorder's private keys, in the order of the key set
|
|
108
|
+
* @returns {Promise<{record: object, fingerprint: string, fingerprintBytes: Uint8Array}>}
|
|
109
|
+
* "fingerprintBytes" is what a time-stamp service is asked to stamp
|
|
110
|
+
*/
|
|
111
|
+
export async function writeSeal(book, fields, privateKeys) {
|
|
112
|
+
const lines = book.split('\n');
|
|
113
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
114
|
+
const content = {
|
|
115
|
+
type: KINDS.seal.type,
|
|
116
|
+
id: fields.id ?? randomId(),
|
|
117
|
+
by: fields.by,
|
|
118
|
+
size: lines.length,
|
|
119
|
+
root: toBase64url(await treeRoot(lines.map(utf8))),
|
|
120
|
+
when: formatTime(fields.when ?? Date.now()),
|
|
121
|
+
};
|
|
122
|
+
if (fields.previous) content.previous = fields.previous;
|
|
123
|
+
validateSealContent(content);
|
|
124
|
+
const { contentBytes, payloadB64 } = encodeContent(content);
|
|
125
|
+
const headers = protectedHeaders('seal');
|
|
126
|
+
const signatures = [];
|
|
127
|
+
for (let i = 0; i < SEAL_METHODS.length; i++) {
|
|
128
|
+
const signature = await sign(SEAL_METHODS[i], privateKeys[i], signingInput(headers[i], payloadB64));
|
|
129
|
+
signatures.push({ protected: headers[i], signature: toBase64url(signature) });
|
|
130
|
+
}
|
|
131
|
+
const name = await fingerprint(contentBytes);
|
|
132
|
+
return { record: withinSize({ payload: payloadB64, signatures }), fingerprint: name, fingerprintBytes: fromBase64url(name) };
|
|
133
|
+
}
|
package/src/service.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// The other side's voice: two records a service writes and signs itself.
|
|
2
|
+
// A refusal says "this agent asked for something, and we refused". Terms
|
|
3
|
+
// say which actions the service accepts from agents.
|
|
4
|
+
// Format description, sections 18 and 19.
|
|
5
|
+
|
|
6
|
+
import { actionKind, isNeverName } from './actions.js';
|
|
7
|
+
import { formatTime, randomId } from './encoding.js';
|
|
8
|
+
import * as f from './fields.js';
|
|
9
|
+
import { KINDS } from './jws.js';
|
|
10
|
+
import { checkKeySet } from './keys.js';
|
|
11
|
+
import { signWithKeySet, validateRequest } from './stub.js';
|
|
12
|
+
|
|
13
|
+
/** Why a service refused. A refusal gives exactly one of these. */
|
|
14
|
+
export const REFUSAL_REASONS = {
|
|
15
|
+
'not-in-slip': 'The slip does not cover the action, or does not name this service.',
|
|
16
|
+
'over-limit': 'The action would pass a limit of the slip.',
|
|
17
|
+
'outside-valid-time': 'The slip was not in force at that time.',
|
|
18
|
+
'slip-not-sound': 'The slip did not pass its check.',
|
|
19
|
+
'needs-approval': 'The slip asks for the person\'s approval, and none was shown.',
|
|
20
|
+
'against-terms': 'The service\'s terms for agents do not accept the action.',
|
|
21
|
+
other: 'Another reason.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function validateBy(by) {
|
|
25
|
+
f.members(by, ['keys', 'name'], [], 'by');
|
|
26
|
+
f.label(by.name, 'by.name');
|
|
27
|
+
checkKeySet(by.keys, 'by.keys');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Confirm every member of a refusal's content.
|
|
32
|
+
* @param {any} c
|
|
33
|
+
*/
|
|
34
|
+
export function validateRefusalContent(c) {
|
|
35
|
+
f.members(c, ['action', 'by', 'id', 'reason', 'slip', 'type', 'when'], ['amount'], 'refusal');
|
|
36
|
+
f.id(c.id, 'id');
|
|
37
|
+
f.fingerprintText(c.slip, 'slip');
|
|
38
|
+
validateBy(c.by);
|
|
39
|
+
// What was asked for is written down as it was asked, even a reserved
|
|
40
|
+
// name that is not on the shared list: that may be why it was refused.
|
|
41
|
+
f.shortName(c.action, 'action');
|
|
42
|
+
if (Object.hasOwn(c, 'amount')) validateRequest({ action: 'x', amount: c.amount });
|
|
43
|
+
if (typeof c.reason !== 'string' || !Object.hasOwn(REFUSAL_REASONS, c.reason)) throw f.fail('reason', 'must be one of the listed reasons.');
|
|
44
|
+
f.time(c.when, 'when');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Confirm every member of a service's terms for agents.
|
|
49
|
+
* @param {any} c
|
|
50
|
+
*/
|
|
51
|
+
export function validateTermsContent(c) {
|
|
52
|
+
f.members(c, ['accepts', 'by', 'id', 'never', 'type', 'validFrom', 'validUntil'], [], 'terms');
|
|
53
|
+
f.id(c.id, 'id');
|
|
54
|
+
validateBy(c.by);
|
|
55
|
+
f.list(c.accepts, 1, 64, 'accepts');
|
|
56
|
+
c.accepts.forEach((a, i) => f.actionName(a, `accepts[${i}]`));
|
|
57
|
+
if (new Set(c.accepts).size !== c.accepts.length) throw f.fail('accepts', 'an action name is repeated.');
|
|
58
|
+
f.list(c.never, 0, 16, 'never');
|
|
59
|
+
c.never.forEach((n, i) => {
|
|
60
|
+
if (!isNeverName(n)) throw f.fail(`never[${i}]`, 'must be a listed kind of action or a listed rule of conduct.');
|
|
61
|
+
});
|
|
62
|
+
if (new Set(c.never).size !== c.never.length) throw f.fail('never', 'a name is repeated.');
|
|
63
|
+
for (const a of c.accepts) {
|
|
64
|
+
if (c.never.includes(actionKind(a))) throw f.fail('never', 'forbids a kind of action that the terms also accept.');
|
|
65
|
+
}
|
|
66
|
+
const from = f.time(c.validFrom, 'validFrom');
|
|
67
|
+
const until = f.time(c.validUntil, 'validUntil');
|
|
68
|
+
if (!(from < until)) throw f.fail('validUntil', 'must be later than validFrom.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Write and sign a refusal: the service's statement that an agent asked for
|
|
73
|
+
* something under a slip, and was refused.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} fields
|
|
76
|
+
* @param {string} fields.slip the fingerprint of the slip the agent showed
|
|
77
|
+
* @param {{keys: object[], name: string}} fields.by the service's public key set and its name (a label)
|
|
78
|
+
* @param {string} fields.action what the agent asked for
|
|
79
|
+
* @param {{unit: string, value: number}} [fields.amount]
|
|
80
|
+
* @param {string} fields.reason one of REFUSAL_REASONS
|
|
81
|
+
* @param {number|Date} [fields.when] defaults to now
|
|
82
|
+
* @param {CryptoKey[]} privateKeys the service's private keys: Ed25519, then ML-DSA-87
|
|
83
|
+
* @returns {Promise<{record: object, fingerprint: string}>}
|
|
84
|
+
*/
|
|
85
|
+
export async function writeRefusal(fields, privateKeys) {
|
|
86
|
+
const content = {
|
|
87
|
+
type: KINDS.refusal.type,
|
|
88
|
+
id: fields.id ?? randomId(),
|
|
89
|
+
slip: fields.slip,
|
|
90
|
+
by: fields.by,
|
|
91
|
+
action: fields.action,
|
|
92
|
+
reason: fields.reason,
|
|
93
|
+
when: formatTime(fields.when ?? Date.now()),
|
|
94
|
+
};
|
|
95
|
+
if (fields.amount) content.amount = fields.amount;
|
|
96
|
+
validateRefusalContent(content);
|
|
97
|
+
return signWithKeySet('refusal', content, privateKeys);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Write and sign a service's terms for agents.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} fields
|
|
104
|
+
* @param {{keys: object[], name: string}} fields.by the service's public key set and its name (a label)
|
|
105
|
+
* @param {string[]} fields.accepts the actions the service accepts from agents
|
|
106
|
+
* @param {string[]} [fields.never] kinds of action and rules of conduct the service asks agents to keep
|
|
107
|
+
* @param {string} fields.validFrom
|
|
108
|
+
* @param {string} fields.validUntil
|
|
109
|
+
* @param {CryptoKey[]} privateKeys the service's private keys: Ed25519, then ML-DSA-87
|
|
110
|
+
* @returns {Promise<{record: object, fingerprint: string}>}
|
|
111
|
+
*/
|
|
112
|
+
export async function writeTerms(fields, privateKeys) {
|
|
113
|
+
const content = {
|
|
114
|
+
type: KINDS.terms.type,
|
|
115
|
+
id: fields.id ?? randomId(),
|
|
116
|
+
by: fields.by,
|
|
117
|
+
accepts: fields.accepts,
|
|
118
|
+
never: fields.never ?? [],
|
|
119
|
+
validFrom: fields.validFrom,
|
|
120
|
+
validUntil: fields.validUntil,
|
|
121
|
+
};
|
|
122
|
+
validateTermsContent(content);
|
|
123
|
+
return signWithKeySet('terms', content, privateKeys);
|
|
124
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Checking and making signatures with the cryptography built into the
|
|
2
|
+
// browser or Node.js (Web Crypto). No outside code.
|
|
3
|
+
//
|
|
4
|
+
// Methods:
|
|
5
|
+
// Ed25519 RFC 8032
|
|
6
|
+
// ML-DSA-87 FIPS 204, plain method, empty context (RFC 9964 section 5)
|
|
7
|
+
// ES256 ECDSA with P-256 and SHA-256 (FIPS 186-5), passkeys only
|
|
8
|
+
// RS256 RSASSA-PKCS1-v1_5 with SHA-256 (RFC 8017), passkeys only
|
|
9
|
+
// SLH-DSA-SHA2-256s FIPS 205, plain method, empty context; seals only
|
|
10
|
+
//
|
|
11
|
+
// SLH-DSA is not yet in Web Crypto anywhere. Node.js has it in its own
|
|
12
|
+
// cryptography module, which is asked for here without an import, so that
|
|
13
|
+
// this file still loads in a browser. In a browser the method is reported as
|
|
14
|
+
// not built in, which is never a pass.
|
|
15
|
+
|
|
16
|
+
import { importPublicKey } from './keys.js';
|
|
17
|
+
|
|
18
|
+
const SLH = 'SLH-DSA-SHA2-256s';
|
|
19
|
+
const nodeCrypto = globalThis.process && typeof globalThis.process.getBuiltinModule === 'function' ? globalThis.process.getBuiltinModule('node:crypto') : null;
|
|
20
|
+
|
|
21
|
+
function slhKey(pub) {
|
|
22
|
+
return nodeCrypto.createPublicKey({ key: { kty: 'AKP', alg: SLH, pub }, format: 'jwk' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @typedef {'valid'|'invalid'|'unavailable'} SignatureState */
|
|
26
|
+
|
|
27
|
+
const VERIFY_PARAMS = {
|
|
28
|
+
Ed25519: { name: 'Ed25519' },
|
|
29
|
+
'ML-DSA-87': { name: 'ML-DSA-87' },
|
|
30
|
+
ES256: { name: 'ECDSA', hash: 'SHA-256' },
|
|
31
|
+
RS256: { name: 'RSASSA-PKCS1-v1_5' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Whether this device has a method built in. Ed25519 and ML-DSA are newer
|
|
35
|
+
// additions to Web Crypto, so they are probed once by making a key. ES256 and
|
|
36
|
+
// RS256 have been in Web Crypto from the start.
|
|
37
|
+
const available = new Map();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} alg
|
|
41
|
+
* @returns {Promise<boolean>} whether this device can check signatures made with this method
|
|
42
|
+
*/
|
|
43
|
+
export function methodAvailable(alg) {
|
|
44
|
+
if (alg === 'ES256' || alg === 'RS256') return Promise.resolve(true);
|
|
45
|
+
if (alg === SLH && !available.has(alg)) {
|
|
46
|
+
let has = false;
|
|
47
|
+
try {
|
|
48
|
+
// An all-zero key is a key of the right shape: loading it shows whether the method is there.
|
|
49
|
+
slhKey('A'.repeat(86));
|
|
50
|
+
has = true;
|
|
51
|
+
} catch {
|
|
52
|
+
has = false;
|
|
53
|
+
}
|
|
54
|
+
available.set(alg, Promise.resolve(has));
|
|
55
|
+
}
|
|
56
|
+
if (!available.has(alg)) {
|
|
57
|
+
const subtle = globalThis.crypto && globalThis.crypto.subtle;
|
|
58
|
+
const probe = subtle
|
|
59
|
+
? Promise.resolve()
|
|
60
|
+
.then(() => subtle.generateKey({ name: alg }, false, ['sign', 'verify']))
|
|
61
|
+
.then(() => true, () => false)
|
|
62
|
+
: Promise.resolve(false);
|
|
63
|
+
available.set(alg, probe);
|
|
64
|
+
}
|
|
65
|
+
return available.get(alg);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check one signature.
|
|
70
|
+
*
|
|
71
|
+
* "unavailable" means this device has no built-in support for the method. It
|
|
72
|
+
* is never treated as a pass. On a device that has the method, any failure,
|
|
73
|
+
* including a key the device refuses to load, is "invalid".
|
|
74
|
+
*
|
|
75
|
+
* @param {any} jwk a public key that has passed checkKey
|
|
76
|
+
* @param {Uint8Array} signature
|
|
77
|
+
* @param {Uint8Array} data the bytes that were signed
|
|
78
|
+
* @param {string[]} [without] methods to treat as not built in, to see how a
|
|
79
|
+
* check behaves on a device without them; it can only withhold a pass
|
|
80
|
+
* @returns {Promise<SignatureState>}
|
|
81
|
+
*/
|
|
82
|
+
export async function verifySignature(jwk, signature, data, without = []) {
|
|
83
|
+
if (jwk.alg === SLH) {
|
|
84
|
+
if (without.includes(SLH) || !(await methodAvailable(SLH))) return 'unavailable';
|
|
85
|
+
try {
|
|
86
|
+
return nodeCrypto.verify(null, data, slhKey(jwk.pub), signature) ? 'valid' : 'invalid';
|
|
87
|
+
} catch {
|
|
88
|
+
return 'invalid';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!Object.hasOwn(VERIFY_PARAMS, jwk.alg)) return 'invalid';
|
|
92
|
+
const params = VERIFY_PARAMS[jwk.alg];
|
|
93
|
+
if (without.includes(jwk.alg) || !(await methodAvailable(jwk.alg))) return 'unavailable';
|
|
94
|
+
try {
|
|
95
|
+
const key = await importPublicKey(jwk);
|
|
96
|
+
return (await globalThis.crypto.subtle.verify(params, key, signature, data)) ? 'valid' : 'invalid';
|
|
97
|
+
} catch {
|
|
98
|
+
return 'invalid';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sign with one private key of a key set.
|
|
104
|
+
* @param {string} alg 'Ed25519', 'ML-DSA-87' or, for a seal, 'SLH-DSA-SHA2-256s'
|
|
105
|
+
* @param {any} privateKey
|
|
106
|
+
* @param {Uint8Array} data
|
|
107
|
+
* @returns {Promise<Uint8Array>}
|
|
108
|
+
*/
|
|
109
|
+
export async function sign(alg, privateKey, data) {
|
|
110
|
+
if (alg === SLH) return new Uint8Array(nodeCrypto.sign(null, data, privateKey));
|
|
111
|
+
return new Uint8Array(await globalThis.crypto.subtle.sign({ name: alg }, privateKey, data));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Make a new key set for a recorder, which signs seals: an Ed25519 key, an
|
|
116
|
+
* ML-DSA-87 key and an SLH-DSA-SHA2-256s key. It needs Node.js: no browser
|
|
117
|
+
* has the third method.
|
|
118
|
+
* @returns {Promise<{keys: object[], privateKeys: any[]}>}
|
|
119
|
+
*/
|
|
120
|
+
export async function generateSealKeySet() {
|
|
121
|
+
if (!(await methodAvailable(SLH))) throw new Error('SLH-DSA is not built into this device.');
|
|
122
|
+
const two = await generateKeySet();
|
|
123
|
+
const slh = nodeCrypto.generateKeyPairSync('slh-dsa-sha2-256s');
|
|
124
|
+
const { pub } = slh.publicKey.export({ format: 'jwk' });
|
|
125
|
+
return { keys: [...two.keys, { alg: SLH, kty: 'AKP', pub }], privateKeys: [...two.privateKeys, slh.privateKey] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Make a new key set for an agent or a service: an Ed25519 key and an
|
|
130
|
+
* ML-DSA-87 key. The private halves cannot be exported from the returned
|
|
131
|
+
* objects; they stay in the memory of this process.
|
|
132
|
+
* @returns {Promise<{keys: object[], privateKeys: CryptoKey[]}>} the public key set, and the private keys in the same order
|
|
133
|
+
*/
|
|
134
|
+
export async function generateKeySet() {
|
|
135
|
+
const subtle = globalThis.crypto.subtle;
|
|
136
|
+
const ed = await subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify']);
|
|
137
|
+
const ml = await subtle.generateKey({ name: 'ML-DSA-87' }, false, ['sign', 'verify']);
|
|
138
|
+
const edJwk = await subtle.exportKey('jwk', ed.publicKey);
|
|
139
|
+
const mlJwk = await subtle.exportKey('jwk', ml.publicKey);
|
|
140
|
+
return {
|
|
141
|
+
keys: [
|
|
142
|
+
{ alg: 'Ed25519', crv: 'Ed25519', kty: 'OKP', x: edJwk.x },
|
|
143
|
+
{ alg: 'ML-DSA-87', kty: 'AKP', pub: mlJwk.pub },
|
|
144
|
+
],
|
|
145
|
+
privateKeys: [ed.privateKey, ml.privateKey],
|
|
146
|
+
};
|
|
147
|
+
}
|