noqtis 0.1.0 → 0.1.2
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 +70 -7
- package/dist/noqtis.js +1 -1
- package/dist/noqtis.wasm +0 -0
- package/lib/index.cjs +13 -0
- package/lib/index.d.cts +105 -6
- package/lib/index.d.ts +107 -7
- package/lib/index.js +396 -24
- package/lib/node.d.cts +7 -4
- package/lib/node.d.ts +7 -4
- package/lib/node.js +11 -2
- package/package.json +2 -2
package/lib/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type SignAlg =
|
|
|
12
12
|
| 'SLH-DSA-SHAKE-128s' | 'SLH-DSA-SHAKE-128f'
|
|
13
13
|
| 'SLH-DSA-SHAKE-192s' | 'SLH-DSA-SHAKE-192f'
|
|
14
14
|
| 'SLH-DSA-SHAKE-256s' | 'SLH-DSA-SHAKE-256f'
|
|
15
|
+
| 'ML-DSA-44+Ed25519' | 'ML-DSA-65+Ed25519' | 'ML-DSA-87+Ed25519'
|
|
16
|
+
| 'Ed25519'
|
|
15
17
|
| (string & {});
|
|
16
18
|
|
|
17
19
|
export type KemAlg =
|
|
@@ -61,7 +63,7 @@ export interface SignOptions {
|
|
|
61
63
|
ctx?: Data;
|
|
62
64
|
/** All-zero per-signature randomness (the deterministic variant). */
|
|
63
65
|
deterministic?: boolean;
|
|
64
|
-
/**
|
|
66
|
+
/** Per-signature randomness: 32 bytes ML-DSA and hybrids, n SLH-DSA. */
|
|
65
67
|
rnd?: Bytes;
|
|
66
68
|
/** HashML-DSA / HashSLH-DSA with this hash; msg is hashed inside. */
|
|
67
69
|
prehash?: HashAlg;
|
|
@@ -73,7 +75,11 @@ export interface VerifyOptions {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export const sign: {
|
|
76
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* seed: xi (32 bytes) for ML-DSA, SK.seed||SK.prf||PK.seed for SLH-DSA,
|
|
80
|
+
* the 32-byte private key for Ed25519, xi||the Ed25519 key (64 bytes)
|
|
81
|
+
* for a hybrid.
|
|
82
|
+
*/
|
|
77
83
|
keygen(alg: SignAlg, opts?: { seed?: Bytes }): KeyPair;
|
|
78
84
|
sign(alg: SignAlg, secretKey: Bytes, msg: Data, opts?: SignOptions): Bytes;
|
|
79
85
|
/** false when the signature does not verify or is malformed. */
|
|
@@ -109,12 +115,105 @@ export interface CheckResult {
|
|
|
109
115
|
checks: CheckRow[];
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
export interface CheckOptions {
|
|
119
|
+
/** The public key the seal must carry; without it the seal's own. */
|
|
120
|
+
trust?: Bytes;
|
|
121
|
+
/** DER certificates of TSA trust anchors, at most 32. */
|
|
122
|
+
anchors?: Bytes[];
|
|
123
|
+
/** Check just these paths; "partial" rows say what was left out. */
|
|
124
|
+
only?: string[];
|
|
125
|
+
/** Capsules among which a format 2 capsule's inputs are found by id. */
|
|
126
|
+
lineage?: Files[];
|
|
127
|
+
/** Key statements from capsule.rotate / capsule.revoke; need trust. */
|
|
128
|
+
keys?: Data[];
|
|
129
|
+
/** A proof from capsule.prove, checked instead of (or with) the files. */
|
|
130
|
+
proof?: Data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface SealOptions {
|
|
134
|
+
/** A JSON value, or JSON text, kept in the manifest. */
|
|
135
|
+
meta?: unknown;
|
|
136
|
+
/** Creation time in seconds since 1970; default the clock. */
|
|
137
|
+
time?: number;
|
|
138
|
+
/** 1 (the default) or 2: a Merkle tree over the files. */
|
|
139
|
+
format?: 1 | 2;
|
|
140
|
+
/** Format 2: the capsules this one was made from, by capsule.id. */
|
|
141
|
+
inputs?: { name: string; id: Bytes }[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface KeyOptions {
|
|
145
|
+
/** When it takes effect: seconds since 1970 or a Date; default now. */
|
|
146
|
+
at?: number | Date;
|
|
147
|
+
/** Up to 255 bytes of text. */
|
|
148
|
+
reason?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface FileEntry {
|
|
152
|
+
path: string;
|
|
153
|
+
size: number;
|
|
154
|
+
/** 64 hex digits. */
|
|
155
|
+
sha256: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
112
158
|
export const capsule: {
|
|
113
|
-
|
|
114
|
-
|
|
159
|
+
/** files may be null when opts.proof is given. */
|
|
160
|
+
check(files: Files | null, opts?: CheckOptions): CheckResult;
|
|
115
161
|
seal(files: Files, alg: SignAlg, secretKey: Bytes, publicKey: Bytes,
|
|
116
|
-
opts?: {
|
|
117
|
-
|
|
162
|
+
opts?: SealOptions): { 'noqtis.json': Bytes; 'noqtis.sig': Bytes };
|
|
163
|
+
/** The capsule's id (32 bytes); nothing is checked. */
|
|
164
|
+
id(files: Files): Bytes;
|
|
165
|
+
/** A proof (JSON bytes) that paths are files of a format 2 capsule. */
|
|
166
|
+
prove(files: Files, paths: string | string[]): Bytes;
|
|
167
|
+
/** The files the manifest lists; nothing is checked. */
|
|
168
|
+
files(files: Files): FileEntry[];
|
|
169
|
+
/** A key statement (JSON bytes): the key pair is replaced by the new. */
|
|
170
|
+
rotate(alg: SignAlg, secretKey: Bytes, publicKey: Bytes, newAlg: SignAlg,
|
|
171
|
+
newSecretKey: Bytes, newPublicKey: Bytes, opts?: KeyOptions): Bytes;
|
|
172
|
+
/** A key statement (JSON bytes) that the key pair is revoked. */
|
|
173
|
+
revoke(alg: SignAlg, secretKey: Bytes, publicKey: Bytes,
|
|
174
|
+
opts?: KeyOptions): Bytes;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const dsse: {
|
|
178
|
+
/** A DSSE envelope (canonical JSON bytes) signed by the key pair. */
|
|
179
|
+
sign(alg: SignAlg, secretKey: Bytes, publicKey: Bytes, type: string,
|
|
180
|
+
payload: Data): Bytes;
|
|
181
|
+
/** The payload and its type, or null when no signature verifies. */
|
|
182
|
+
verify(alg: SignAlg, publicKey: Bytes, envelope: Data):
|
|
183
|
+
{ payload: Bytes; payloadType: string } | null;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export interface CardFields {
|
|
187
|
+
/** 1 to 255 bytes of text, no control character, no edge spaces. */
|
|
188
|
+
name: string;
|
|
189
|
+
/** An ORCID iD or its https://orcid.org/ address; check digit verified. */
|
|
190
|
+
orcid?: string | null;
|
|
191
|
+
email?: string | null;
|
|
192
|
+
/** An https:// address. */
|
|
193
|
+
url?: string | null;
|
|
194
|
+
affil?: string | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface Card {
|
|
198
|
+
name: string;
|
|
199
|
+
orcid: string | null;
|
|
200
|
+
email: string | null;
|
|
201
|
+
url: string | null;
|
|
202
|
+
affil: string | null;
|
|
203
|
+
/** YYYY-MM-DDTHH:MM:SSZ */
|
|
204
|
+
created: string;
|
|
205
|
+
alg: string;
|
|
206
|
+
publicKey: Bytes;
|
|
207
|
+
/** The SHA-256 of publicKey, hex. */
|
|
208
|
+
keySha256: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const idcard: {
|
|
212
|
+
/** An identity card (JSON bytes) for the key pair, signed by it. */
|
|
213
|
+
make(alg: SignAlg, secretKey: Bytes, publicKey: Bytes, fields: CardFields,
|
|
214
|
+
opts?: { at?: number | Date }): Bytes;
|
|
215
|
+
/** The card, or null when it does not verify or is not trust's. */
|
|
216
|
+
verify(card: Data, opts?: { trust?: Bytes | null }): Card | null;
|
|
118
217
|
};
|
|
119
218
|
|
|
120
219
|
export const keyfile: {
|
|
@@ -140,7 +239,8 @@ declare const _default: {
|
|
|
140
239
|
init: typeof init; version: typeof version; algs: typeof algs;
|
|
141
240
|
random: typeof random; hash: typeof hash; hmac: typeof hmac;
|
|
142
241
|
sign: typeof sign; kem: typeof kem; capsule: typeof capsule;
|
|
143
|
-
|
|
242
|
+
dsse: typeof dsse; idcard: typeof idcard; keyfile: typeof keyfile;
|
|
243
|
+
NoqtisError: typeof NoqtisError;
|
|
144
244
|
codes: typeof codes; errmsg: typeof errmsg;
|
|
145
245
|
};
|
|
146
246
|
export default _default;
|
package/lib/index.js
CHANGED
|
@@ -242,7 +242,7 @@ export function algs() {
|
|
|
242
242
|
return out;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
/** The library version, "0.1.
|
|
245
|
+
/** The library version, "0.1.2". */
|
|
246
246
|
export function version() {
|
|
247
247
|
need();
|
|
248
248
|
return M.UTF8ToString(M._nqver());
|
|
@@ -348,7 +348,9 @@ export const sign = Object.freeze({
|
|
|
348
348
|
const pn = h.alloc(4);
|
|
349
349
|
let rc;
|
|
350
350
|
if (opts.rnd !== undefined || opts.deterministic) {
|
|
351
|
-
|
|
351
|
+
// 32 for ML-DSA and the hybrids, none for Ed25519, n for SLH-DSA
|
|
352
|
+
const n = a.name === 'Ed25519' ? 0
|
|
353
|
+
: a.name.startsWith('ML-DSA') ? 32 : a.pk / 2;
|
|
352
354
|
const r = opts.rnd !== undefined ? key(opts.rnd, 'rnd')
|
|
353
355
|
: new Uint8Array(n);
|
|
354
356
|
const pr = h.put(r);
|
|
@@ -473,10 +475,8 @@ function rmtree(path) {
|
|
|
473
475
|
}
|
|
474
476
|
}
|
|
475
477
|
|
|
476
|
-
// Writes files under a fresh folder
|
|
477
|
-
function
|
|
478
|
-
need();
|
|
479
|
-
const list = entries(files);
|
|
478
|
+
// Writes the parsed files under a fresh folder and returns its path.
|
|
479
|
+
function mkcap(list) {
|
|
480
480
|
const dir = `/nq${++fsn}`;
|
|
481
481
|
M.FS.mkdir(dir);
|
|
482
482
|
try {
|
|
@@ -497,12 +497,30 @@ function withdir(files, fn) {
|
|
|
497
497
|
}
|
|
498
498
|
M.FS.writeFile(f, data);
|
|
499
499
|
}
|
|
500
|
-
|
|
501
|
-
} finally {
|
|
500
|
+
} catch (e) {
|
|
502
501
|
rmtree(dir);
|
|
502
|
+
throw e;
|
|
503
|
+
}
|
|
504
|
+
return dir;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Writes each capsule under a fresh folder, runs fn(dirs), removes them.
|
|
508
|
+
function withdirs(caps, fn) {
|
|
509
|
+
need();
|
|
510
|
+
const lists = caps.map(entries);
|
|
511
|
+
const dirs = [];
|
|
512
|
+
try {
|
|
513
|
+
for (const l of lists) dirs.push(mkcap(l));
|
|
514
|
+
return fn(dirs);
|
|
515
|
+
} finally {
|
|
516
|
+
for (const d of dirs) rmtree(d);
|
|
503
517
|
}
|
|
504
518
|
}
|
|
505
519
|
|
|
520
|
+
function withdir(files, fn) {
|
|
521
|
+
return withdirs([files], (d) => fn(d[0]));
|
|
522
|
+
}
|
|
523
|
+
|
|
506
524
|
function report(h, rp) {
|
|
507
525
|
const r = h.u32(rp);
|
|
508
526
|
const checks = [];
|
|
@@ -527,30 +545,143 @@ function report(h, rp) {
|
|
|
527
545
|
return checks;
|
|
528
546
|
}
|
|
529
547
|
|
|
548
|
+
// An array of byte strings in WebAssembly memory: { p, l, n }, the
|
|
549
|
+
// pointer and length arrays (0 when empty) and the count.
|
|
550
|
+
function blist(h, xs, what, str) {
|
|
551
|
+
if (!Array.isArray(xs)) {
|
|
552
|
+
throw new TypeError(`noqtis: ${what} must be an array`);
|
|
553
|
+
}
|
|
554
|
+
const p = h.alloc(4 * xs.length);
|
|
555
|
+
const l = h.alloc(4 * xs.length);
|
|
556
|
+
xs.forEach((x, i) => {
|
|
557
|
+
let b;
|
|
558
|
+
if (str) {
|
|
559
|
+
if (typeof x !== 'string' || x.includes('\0')) {
|
|
560
|
+
throw new TypeError(`noqtis: ${what} must be strings`);
|
|
561
|
+
}
|
|
562
|
+
b = enc.encode(x);
|
|
563
|
+
const q = h.alloc(b.length + 1);
|
|
564
|
+
M.HEAPU8.set(b, q);
|
|
565
|
+
M.HEAPU8[q + b.length] = 0;
|
|
566
|
+
M.HEAPU32[(p >>> 2) + i] = q;
|
|
567
|
+
} else {
|
|
568
|
+
b = bytes(x, what);
|
|
569
|
+
M.HEAPU32[(p >>> 2) + i] = h.put(b);
|
|
570
|
+
}
|
|
571
|
+
M.HEAPU32[(l >>> 2) + i] = b.length;
|
|
572
|
+
});
|
|
573
|
+
const n = xs.length;
|
|
574
|
+
return { p: n ? p : 0, l: n ? l : 0, n };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// A call that sets *outlen also with NQEBUF: once for the size, then
|
|
578
|
+
// again with that much room.
|
|
579
|
+
function sized(h, fn, guess) {
|
|
580
|
+
const pn = h.alloc(4);
|
|
581
|
+
let cap = guess;
|
|
582
|
+
for (let k = 0; k < 2; k++) {
|
|
583
|
+
const po = h.alloc(cap);
|
|
584
|
+
const rc = fn(po, cap, pn);
|
|
585
|
+
const n = h.u32(pn);
|
|
586
|
+
if (rc === CODES.EBUF && n > cap && k === 0) {
|
|
587
|
+
cap = n;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
check(rc);
|
|
591
|
+
return h.get(po, n);
|
|
592
|
+
}
|
|
593
|
+
throw new NoqtisError(CODES.EBUF);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function capsrc(x) {
|
|
597
|
+
if (x === null || x === undefined) return null;
|
|
598
|
+
return x;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function epoch(at) {
|
|
602
|
+
if (at === undefined) return BigInt(Math.floor(Date.now() / 1000));
|
|
603
|
+
if (at instanceof Date) return BigInt(Math.floor(at.getTime() / 1000));
|
|
604
|
+
if (!Number.isSafeInteger(at)) throw new NoqtisError(CODES.EARG, 'at');
|
|
605
|
+
return BigInt(at);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function kstmt(alg, secretKey, publicKey, nalg, nsk, npk, opts) {
|
|
609
|
+
const a = salg(alg);
|
|
610
|
+
const b = nalg === null ? a : salg(nalg);
|
|
611
|
+
const sk = key(secretKey, 'secretKey');
|
|
612
|
+
const pk = key(publicKey, 'publicKey');
|
|
613
|
+
const at = epoch(opts.at);
|
|
614
|
+
if (opts.reason !== undefined && typeof opts.reason !== 'string') {
|
|
615
|
+
throw new TypeError('noqtis: reason must be a string');
|
|
616
|
+
}
|
|
617
|
+
return run((h) => {
|
|
618
|
+
const ns = nsk === null ? null : key(nsk, 'newSecretKey');
|
|
619
|
+
const np = npk === null ? null : key(npk, 'newPublicKey');
|
|
620
|
+
const pns = ns ? h.put(ns) : 0;
|
|
621
|
+
const pnp = np ? h.put(np) : 0;
|
|
622
|
+
const psk = h.put(sk);
|
|
623
|
+
const ppk = h.put(pk);
|
|
624
|
+
const why = opts.reason === undefined ? 0 : h.str(opts.reason);
|
|
625
|
+
return sized(h, (po, cap, pn) => M._nqkstmt(a.id, psk, sk.length, ppk,
|
|
626
|
+
pk.length, b.id, pns, ns ? ns.length : 0, pnp, np ? np.length : 0, at,
|
|
627
|
+
why, po, cap, pn), 4 * (pk.length + (np ? np.length : 0)) + 16384);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
530
631
|
export const capsule = Object.freeze({
|
|
531
632
|
/**
|
|
532
633
|
* Checks a capsule given as its files (a Map, an object or [path, bytes]
|
|
533
634
|
* pairs, with '/' between folders), including noqtis.json and
|
|
534
635
|
* noqtis.sig. Options: `trust` (the public key the seal must carry;
|
|
535
636
|
* without it the seal's own key is accepted and the report says so),
|
|
536
|
-
* `anchors` (DER certificates of TSA trust anchors, at most 32)
|
|
637
|
+
* `anchors` (DER certificates of TSA trust anchors, at most 32),
|
|
638
|
+
* `only` (check just these paths; "partial" rows say what was left out),
|
|
639
|
+
* `lineage` (capsules, as files, among which a format 2 capsule's inputs
|
|
640
|
+
* are found by id and checked, to 64 levels), `keys` (key statements
|
|
641
|
+
* from capsule.rotate / capsule.revoke, at most 32; they need `trust`),
|
|
642
|
+
* `proof` (a proof from capsule.prove: checked instead of the files,
|
|
643
|
+
* which may then be null; given, the proven files are checked too).
|
|
537
644
|
* Returns { ok, code, checks: [{ check, ok, detail }] }.
|
|
538
645
|
*/
|
|
539
646
|
check(files, opts = {}) {
|
|
540
|
-
|
|
647
|
+
const pf = opts.proof === undefined ? null : bytes(opts.proof, 'proof');
|
|
648
|
+
if (pf !== null && (opts.only !== undefined ||
|
|
649
|
+
opts.lineage !== undefined || opts.keys !== undefined)) {
|
|
650
|
+
throw new NoqtisError(CODES.EARG,
|
|
651
|
+
'only, lineage and keys do not apply to a proof');
|
|
652
|
+
}
|
|
653
|
+
if (opts.keys !== undefined && opts.trust === undefined) {
|
|
654
|
+
throw new NoqtisError(CODES.EARG, 'keys need trust');
|
|
655
|
+
}
|
|
656
|
+
const lin = opts.lineage === undefined ? [] : opts.lineage;
|
|
657
|
+
if (!Array.isArray(lin)) {
|
|
658
|
+
throw new TypeError('noqtis: lineage must be an array of capsules');
|
|
659
|
+
}
|
|
660
|
+
const f = capsrc(files);
|
|
661
|
+
if (f === null && pf === null) {
|
|
662
|
+
throw new TypeError('noqtis: files (or opts.proof) are needed');
|
|
663
|
+
}
|
|
664
|
+
const caps = f === null ? lin : [f, ...lin];
|
|
665
|
+
return withdirs(caps, (dirs) => run((h) => {
|
|
541
666
|
const t = opts.trust === undefined ? null : key(opts.trust, 'trust');
|
|
542
|
-
const anc = opts.anchors === undefined ? [] : opts.anchors
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
667
|
+
const anc = blist(h, opts.anchors === undefined ? [] : opts.anchors,
|
|
668
|
+
'anchors', false);
|
|
669
|
+
const only = blist(h, opts.only === undefined ? [] : opts.only,
|
|
670
|
+
'only', true);
|
|
671
|
+
const ld = blist(h, f === null ? dirs : dirs.slice(1), 'lineage', true);
|
|
672
|
+
const st = blist(h, opts.keys === undefined ? [] : opts.keys, 'keys',
|
|
673
|
+
false);
|
|
674
|
+
const o = h.alloc(48); // nqcopt: twelve 32-bit members
|
|
675
|
+
const w = [t ? h.put(t) : 0, t ? t.length : 0, anc.p, anc.l, anc.n,
|
|
676
|
+
only.p, only.n, ld.p, ld.n, st.p, st.l, st.n];
|
|
677
|
+
w.forEach((v, i) => {
|
|
678
|
+
M.HEAPU32[(o >>> 2) + i] = v;
|
|
549
679
|
});
|
|
550
680
|
const rp = h.alloc(4);
|
|
551
681
|
M.HEAPU32[rp >>> 2] = 0;
|
|
552
|
-
const
|
|
553
|
-
|
|
682
|
+
const dir = f === null ? 0 : h.str(dirs[0]);
|
|
683
|
+
const rc = pf === null ? M._nqchkx(dir, o, rp)
|
|
684
|
+
: M._nqchkp(h.put(pf), pf.length, o, dir, rp);
|
|
554
685
|
const checks = report(h, rp);
|
|
555
686
|
if (rc !== CODES.OK && rc !== CODES.EVRFY) throw new NoqtisError(rc);
|
|
556
687
|
return { ok: rc === CODES.OK, code: rc, checks };
|
|
@@ -561,7 +692,10 @@ export const capsule = Object.freeze({
|
|
|
561
692
|
* Seals the files: returns { 'noqtis.json', 'noqtis.sig' } as bytes, to
|
|
562
693
|
* store beside them. Options: `meta` (a JSON value or JSON text, kept in
|
|
563
694
|
* the manifest), `time` (creation time in seconds since 1970; default
|
|
564
|
-
* the clock)
|
|
695
|
+
* the clock), `format` (1, the default, or 2: a Merkle tree over the
|
|
696
|
+
* files, for capsule.prove), `inputs` (format 2, the default with it:
|
|
697
|
+
* [{ name, id }], the capsules this one was made from, each id the
|
|
698
|
+
* 32 bytes capsule.id gives).
|
|
565
699
|
*/
|
|
566
700
|
seal(files, alg, secretKey, publicKey, opts = {}) {
|
|
567
701
|
const a = salg(alg);
|
|
@@ -572,6 +706,16 @@ export const capsule = Object.freeze({
|
|
|
572
706
|
meta = typeof opts.meta === 'string' ? opts.meta
|
|
573
707
|
: JSON.stringify(opts.meta);
|
|
574
708
|
}
|
|
709
|
+
const ins = opts.inputs;
|
|
710
|
+
const fmt = opts.format === undefined ? (ins === undefined ? 1 : 2)
|
|
711
|
+
: opts.format;
|
|
712
|
+
if (fmt !== 1 && fmt !== 2) throw new NoqtisError(CODES.EARG, 'format');
|
|
713
|
+
if (fmt === 1 && ins !== undefined) {
|
|
714
|
+
throw new NoqtisError(CODES.EARG, 'inputs need format 2');
|
|
715
|
+
}
|
|
716
|
+
if (ins !== undefined && !Array.isArray(ins)) {
|
|
717
|
+
throw new TypeError('noqtis: inputs must be an array');
|
|
718
|
+
}
|
|
575
719
|
return withdir(files, (dir) => run((h) => {
|
|
576
720
|
const ev = h.str('SOURCE_DATE_EPOCH');
|
|
577
721
|
if (opts.time !== undefined) {
|
|
@@ -583,8 +727,27 @@ export const capsule = Object.freeze({
|
|
|
583
727
|
M._unsetenv(ev);
|
|
584
728
|
}
|
|
585
729
|
try {
|
|
586
|
-
|
|
587
|
-
|
|
730
|
+
const pd = h.str(dir);
|
|
731
|
+
const psk = h.put(sk);
|
|
732
|
+
const ppk = h.put(pk);
|
|
733
|
+
const pm = meta === null ? 0 : h.str(meta);
|
|
734
|
+
if (fmt === 1) {
|
|
735
|
+
check(M._nqseal(pd, a.id, psk, sk.length, ppk, pk.length, pm, 0));
|
|
736
|
+
} else {
|
|
737
|
+
const list = ins === undefined ? [] : ins;
|
|
738
|
+
const pi = h.alloc(36 * list.length); // nqcin: id[32], name
|
|
739
|
+
list.forEach((x, i) => {
|
|
740
|
+
const id = key(x && x.id, 'inputs[].id');
|
|
741
|
+
if (id.length !== 32 || typeof x.name !== 'string' ||
|
|
742
|
+
x.name.includes('\0')) {
|
|
743
|
+
throw new NoqtisError(CODES.EARG, 'inputs: { name, id }');
|
|
744
|
+
}
|
|
745
|
+
M.HEAPU8.set(id, pi + 36 * i);
|
|
746
|
+
M.HEAPU32[(pi + 36 * i + 32) >>> 2] = h.str(x.name);
|
|
747
|
+
});
|
|
748
|
+
check(M._nqseal2(pd, a.id, psk, sk.length, ppk, pk.length, pm,
|
|
749
|
+
list.length ? pi : 0, list.length));
|
|
750
|
+
}
|
|
588
751
|
} finally {
|
|
589
752
|
M._unsetenv(ev);
|
|
590
753
|
}
|
|
@@ -594,6 +757,215 @@ export const capsule = Object.freeze({
|
|
|
594
757
|
};
|
|
595
758
|
}));
|
|
596
759
|
},
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* The capsule's id, 32 bytes: the SHA-256 of what its seal signs, as
|
|
763
|
+
* seal's `inputs` take it. Only noqtis.sig is read; nothing is checked.
|
|
764
|
+
*/
|
|
765
|
+
id(files) {
|
|
766
|
+
return withdir(files, (dir) => run((h) => {
|
|
767
|
+
const pi = h.alloc(32);
|
|
768
|
+
check(M._nqcapid(h.str(dir), pi));
|
|
769
|
+
return h.get(pi, 32);
|
|
770
|
+
}));
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* A proof (noqtis-proof-1, JSON bytes) that the paths are files of the
|
|
775
|
+
* format 2 capsule: its seal, the manifest's head and each file's entry
|
|
776
|
+
* and Merkle audit path, and nothing of the other files. Check the
|
|
777
|
+
* capsule first; capsule.check(null, { proof }) checks the proof.
|
|
778
|
+
*/
|
|
779
|
+
prove(files, paths) {
|
|
780
|
+
const ps = typeof paths === 'string' ? [paths] : paths;
|
|
781
|
+
return withdir(files, (dir) => run((h) => {
|
|
782
|
+
const l = blist(h, ps, 'paths', true);
|
|
783
|
+
if (l.n === 0) throw new NoqtisError(CODES.EARG, 'paths');
|
|
784
|
+
const pd = h.str(dir);
|
|
785
|
+
return sized(h, (po, cap, pn) => M._nqprove(pd, l.p, l.n, po, cap,
|
|
786
|
+
pn), 65536);
|
|
787
|
+
}));
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* The files the manifest lists, [{ path, size, sha256 }] in manifest
|
|
792
|
+
* order, read with nothing checked (only noqtis.json and noqtis.sig are
|
|
793
|
+
* needed).
|
|
794
|
+
*/
|
|
795
|
+
files(files) {
|
|
796
|
+
return withdir(files, (dir) => run((h) => {
|
|
797
|
+
const rp = h.alloc(4);
|
|
798
|
+
M.HEAPU32[rp >>> 2] = 0;
|
|
799
|
+
const rc = M._nqfiles(h.str(dir), rp);
|
|
800
|
+
const rows = report(h, rp);
|
|
801
|
+
const bad = rows.find((r) => !r.ok);
|
|
802
|
+
if (rc !== CODES.OK || bad) {
|
|
803
|
+
throw new NoqtisError(rc === CODES.OK ? CODES.EDEC : rc,
|
|
804
|
+
bad ? bad.detail : undefined);
|
|
805
|
+
}
|
|
806
|
+
return rows.filter((r) => r.check === 'info.file').map((r) => {
|
|
807
|
+
const a = r.detail.indexOf(' ');
|
|
808
|
+
const b = r.detail.indexOf(' ', a + 1);
|
|
809
|
+
return {
|
|
810
|
+
path: r.detail.slice(b + 1),
|
|
811
|
+
size: Number(r.detail.slice(0, a)),
|
|
812
|
+
sha256: r.detail.slice(a + 1, b),
|
|
813
|
+
};
|
|
814
|
+
});
|
|
815
|
+
}));
|
|
816
|
+
},
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* A key statement (noqtis-keys-1, JSON bytes): the key pair of alg is
|
|
820
|
+
* replaced by the new pair of newAlg from `opts.at` (seconds since
|
|
821
|
+
* 1970 or a Date; default now), signed by both. `opts.reason`: up to
|
|
822
|
+
* 255 bytes of text. Both must be post-quantum (Ed25519 alone is
|
|
823
|
+
* refused). capsule.check takes it in `keys`.
|
|
824
|
+
*/
|
|
825
|
+
rotate(alg, secretKey, publicKey, newAlg, newSecretKey, newPublicKey,
|
|
826
|
+
opts = {}) {
|
|
827
|
+
return kstmt(alg, secretKey, publicKey, newAlg, newSecretKey,
|
|
828
|
+
newPublicKey, opts);
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
/** A key statement that the key pair is revoked from `opts.at`. */
|
|
832
|
+
revoke(alg, secretKey, publicKey, opts = {}) {
|
|
833
|
+
return kstmt(alg, secretKey, publicKey, null, null, null, opts);
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// ---- identity cards ------------------------------------------------------
|
|
838
|
+
|
|
839
|
+
// nqcardf in wasm32 memory: the offsets of include/noqtis.h's struct,
|
|
840
|
+
// which tests/tcard.c asserts when built for wasm32
|
|
841
|
+
const CF = {
|
|
842
|
+
alg: 0, pk: 4, pklen: 2628, fp: 2632, utc: 2672, name: 2693, orcid: 2949,
|
|
843
|
+
email: 2969, url: 3224, affil: 4249, size: 4512,
|
|
844
|
+
};
|
|
845
|
+
const FIELDS = ['name', 'orcid', 'email', 'url', 'affil'];
|
|
846
|
+
|
|
847
|
+
export const idcard = Object.freeze({
|
|
848
|
+
/**
|
|
849
|
+
* An identity card (noqtis-idcard-1, canonical JSON bytes): the public
|
|
850
|
+
* key with `fields.name` and, when given, `orcid` (its check digit
|
|
851
|
+
* verified; its https://orcid.org/ address is stored bare), `email`,
|
|
852
|
+
* `url` (https://) and `affil`, made at `opts.at` (seconds since 1970
|
|
853
|
+
* or a Date; default now) and signed by the key pair under the card's
|
|
854
|
+
* own context. A field out of its limits throws NQEARG; Ed25519 alone
|
|
855
|
+
* NQENOSUP.
|
|
856
|
+
*/
|
|
857
|
+
make(alg, secretKey, publicKey, fields, opts = {}) {
|
|
858
|
+
const a = salg(alg);
|
|
859
|
+
const sk = key(secretKey, 'secretKey');
|
|
860
|
+
const pk = key(publicKey, 'publicKey');
|
|
861
|
+
if (fields === null || typeof fields !== 'object') {
|
|
862
|
+
throw new TypeError('noqtis: fields must be an object with a name');
|
|
863
|
+
}
|
|
864
|
+
for (const f of FIELDS) {
|
|
865
|
+
const v = fields[f];
|
|
866
|
+
if (v === undefined || v === null) {
|
|
867
|
+
if (f === 'name') throw new TypeError('noqtis: fields.name is needed');
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (typeof v !== 'string') {
|
|
871
|
+
throw new TypeError(`noqtis: fields.${f} must be a string`);
|
|
872
|
+
}
|
|
873
|
+
if (v.includes('\0')) throw new NoqtisError(CODES.EARG, f);
|
|
874
|
+
}
|
|
875
|
+
const at = epoch(opts.at);
|
|
876
|
+
return run((h) => {
|
|
877
|
+
const psk = h.put(sk);
|
|
878
|
+
const ppk = h.put(pk);
|
|
879
|
+
const p = FIELDS.map((f) =>
|
|
880
|
+
(fields[f] === undefined || fields[f] === null ? 0 : h.str(fields[f])));
|
|
881
|
+
return sized(h, (po, cap, pn) => M._nqcard(a.id, psk, sk.length, ppk,
|
|
882
|
+
pk.length, p[0], p[1], p[2], p[3], p[4], at, po, cap, pn),
|
|
883
|
+
4 * pk.length + 65536);
|
|
884
|
+
});
|
|
885
|
+
},
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* The card's fields when it verifies (and, with `opts.trust`, is for
|
|
889
|
+
* that public key): { name, orcid, email, url, affil } (null for a
|
|
890
|
+
* field it has not), `created`, `alg`, `publicKey` and `keySha256` (hex,
|
|
891
|
+
* as `noqtis inspect` shows a seal's key); null when its signature does
|
|
892
|
+
* not verify or it is for another key. Input that is not a card throws
|
|
893
|
+
* (NQEDEC). A card is self-signed: compare keySha256 with the one its
|
|
894
|
+
* holder published, or give `trust`.
|
|
895
|
+
*/
|
|
896
|
+
verify(card, opts = {}) {
|
|
897
|
+
const c = bytes(card, 'card');
|
|
898
|
+
const t = opts.trust === undefined || opts.trust === null
|
|
899
|
+
? null : key(opts.trust, 'trust');
|
|
900
|
+
return run((h) => {
|
|
901
|
+
const pc = h.put(c);
|
|
902
|
+
const pt = t ? h.put(t) : 0;
|
|
903
|
+
const f = h.alloc(CF.size);
|
|
904
|
+
const rc = M._nqcardvrfy(pc, c.length, pt, t ? t.length : 0, f);
|
|
905
|
+
if (rc === CODES.EVRFY) return null;
|
|
906
|
+
check(rc);
|
|
907
|
+
const s = (o) => M.UTF8ToString(f + o);
|
|
908
|
+
const opt = (o) => (M.HEAPU8[f + o] === 0 ? null : s(o));
|
|
909
|
+
const fp = h.get(f + CF.fp, 32);
|
|
910
|
+
return {
|
|
911
|
+
name: s(CF.name), orcid: opt(CF.orcid), email: opt(CF.email),
|
|
912
|
+
url: opt(CF.url), affil: opt(CF.affil), created: s(CF.utc),
|
|
913
|
+
alg: M.UTF8ToString(M._nqsname(M.HEAPU32[f >>> 2])),
|
|
914
|
+
publicKey: h.get(f + CF.pk, h.u32(f + CF.pklen)),
|
|
915
|
+
keySha256: Array.from(fp, (b) => b.toString(16).padStart(2, '0'))
|
|
916
|
+
.join(''),
|
|
917
|
+
};
|
|
918
|
+
});
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// ---- DSSE envelopes ------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
export const dsse = Object.freeze({
|
|
925
|
+
/**
|
|
926
|
+
* A DSSE envelope (canonical JSON bytes) of payload with payloadType
|
|
927
|
+
* `type` (1 to 255 printable ASCII bytes, no space, quote or
|
|
928
|
+
* backslash), signed by the key pair over the pre-authentication
|
|
929
|
+
* encoding; keyid is the hex SHA-256 of the public key.
|
|
930
|
+
*/
|
|
931
|
+
sign(alg, secretKey, publicKey, type, payload) {
|
|
932
|
+
const a = salg(alg);
|
|
933
|
+
const sk = key(secretKey, 'secretKey');
|
|
934
|
+
const pk = key(publicKey, 'publicKey');
|
|
935
|
+
if (typeof type !== 'string' || type.includes('\0')) {
|
|
936
|
+
throw new TypeError('noqtis: type must be a string');
|
|
937
|
+
}
|
|
938
|
+
const d = bytes(payload, 'payload');
|
|
939
|
+
return run((h) => {
|
|
940
|
+
const psk = h.put(sk);
|
|
941
|
+
const ppk = h.put(pk);
|
|
942
|
+
const pt = h.str(type);
|
|
943
|
+
const pd = h.put(d);
|
|
944
|
+
return sized(h, (po, cap, pn) => M._nqdsse(a.id, psk, sk.length, ppk,
|
|
945
|
+
pk.length, pt, pd, d.length, po, cap, pn), 2 * d.length + 65536);
|
|
946
|
+
});
|
|
947
|
+
},
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* { payload, payloadType } when one of the envelope's signatures (at
|
|
951
|
+
* most 16) verifies under publicKey; null when none does. Text that is
|
|
952
|
+
* not an envelope throws (NQEDEC).
|
|
953
|
+
*/
|
|
954
|
+
verify(alg, publicKey, envelope) {
|
|
955
|
+
const a = salg(alg);
|
|
956
|
+
const pk = key(publicKey, 'publicKey');
|
|
957
|
+
const e = bytes(envelope, 'envelope');
|
|
958
|
+
return run((h) => {
|
|
959
|
+
const po = h.alloc(e.length);
|
|
960
|
+
const pn = h.alloc(4);
|
|
961
|
+
const pt = h.alloc(256);
|
|
962
|
+
const rc = M._nqdssevrfy(a.id, h.put(pk), pk.length, h.put(e),
|
|
963
|
+
e.length, po, e.length, pn, pt);
|
|
964
|
+
if (rc === CODES.EVRFY) return null;
|
|
965
|
+
check(rc);
|
|
966
|
+
return { payload: h.get(po, h.u32(pn)), payloadType: M.UTF8ToString(pt) };
|
|
967
|
+
});
|
|
968
|
+
},
|
|
597
969
|
});
|
|
598
970
|
|
|
599
971
|
// ---- key files (the CLI's text format) ----------------------------------
|
|
@@ -727,6 +1099,6 @@ export function _heap() {
|
|
|
727
1099
|
}
|
|
728
1100
|
|
|
729
1101
|
export default {
|
|
730
|
-
init, version, algs, random, hash, hmac, sign, kem, capsule,
|
|
731
|
-
NoqtisError, codes, errmsg,
|
|
1102
|
+
init, version, algs, random, hash, hmac, sign, kem, capsule, dsse,
|
|
1103
|
+
idcard, keyfile, NoqtisError, codes, errmsg,
|
|
732
1104
|
};
|
package/lib/node.d.cts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
2
2
|
// The CommonJS helpers return promises (they load the ES module lazily).
|
|
3
|
-
import type { Bytes, CheckResult, SignAlg }
|
|
3
|
+
import type { Bytes, CheckOptions, CheckResult, Files, SealOptions, SignAlg }
|
|
4
|
+
from './index.cjs';
|
|
4
5
|
|
|
5
6
|
export function readCapsule(dir: string): Promise<Map<string, Bytes>>;
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
/** lineage may name folders; dir may be null with opts.proof. */
|
|
8
|
+
export function checkDir(dir: string | null,
|
|
9
|
+
opts?: Omit<CheckOptions, 'lineage'> & { lineage?: (string | Files)[] }):
|
|
10
|
+
Promise<CheckResult>;
|
|
8
11
|
export function sealDir(dir: string, alg: SignAlg, secretKey: Bytes,
|
|
9
|
-
publicKey: Bytes, opts?:
|
|
12
|
+
publicKey: Bytes, opts?: SealOptions):
|
|
10
13
|
Promise<{ 'noqtis.json': Bytes; 'noqtis.sig': Bytes }>;
|
package/lib/node.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
2
|
-
import type { Bytes, CheckResult, SignAlg }
|
|
2
|
+
import type { Bytes, CheckOptions, CheckResult, Files, SealOptions, SignAlg }
|
|
3
|
+
from './index.js';
|
|
3
4
|
|
|
4
5
|
/** Every regular file under dir; a link or special file throws (NQEARG). */
|
|
5
6
|
export function readCapsule(dir: string): Map<string, Bytes>;
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
/** lineage may name folders; dir may be null with opts.proof. */
|
|
8
|
+
export function checkDir(dir: string | null,
|
|
9
|
+
opts?: Omit<CheckOptions, 'lineage'> & { lineage?: (string | Files)[] }):
|
|
10
|
+
CheckResult;
|
|
8
11
|
/** Writes noqtis.json and noqtis.sig into dir and returns them. */
|
|
9
12
|
export function sealDir(dir: string, alg: SignAlg, secretKey: Bytes,
|
|
10
|
-
publicKey: Bytes, opts?:
|
|
13
|
+
publicKey: Bytes, opts?: SealOptions):
|
|
11
14
|
{ 'noqtis.json': Bytes; 'noqtis.sig': Bytes };
|