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/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
- /** The per-signature randomness itself (32 bytes ML-DSA, n SLH-DSA). */
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
- /** seed: xi (32 bytes) for ML-DSA, SK.seed||SK.prf||PK.seed for SLH-DSA. */
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
- check(files: Files, opts?: { trust?: Bytes; anchors?: Bytes[] }):
114
- CheckResult;
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?: { meta?: unknown; time?: number }):
117
- { 'noqtis.json': Bytes; 'noqtis.sig': Bytes };
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
- keyfile: typeof keyfile; NoqtisError: typeof NoqtisError;
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.0". */
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
- const n = a.name.startsWith('ML-DSA') ? 32 : a.pk / 2;
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, runs fn(dir), removes the folder.
477
- function withdir(files, fn) {
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
- return fn(dir);
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
- return withdir(files, (dir) => run((h) => {
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
- const pa = h.alloc(4 * anc.length);
544
- const pl = h.alloc(4 * anc.length);
545
- anc.forEach((c, i) => {
546
- const b = key(c, 'anchor');
547
- M.HEAPU32[(pa >>> 2) + i] = h.put(b);
548
- M.HEAPU32[(pl >>> 2) + i] = b.length;
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 rc = M._nqchkt(h.str(dir), t ? h.put(t) : 0, t ? t.length : 0,
553
- anc.length ? pa : 0, anc.length ? pl : 0, anc.length, rp);
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
- check(M._nqseal(h.str(dir), a.id, h.put(sk), sk.length, h.put(pk),
587
- pk.length, meta === null ? 0 : h.str(meta), 0));
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, keyfile,
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 } from './index.cjs';
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
- export function checkDir(dir: string,
7
- opts?: { trust?: Bytes; anchors?: Bytes[] }): Promise<CheckResult>;
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?: { meta?: unknown; time?: number }):
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 } from './index.js';
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
- export function checkDir(dir: string,
7
- opts?: { trust?: Bytes; anchors?: Bytes[] }): CheckResult;
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?: { meta?: unknown; time?: number }):
13
+ publicKey: Bytes, opts?: SealOptions):
11
14
  { 'noqtis.json': Bytes; 'noqtis.sig': Bytes };