@majikah/majik-signature 0.2.7 → 0.2.8
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 +451 -41
- package/dist/core/types.d.ts +20 -0
- package/dist/majik-signature.d.ts +24 -1
- package/dist/majik-signature.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
**Majik Signature** is a hybrid post-quantum content signing and verification library for the Majikah ecosystem. Built on top of [**Majik Key**](https://www.npmjs.com/package/@majikah/majik-key), it produces tamper-evident, forgery-resistant digital signatures for any content — plaintext, JSON, PDFs, audio, video, Office documents, or raw binary — using a dual-algorithm architecture that combines classical **Ed25519** with post-quantum **ML-DSA-87** (FIPS-204).
|
|
7
7
|
|
|
8
|
-
Beyond signing raw bytes, Majik Signature can **embed signatures directly into a file's native format
|
|
8
|
+
Beyond signing raw bytes, Majik Signature can **embed signatures directly into a file's native format**, or produce **detached envelopes** that travel independently of the file — as portable JSON, base64, or a dedicated self-describing binary container (`.mjksig`). It supports **multi-party signing**, **signing allowlists**, **envelope sealing**, **trusted timestamps (TSA)**, **batch/folder signing** via a manifest format (`.mjksmap`), **chronological signing-order verification**, and **blockchain anchor registration**.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -19,19 +19,28 @@ Beyond signing raw bytes, Majik Signature can **embed signatures directly into a
|
|
|
19
19
|
- [Quick Start](#quick-start)
|
|
20
20
|
- [Signing Raw Content](#signing-raw-content)
|
|
21
21
|
- [File Embedding](#file-embedding)
|
|
22
|
+
- [Detached Signing](#detached-signing)
|
|
22
23
|
- [Multi-Signature Files & Allowlists](#multi-signature-files--allowlists)
|
|
23
24
|
- [Sealing an Envelope](#sealing-an-envelope)
|
|
24
25
|
- [Trusted Timestamps (TSA)](#trusted-timestamps-tsa)
|
|
26
|
+
- [Batch Signing a Folder](#batch-signing-a-folder)
|
|
27
|
+
- [Verifying Signing Order](#verifying-signing-order)
|
|
28
|
+
- [Chain Anchoring](#chain-anchoring)
|
|
25
29
|
- [API Reference](#api-reference)
|
|
26
30
|
- [Content Signing](#content-signing-bytesstrings)
|
|
27
31
|
- [File Embedding](#file-embedding-api)
|
|
32
|
+
- [Detached Signing & MajikSignatureEnvelope](#detached-signing--majiksignatureenvelope-api)
|
|
28
33
|
- [Multi-Signature & Allowlist](#multi-signature--allowlist-api)
|
|
29
34
|
- [Sealing](#sealing-api)
|
|
30
35
|
- [Trusted Timestamps](#trusted-timestamps-api)
|
|
36
|
+
- [Batch Signing & MajikSignatureMap](#batch-signing--majiksignaturemap-api)
|
|
37
|
+
- [Signature Order Verification](#signature-order-verification-api)
|
|
38
|
+
- [Chain Anchoring API](#chain-anchoring-api)
|
|
31
39
|
- [Image Stamping (Experimental)](#image-stamping-experimental)
|
|
32
40
|
- [Serialization](#serialization)
|
|
33
41
|
- [Supported File Formats](#supported-file-formats)
|
|
34
42
|
- [Signature & Envelope Structure](#signature--envelope-structure)
|
|
43
|
+
- [Binary Container Formats (MJKSIG / MJKSMAP)](#binary-container-formats-mjksig--mjksmap)
|
|
35
44
|
- [Error Handling](#error-handling)
|
|
36
45
|
- [Security Considerations](#security-considerations)
|
|
37
46
|
- [The Majikah Ecosystem](#the-majikah-ecosystem)
|
|
@@ -48,8 +57,9 @@ Beyond signing raw bytes, Majik Signature can **embed signatures directly into a
|
|
|
48
57
|
Most "digital signature" libraries only sign raw bytes and leave you to figure out storage, transport, and file compatibility yourself. Majik Signature is built to be dropped into a real application:
|
|
49
58
|
|
|
50
59
|
- **Hybrid post-quantum by default** — every signature is Ed25519 + ML-DSA-87, not an opt-in extra.
|
|
51
|
-
- **Signatures live inside the file** —
|
|
52
|
-
- **Multi-party signing is a first-class concept** — not bolted on. Allowlists, sealing,
|
|
60
|
+
- **Signatures live inside the file, or travel detached** — embed directly into the file's native format, or produce a portable envelope (JSON, base64, or the self-describing `.mjksig` binary container) for out-of-band verification workflows.
|
|
61
|
+
- **Multi-party signing is a first-class concept** — not bolted on. Allowlists, sealing, issuer semantics, and chronological order verification are part of the core envelope model.
|
|
62
|
+
- **Batch-aware** — sign or verify an entire folder or zip's contents against a single manifest (`.mjksmap`) instead of tracking one signature file per asset.
|
|
53
63
|
- **Deterministic by design** — the library goes out of its way to guarantee that signing and verifying the same content always produces the same canonical bytes (see [Signature & Envelope Structure](#signature--envelope-structure)).
|
|
54
64
|
- **No native dependencies** — pure TypeScript/WASM-free cryptography, works identically in Node.js, browsers, Tauri, Deno, and Bun.
|
|
55
65
|
|
|
@@ -57,7 +67,6 @@ Most "digital signature" libraries only sign raw bytes and leave you to figure o
|
|
|
57
67
|
|
|
58
68
|
## Security Architecture
|
|
59
69
|
|
|
60
|
-
|
|
61
70
|
```mermaid
|
|
62
71
|
flowchart TD
|
|
63
72
|
A[12/24-word BIP-39 Seed Phrase] --> B[Majik Key]
|
|
@@ -67,28 +76,16 @@ flowchart TD
|
|
|
67
76
|
S --> S1[Ed25519]
|
|
68
77
|
S --> S2[ML-DSA-87]
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
79
|
%% Identity branch
|
|
73
80
|
B --> I[Identity]
|
|
74
81
|
I --> I1[BIP-39]
|
|
75
82
|
I --> I2[X25519]
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
84
|
%% Products (fan-in)
|
|
80
85
|
S1 --> P1[Majik Signature]
|
|
81
86
|
S2 --> P1
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
|
|
92
89
|
### 1. Hybrid Dual-Algorithm Signing
|
|
93
90
|
|
|
94
91
|
Every Majik Signature is produced by **two independent signing algorithms** over the same canonical payload:
|
|
@@ -126,7 +123,7 @@ This binding means a valid signature cannot be reused on different content, tran
|
|
|
126
123
|
|
|
127
124
|
Content is never embedded in the envelope — only its SHA-256 hash is signed. A 500 MB video signs at the same speed as a 10-byte string, and every content type is supported identically.
|
|
128
125
|
|
|
129
|
-
### 4. File Embedding Integrity
|
|
126
|
+
### 4. File Embedding & Detachment Integrity
|
|
130
127
|
|
|
131
128
|
When a signature is embedded into a file, it always covers the **original file bytes before embedding**. Verification strips the embedded envelope before re-hashing, so the round-trip is always:
|
|
132
129
|
|
|
@@ -134,11 +131,13 @@ When a signature is embedded into a file, it always covers the **original file b
|
|
|
134
131
|
sign(originalBytes) → embed into file → extract → strip → verify(originalBytes)
|
|
135
132
|
```
|
|
136
133
|
|
|
134
|
+
The same guarantee applies to **detached** signing: `signFileDetached()` signs the clean, stripped bytes and returns them alongside the envelope — the envelope and the file travel separately, but verification always strips the file first, so an accidentally-still-embedded envelope never double-counts or corrupts the hash.
|
|
135
|
+
|
|
137
136
|
Re-signing (or re-embedding) the same file is always safe — the existing envelope is stripped before the new one is written, so signatures never stack or corrupt each other.
|
|
138
137
|
|
|
139
138
|
### 5. Multi-Party Signing, Allowlists & Sealing
|
|
140
139
|
|
|
141
|
-
Files don't hold a single signature — they hold a **`MultiSigEnvelope
|
|
140
|
+
Files don't hold a single signature — they hold a **`MultiSigEnvelope`** (modeled at runtime by the `MajikSignatureEnvelope` class): an array of per-signer envelopes, plus optional allowlist, seal, and chain-anchor metadata.
|
|
142
141
|
|
|
143
142
|
- **Open signing** (default): anyone with a `MajikKey` can add a signature.
|
|
144
143
|
- **Restricted signing**: the first signer may supply an `expectedSigners` allowlist. That allowlist is cryptographically committed to via `allowlistHash` in the issuer's own canonical payload — tampering with the allowlist after the fact breaks the issuer's signature. Non-listed signers are rejected *before* any cryptographic operation runs.
|
|
@@ -146,7 +145,11 @@ Files don't hold a single signature — they hold a **`MultiSigEnvelope`**: an a
|
|
|
146
145
|
|
|
147
146
|
### 6. Trusted Timestamps (TSA)
|
|
148
147
|
|
|
149
|
-
A `MajikSignature` can optionally carry a `MajikTimestamp` — a signature from a timestamp authority over a canonical payload (`"majikah-tsa-v-1:"` domain) binding a digest, a server-generated nonce, and a server-authoritative timestamp. The TSA signature is itself a full Majik Signature (Ed25519 + ML-DSA-87), so it inherits the same hybrid guarantees.
|
|
148
|
+
A `MajikSignature` can optionally carry a `MajikTimestamp` — a signature from a timestamp authority over a canonical payload (`"majikah-tsa-v-1:"` domain) binding a digest, a server-generated nonce, and a server-authoritative timestamp. The TSA signature is itself a full Majik Signature (Ed25519 + ML-DSA-87), so it inherits the same hybrid guarantees. TSA timestamps are also what power *attested* [signing-order verification](#verifying-signing-order) — see below.
|
|
149
|
+
|
|
150
|
+
### 7. Chain Anchoring
|
|
151
|
+
|
|
152
|
+
A sealed envelope's seal hash can be committed to an external blockchain (handled by a companion product, e.g. `majik-notary`) and the resulting confirmed anchor registered back into the envelope via `MajikChainAnchor` records. The SDK does not talk to any chain itself — it only builds the canonical memo to anchor (`buildChainAnchorMemo()`) and embeds/reads already-confirmed anchors. Anchoring requires the envelope to already be sealed.
|
|
150
153
|
|
|
151
154
|
---
|
|
152
155
|
|
|
@@ -163,10 +166,12 @@ Verification is fully **public** — anyone with the signer's public keys can ve
|
|
|
163
166
|
- **Content Provenance** — prove that music, art, a document, or a dataset was produced by a specific identity.
|
|
164
167
|
- **File Integrity** — detect any tampering or modification to distributed files.
|
|
165
168
|
- **API Payload Signing** — sign JSON requests/responses for non-repudiation.
|
|
166
|
-
- **Document Authentication** — certify contracts, legal records, or invoices, with support for multiple independent signers and a final seal.
|
|
169
|
+
- **Document Authentication** — certify contracts, legal records, or invoices, with support for multiple independent signers, chronological approval order, and a final seal.
|
|
167
170
|
- **Media Certification** — stamp audio, video, or image files as authentic originals, with the signature embedded directly in the file.
|
|
168
171
|
- **Software Distribution** — sign release artifacts to prove they come from the original author.
|
|
169
172
|
- **Identity-Bound Messaging** — bind signed content to a verifiable identity across the Majikah ecosystem.
|
|
173
|
+
- **Batch Asset Signing** — sign every file in a folder, project export, or zip archive against a single manifest, resilient to files being renamed or relocated afterward.
|
|
174
|
+
- **Approval Workflows** — verify not just *who* signed a contract, but that they signed it in the *required order* (e.g. legal before finance, requester before approver).
|
|
170
175
|
|
|
171
176
|
---
|
|
172
177
|
|
|
@@ -176,28 +181,38 @@ Verification is fully **public** — anyone with the signer's public keys can ve
|
|
|
176
181
|
|
|
177
182
|
- **Hybrid signatures** — Ed25519 (classical) + ML-DSA-87 (post-quantum, FIPS-204, Category 5); both must verify
|
|
178
183
|
- **Tamper detection** — SHA-256 content hash is bound inside the signed payload; any byte change invalidates both signatures
|
|
179
|
-
- **Domain separation** — distinct prefixes for signing (`majik-signature-v1:`), sealing (`majik-seal-v1:`),
|
|
184
|
+
- **Domain separation** — distinct prefixes for signing (`majik-signature-v1:`), sealing (`majik-seal-v1:`), timestamping (`majikah-tsa-v-1:`), and chain anchoring (`majik-notary-v-1:`) prevent cross-protocol signature reuse
|
|
180
185
|
- **Signer & timestamp binding** — both are part of the signed payload; neither can be altered or transferred after signing
|
|
181
186
|
- **No private key for verification** — pure public-key verification, safe to run anywhere
|
|
182
187
|
|
|
183
188
|
### Multi-Party Signing
|
|
184
189
|
|
|
185
|
-
- **Multi-signature envelopes** — any number of independent signers on one file
|
|
190
|
+
- **Multi-signature envelopes** — any number of independent signers on one file, modeled by the immutable `MajikSignatureEnvelope` class
|
|
186
191
|
- **Signing allowlists** — restrict who may sign, enforced before any cryptographic operation
|
|
187
192
|
- **Cryptographically committed allowlists** — tampering with the allowlist invalidates the issuer's signature
|
|
188
193
|
- **Sealing** — the issuer can permanently lock an envelope against further signatures
|
|
194
|
+
- **Chronological order verification** — verify that signers signed in a required sequence, with TSA-attested timestamps preferred over self-reported ones, and a strict mode to reject unexpected extra signers
|
|
189
195
|
- **Status queries** — `getSignatories()`, `getIssuer()`, `getEnvelopeInfo()`, `canSign()` for building signing-status UI without manually walking the envelope
|
|
190
196
|
|
|
197
|
+
### Detached Signing & Batch Workflows
|
|
198
|
+
|
|
199
|
+
- **Detached envelopes** — sign a file and receive the envelope separately, for external verification pipelines where payload and signature travel independently
|
|
200
|
+
- **Self-describing binary containers** — `.mjksig` for a single detached envelope, `.mjksmap` for a manifest covering an entire batch, each with magic bytes, a version header, and a length-prefixed payload
|
|
201
|
+
- **Batch signing** — sign every file in a folder or zip in one call, packaged as one `.mjksmap` manifest or as separate `.mjksig` files per asset
|
|
202
|
+
- **Relocation-tolerant batch verification** — a file renamed or moved after signing is still found and verified by content hash, not just by its original path
|
|
203
|
+
- **Per-file batch verification reporting** — `verified` / `invalid` / `tampered` / `not_in_map` status per file, plus a one-glance summary
|
|
204
|
+
|
|
191
205
|
### Content Format Support
|
|
192
206
|
|
|
193
207
|
- **Plain text, JSON, binary** — `Uint8Array` or `string`
|
|
194
208
|
- **PDF** — signature appended as a spec-compliant binary trailer after the file's `%%EOF`
|
|
195
|
-
- **PNG** — embedded in
|
|
209
|
+
- **PNG** — embedded in an `iTXt` metadata chunk
|
|
196
210
|
- **WAV** — embedded in a RIFF `LIST INFO` chunk
|
|
197
211
|
- **MP3** — embedded in an ID3v2 `TXXX` frame
|
|
198
|
-
- **MP4, MOV, M4A, M4V** — embedded in the `moov/udta` box
|
|
212
|
+
- **MP4, MOV, M4A, M4V** — embedded in the `moov/udta` box, custom `majk` box type
|
|
199
213
|
- **DOCX, XLSX, PPTX, ODT, ODS, ODP** — embedded as a dedicated entry inside the ZIP container
|
|
200
214
|
- **MKV, WebM** — embedded via a custom Matroska metadata tag
|
|
215
|
+
- **JPEG, FLAC** — native format metadata, same round-trip guarantees as other handlers
|
|
201
216
|
- **HTML, Markdown, JSON, plain text, source code** — embedded as an appended, format-appropriate metadata block
|
|
202
217
|
- **Any other format** — universal binary trailer: `[original bytes][signature JSON][8-byte length][8-byte magic]` — cleanly detectable and strippable regardless of format
|
|
203
218
|
|
|
@@ -206,19 +221,21 @@ See [Supported File Formats](#supported-file-formats) for the full handler table
|
|
|
206
221
|
### Developer Experience
|
|
207
222
|
|
|
208
223
|
- **First-class TypeScript support** — full type definitions for every interface and class
|
|
209
|
-
- **Simple core API** — `sign()` / `verify()` for bytes and strings; `signFile()` / `verifyFile()` for files
|
|
224
|
+
- **Simple core API** — `sign()` / `verify()` for bytes and strings; `signFile()` / `verifyFile()` for embedded files; `signFileDetached()` / `verifyFileDetached()` for detached workflows
|
|
210
225
|
- **One-liner file signing** — `MajikSignature.signFile(blob, key)` signs and embeds in a single call
|
|
211
226
|
- **Format auto-detection** — MIME type and magic-byte sniffing, no manual format hints required in most cases
|
|
212
227
|
- **Idempotent re-signing** — safely re-sign or re-embed any file without accumulating stacked or orphaned envelopes
|
|
228
|
+
- **Immutable envelope model** — `MajikSignatureEnvelope` and `MajikSignatureMap` are both immutable; every mutation returns a new instance
|
|
213
229
|
- **Typed error hierarchy** — precise, catchable error classes instead of generic exceptions
|
|
214
230
|
- **Isomorphic** — Node.js, browsers, Tauri, Deno, and Bun; no native bindings
|
|
215
231
|
|
|
216
232
|
### Serialization & Portability
|
|
217
233
|
|
|
218
|
-
- **JSON envelope** — full `toJSON()` / `fromJSON()` round-trip
|
|
234
|
+
- **JSON envelope** — full `toJSON()` / `fromJSON()` round-trip, at both the signature and envelope level
|
|
219
235
|
- **Base64 serialization** — `serialize()` / `deserialize()` for compact transport (HTTP headers, DB columns, etc.)
|
|
220
236
|
- **File-embedded** — the signature lives inside the file itself, no sidecar files needed
|
|
221
237
|
- **Self-contained** — the envelope includes the signer's public keys, verifiable without a separate key registry
|
|
238
|
+
- **Binary containers** — `.mjksig` and `.mjksmap` for detached envelopes and batch manifests that need to travel or be stored on disk as their own file
|
|
222
239
|
|
|
223
240
|
---
|
|
224
241
|
|
|
@@ -303,6 +320,42 @@ const originalBlob = await MajikSignature.stripFrom(signedBlob);
|
|
|
303
320
|
|
|
304
321
|
---
|
|
305
322
|
|
|
323
|
+
### Detached Signing
|
|
324
|
+
|
|
325
|
+
Use detached signing when the signature envelope needs to travel independently of the file — for example, storing envelopes in a database while files sit in blob storage, or verification pipelines that never touch the original file storage layer.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
329
|
+
|
|
330
|
+
// Sign — returns the clean file bytes AND the envelope, separately
|
|
331
|
+
const { blob, envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey);
|
|
332
|
+
|
|
333
|
+
// The envelope can be stored/transported however you like:
|
|
334
|
+
const envelopeJson = envelope.toJSON();
|
|
335
|
+
const envelopeB64 = envelope.serialize();
|
|
336
|
+
const envelopeBinary = envelope.toMJKSIG(); // a portable .mjksig Blob
|
|
337
|
+
|
|
338
|
+
// Verify later, against the same clean file bytes + the detached envelope
|
|
339
|
+
const results = await MajikSignature.verifyFileDetached(blob, envelope, aliceKey);
|
|
340
|
+
console.log('Valid:', results.every((r) => r.valid));
|
|
341
|
+
|
|
342
|
+
// Multiple signers can be added to the same detached envelope before it's ever embedded
|
|
343
|
+
const { envelope: envelope2 } = await MajikSignature.signFileDetached(blob, bobKey, {
|
|
344
|
+
existingEnvelope: envelope, // continue from Alice's envelope
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
You can attach a Trusted Timestamp to a detached signature in the same call:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
const { envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
|
|
352
|
+
tsa: myTsaTimestamp,
|
|
353
|
+
});
|
|
354
|
+
console.log(signature.hasTSA); // true
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
306
359
|
### Multi-Signature Files & Allowlists
|
|
307
360
|
|
|
308
361
|
```typescript
|
|
@@ -385,6 +438,111 @@ console.log('TSA still valid:', signature.verifyTSA().valid);
|
|
|
385
438
|
|
|
386
439
|
---
|
|
387
440
|
|
|
441
|
+
### Batch Signing a Folder
|
|
442
|
+
|
|
443
|
+
Sign every file in a folder, project export, or zip's contents in one call, packaged as a single manifest.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
447
|
+
|
|
448
|
+
const result = await MajikSignature.signBatchDetached(
|
|
449
|
+
[
|
|
450
|
+
{ path: 'docs/report.pdf', blob: reportBlob },
|
|
451
|
+
{ path: 'docs/appendix.pdf', blob: appendixBlob },
|
|
452
|
+
{ path: 'assets/cover.png', blob: coverBlob },
|
|
453
|
+
],
|
|
454
|
+
aliceKey,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (result.mode === 'map') {
|
|
458
|
+
// result.map is a MajikSignatureMap instance; result.mapBlob is a ready-to-store .mjksmap Blob
|
|
459
|
+
zip.file('signatures.mjksmap', await result.mapBlob.arrayBuffer());
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Later — verify an extracted batch against the manifest
|
|
463
|
+
const map = await MajikSignature.getSignatureMapFromMJKSMAP(mapBlob);
|
|
464
|
+
// (or: const map = await MajikSignatureMap.fromMJKSMAP(mapBlob);)
|
|
465
|
+
|
|
466
|
+
const results = await MajikSignature.verifyFilesFromMjksMap(
|
|
467
|
+
map,
|
|
468
|
+
extractedFiles, // { path, blob }[]
|
|
469
|
+
publicKeys,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const summary = MajikSignature.summarizeBatchVerification(results);
|
|
473
|
+
console.log(`${summary.verified}/${summary.total} verified`);
|
|
474
|
+
|
|
475
|
+
if (!summary.allValid) {
|
|
476
|
+
for (const r of results) {
|
|
477
|
+
if (r.status !== 'verified') console.log(r.path, r.status, r.reason);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
A batch-verified file that was renamed or moved after signing is still resolved correctly — matched by content hash and reported with `relocatedFrom` set to its original path, rather than showing up as missing.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
### Verifying Signing Order
|
|
487
|
+
|
|
488
|
+
Beyond confirming *who* signed a file, you can confirm they signed in a required *sequence* — e.g. "Bob must sign before Dave." TSA-attested timestamps are preferred automatically when present; self-reported timestamps are used as a fallback and flagged accordingly.
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
492
|
+
|
|
493
|
+
// expectedOrder accepts MajikKey instances and/or ExpectedSigner objects, mixed freely
|
|
494
|
+
const result = await MajikSignature.verifyFileOrder(signedBlob, [bobKey, daveKey]);
|
|
495
|
+
|
|
496
|
+
if (!result.allExpectedSigned) {
|
|
497
|
+
console.log('Still pending:', result.pendingSigners);
|
|
498
|
+
} else if (!result.allValid) {
|
|
499
|
+
console.log('Invalid/tampered signature(s):', result.invalidSigners);
|
|
500
|
+
} else if (!result.orderRespected) {
|
|
501
|
+
console.log('Signed out of order:', result.violations);
|
|
502
|
+
} else if (result.softTieWarnings.length > 0) {
|
|
503
|
+
console.log('Valid, but note:', result.reason); // identical timestamps — order indistinguishable
|
|
504
|
+
} else {
|
|
505
|
+
console.log('Signed in the expected order — all valid.');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (result.usesUnattestedTimestamp) {
|
|
509
|
+
console.log('Note: order relied on at least one self-reported (non-TSA) timestamp.');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Strict mode — fail if anyone outside expectedOrder signed at all
|
|
513
|
+
const strictResult = await MajikSignature.verifyFileOrder(
|
|
514
|
+
signedBlob,
|
|
515
|
+
[bobKey, daveKey],
|
|
516
|
+
{ strict: true },
|
|
517
|
+
);
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The same check works against a detached envelope via `verifyFileDetachedOrder(file, envelope, expectedOrder, options?)`.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
### Chain Anchoring
|
|
525
|
+
|
|
526
|
+
Chain anchoring is a two-step handoff: this SDK prepares and reads anchor data; a separate chain-integration layer (e.g. `majik-notary`) is responsible for actually submitting and confirming the transaction.
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
530
|
+
|
|
531
|
+
// 1. File must be sealed first
|
|
532
|
+
const { permitted, reason } = await MajikSignature.canAnchor(sealedBlob);
|
|
533
|
+
|
|
534
|
+
// 2. Build the canonical memo to submit on-chain (elsewhere, e.g. via majik-notary)
|
|
535
|
+
const memo = MajikSignature.buildChainAnchorMemo(sealInfo.sealHash);
|
|
536
|
+
|
|
537
|
+
// 3. Once the external chain integration confirms the transaction, register the anchor
|
|
538
|
+
const blob = await MajikSignature.registerChainAnchor(sealedBlob, confirmedAnchor);
|
|
539
|
+
|
|
540
|
+
// 4. Read anchors back later
|
|
541
|
+
const anchors = await MajikSignature.getChainAnchors(blob);
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
388
546
|
## API Reference
|
|
389
547
|
|
|
390
548
|
### Content Signing (bytes/strings)
|
|
@@ -398,6 +556,7 @@ Sign raw bytes or a string with an unlocked `MajikKey`.
|
|
|
398
556
|
- `options?: SignOptions`
|
|
399
557
|
- `contentType?: string` — advisory label (see `CONTENT_TYPES`)
|
|
400
558
|
- `timestamp?: string` — ISO 8601 override (defaults to `new Date().toISOString()`)
|
|
559
|
+
- `expectedSigners?: ExpectedSigner[]` — accepted at the type level for allowlist-establishing flows; ignored by the bare `sign()` call itself (only meaningful via the file-level allowlist APIs)
|
|
401
560
|
|
|
402
561
|
**Returns:** `Promise<MajikSignature>`
|
|
403
562
|
**Throws:** `MajikSignatureKeyError` if the key is locked or has no signing keys.
|
|
@@ -417,7 +576,8 @@ Verify a signature against content and the signer's public keys. Both Ed25519 an
|
|
|
417
576
|
contentHash?: string;
|
|
418
577
|
timestamp: string;
|
|
419
578
|
contentType?: string;
|
|
420
|
-
|
|
579
|
+
handler?: string; // present when result came from a file-level verify
|
|
580
|
+
reason?: string; // present when valid is false
|
|
421
581
|
}
|
|
422
582
|
```
|
|
423
583
|
|
|
@@ -429,6 +589,14 @@ Convenience wrapper — verifies directly against a `MajikKey` instance. Works e
|
|
|
429
589
|
#### `MajikSignature.publicKeysFromMajikKey(key)`
|
|
430
590
|
Extracts `{ signerId, edPublicKey, mlDsaPublicKey }` from a `MajikKey` for use with `verify()`. Works on locked keys.
|
|
431
591
|
|
|
592
|
+
#### `signature.extractPublicKeys()` *(instance method)*
|
|
593
|
+
Extracts `{ signerId, edPublicKey, mlDsaPublicKey }` **from the signature envelope itself**, rather than from a `MajikKey`. Useful when you only have a serialized signature and need its embedded public keys — for example, to independently verify a TSA token via `verifyTSA()`. Validates key lengths before returning.
|
|
594
|
+
|
|
595
|
+
> ⚠️ Keys extracted this way are **self-asserted by the envelope** — always cross-check the returned `signerId` against a trusted source before relying on the result. See [Security Considerations](#security-considerations).
|
|
596
|
+
|
|
597
|
+
#### `signature.validate()` / `signature.isValid()`
|
|
598
|
+
`validate()` re-runs structural validation on the signature's own JSON shape and throws on failure; `isValid()` is the non-throwing boolean wrapper. Useful as a cheap sanity check after deserializing from untrusted storage, before running full cryptographic verification.
|
|
599
|
+
|
|
432
600
|
#### `MajikSignature.fromJSON(json)` / `MajikSignature.deserialize(base64)`
|
|
433
601
|
Reconstruct a `MajikSignature` instance from stored JSON or a base64 string.
|
|
434
602
|
|
|
@@ -450,7 +618,7 @@ Sign a file and embed the signature in one call. Strips any existing envelope fi
|
|
|
450
618
|
- `mimeType?: string` — override auto-detected MIME type
|
|
451
619
|
- `expectedSigners?: ExpectedSigner[]` — only honored on the **first** signature; establishes a signing allowlist
|
|
452
620
|
|
|
453
|
-
**Returns:** `Promise<{ blob: Blob; signature: MajikSignature; handler: string; mimeType: string }>`
|
|
621
|
+
**Returns:** `Promise<{ blob: Blob; signature: MajikSignature; envelope: MajikSignatureEnvelope; handler: string; mimeType: string }>`
|
|
454
622
|
|
|
455
623
|
---
|
|
456
624
|
|
|
@@ -487,10 +655,55 @@ Structural presence check — does the file contain an envelope at all? Does not
|
|
|
487
655
|
|
|
488
656
|
---
|
|
489
657
|
|
|
658
|
+
### Detached Signing & MajikSignatureEnvelope API
|
|
659
|
+
|
|
660
|
+
#### `MajikSignature.signFileDetached(file, key, options?)`
|
|
661
|
+
|
|
662
|
+
Sign a file and return the resulting envelope **detached** — the returned `blob` is the clean, stripped file; the envelope travels separately.
|
|
663
|
+
|
|
664
|
+
- `options?.existingEnvelope?: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob` — continue signing an envelope that started out-of-band (any of: an instance, its JSON shape, raw `.mjksig` bytes, or a `.mjksig` Blob)
|
|
665
|
+
- `options?.expectedSigners?: ExpectedSigner[]` — same allowlist-establishing semantics as `signFile()`
|
|
666
|
+
- `options?.tsa?: MajikTimestamp` — attach a Trusted Timestamp to this signer's entry before it's added to the envelope
|
|
667
|
+
|
|
668
|
+
**Returns:** `Promise<{ blob: Blob; envelope: MajikSignatureEnvelope; signature: MajikSignature; handler: string; mimeType: string }>`
|
|
669
|
+
|
|
670
|
+
#### `MajikSignature.verifyFileDetached(file, envelope, keyOrPublicKeys, options?)`
|
|
671
|
+
Verify a file against a detached envelope (instance, JSON, `.mjksig` bytes, or Blob — accepted via the same flexible shape as `existingEnvelope` above). Strips the file first, in case it also happens to carry an embedded envelope.
|
|
672
|
+
|
|
673
|
+
**Returns:** `Promise<VerificationResult[]>`
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
#### `MajikSignatureEnvelope`
|
|
678
|
+
|
|
679
|
+
The behavior-rich, immutable in-memory counterpart to the wire-format `MultiSigEnvelope` JSON. Every `with*` method returns a **new** instance — the receiver is never mutated. This is the class returned by `signFileDetached()`, accepted by `verifyFileDetached()`, and underlying every multi-sig query method.
|
|
680
|
+
|
|
681
|
+
**State predicates:** `isSealed()`, `hasAllowlist()`, `isFirstSigner()`, `isMultiSig()`, `hasMultipleSignatories()`, `isIssuer(keyOrFingerprint)`
|
|
682
|
+
|
|
683
|
+
**Lookup:** `findSignature(signerId)`, `get signatures`, `get allowlist`, `get chainAnchors`
|
|
684
|
+
|
|
685
|
+
**Allowlist enforcement:** `checkAllowlist(key)`, `assertCanSign(key)` (throws), `canSign(key)` (non-throwing), `verifyAllowlistIntegrity()`
|
|
686
|
+
|
|
687
|
+
**Builders (immutable):** `withSignature(sig)`, `withAllowlist(allowlist, signerId)`, `withSeal(sealedBy, timestamp?)`, `withChainAnchor(anchor)`
|
|
688
|
+
|
|
689
|
+
**Seal queries:** `verifySeal()`, `getSealInfo()`, `canAnchor()`
|
|
690
|
+
|
|
691
|
+
**Signatory resolution:** `resolveIssuer()`, `getSignatories(filter?)`, `getEnvelopeInfo()`
|
|
692
|
+
|
|
693
|
+
**Serialization:** `toJSON()`, `serialize()` / `MajikSignatureEnvelope.deserialize(base64)`, `toMJKSIG()` / `toMJKSIGBytes()` / `MajikSignatureEnvelope.fromMJKSIG(input)`, `MajikSignatureEnvelope.isMJKSIG(input)`, `MajikSignatureEnvelope.getMJKSIGVersion(input)`
|
|
694
|
+
|
|
695
|
+
**Creation / parsing:** `MajikSignatureEnvelope.empty()`, `MajikSignatureEnvelope.fromJSON(json)` (also transparently promotes legacy bare single-sig JSON), `MajikSignatureEnvelope.from(input)` (accepts an instance, JSON, `.mjksig` bytes, or Blob — the universal entry point used internally by every detached-envelope-accepting method)
|
|
696
|
+
|
|
697
|
+
**Validation:** `validate()` (throws), `isValid()` (boolean)
|
|
698
|
+
|
|
699
|
+
See [Binary Container Formats](#binary-container-formats-mjksig--mjksmap) for details on `.mjksig`.
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
490
703
|
### Multi-Signature & Allowlist API
|
|
491
704
|
|
|
492
705
|
#### `MajikSignature.expectedSignerFromKey(key)`
|
|
493
|
-
Builds an `ExpectedSigner` entry (`{ signerId, edPublicKey, mlDsaPublicKey }`) from a `MajikKey`, for use in `signFile()`'s `expectedSigners` option. The key does not need to be unlocked.
|
|
706
|
+
Builds an `ExpectedSigner` entry (`{ signerId, edPublicKey, mlDsaPublicKey }`) from a `MajikKey`, for use in `signFile()`'s / `signFileDetached()`'s `expectedSigners` option. The key does not need to be unlocked.
|
|
494
707
|
|
|
495
708
|
#### `MajikSignature.getAllowlist(file, options?)`
|
|
496
709
|
**Returns:** `Promise<ExpectedSigner[] | null>` — `null` for open-signing or unsigned files.
|
|
@@ -557,7 +770,7 @@ Builds a `MajikTSARequest` (`{ digest: { algorithm: "SHA-256", value } }`) from
|
|
|
557
770
|
**Server-side.** Signs a TSA payload and returns a complete `MajikTimestamp`, including a server-generated nonce and timestamp.
|
|
558
771
|
|
|
559
772
|
#### `signature.addTSA(timestamp)`
|
|
560
|
-
Attaches and validates a `MajikTimestamp` on an existing signature instance. Throws `MajikSignatureError` if a TSA is already present
|
|
773
|
+
Attaches and validates a `MajikTimestamp` on an existing signature instance. Throws `MajikSignatureError` if a TSA is already present or if the digest doesn't match, or `MajikSignatureVerificationError` if the TSA signature itself doesn't verify. A TSA, once attached, cannot be replaced.
|
|
561
774
|
|
|
562
775
|
#### `signature.verifyTSA()`
|
|
563
776
|
Re-verifies the attached TSA's own signature — useful after deserializing a signature from storage.
|
|
@@ -567,6 +780,131 @@ Re-verifies the attached TSA's own signature — useful after deserializing a si
|
|
|
567
780
|
|
|
568
781
|
---
|
|
569
782
|
|
|
783
|
+
### Batch Signing & MajikSignatureMap API
|
|
784
|
+
|
|
785
|
+
#### `MajikSignature.signBatchDetached(files, key, options?)`
|
|
786
|
+
|
|
787
|
+
Sign a batch of files (e.g. a folder or zip's contents) as detached envelopes.
|
|
788
|
+
|
|
789
|
+
- `files: { path: string; blob: Blob }[]` — every path must be unique within the batch
|
|
790
|
+
- `options?.mode?: "map" | "separate"` — `"map"` (default) produces one `MajikSignatureMap` covering the whole batch; `"separate"` produces one `.mjksig` Blob per file
|
|
791
|
+
- `options?.continueOnError?: boolean` — default `false` (abort the whole batch on the first failure); set `true` to collect failures per-file and continue
|
|
792
|
+
|
|
793
|
+
**Returns:**
|
|
794
|
+
```typescript
|
|
795
|
+
| { mode: "map"; map: MajikSignatureMap; mapBlob: Blob; failures: BatchSignFailure[] }
|
|
796
|
+
| { mode: "separate"; signatures: { path: string; blob: Blob }[]; failures: BatchSignFailure[] }
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
#### `MajikSignature.verifyFilesFromMjksMap(map, files, publicKeys, options?)`
|
|
800
|
+
Verify a batch of extracted files against a `MajikSignatureMap`. Never throws per-file by default — every outcome (missing, tampered, relocated-but-valid, invalid, verified) is reported so you can render a full status table in one pass.
|
|
801
|
+
|
|
802
|
+
- `options?.expectedSignerId?: string`
|
|
803
|
+
- `options?.requireAllPresent?: boolean` — escalate a missing file to a thrown error instead of a per-file `"not_in_map"` result
|
|
804
|
+
|
|
805
|
+
**Returns:** `Promise<FileVerifyResult[]>`
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
{
|
|
809
|
+
path: string;
|
|
810
|
+
status: "verified" | "invalid" | "tampered" | "not_in_map";
|
|
811
|
+
results?: VerificationResult[];
|
|
812
|
+
reason?: string;
|
|
813
|
+
relocatedFrom?: string; // present only when found by content match at a different path
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
#### `MajikSignature.verifyFilesFromMjksMapWithKey(map, files, key, options?)`
|
|
818
|
+
Convenience overload — resolves public keys from a `MajikKey` instead of requiring `MajikSignerPublicKeys` directly.
|
|
819
|
+
|
|
820
|
+
#### `MajikSignature.summarizeBatchVerification(results)`
|
|
821
|
+
One-glance pass/fail summary over a `FileVerifyResult[]`.
|
|
822
|
+
|
|
823
|
+
**Returns:** `{ total, verified, invalid, tampered, notInMap, allValid }`
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
#### `MajikSignatureMap`
|
|
828
|
+
|
|
829
|
+
The behavior-rich, immutable class backing `.mjksmap`. Keyed by **path**, not content hash alone — duplicate-content files across a batch are legitimate and must not collide.
|
|
830
|
+
|
|
831
|
+
**Lookup:** `getEntry(path)`, `hasEntry(path)`, `findEntry(path, file)` (hash-verified against stored bytes), `findEntriesByHash(file)`, `resolveEntry(path, file)` (relocation-tolerant — tries the given path first, falls back to content match), `getEnvelope(path)`, `getAllEnvelopes()`
|
|
832
|
+
|
|
833
|
+
**Builders (immutable):** `withEntry(entry)`, `withoutEntry(path)`
|
|
834
|
+
|
|
835
|
+
**Serialization:** `toJSON()`, `toMJKSMAP()` / `toMJKSMAPBytes()` / `MajikSignatureMap.fromMJKSMAP(input)`, `MajikSignatureMap.isMJKSMAP(input)`
|
|
836
|
+
|
|
837
|
+
**Creation / parsing:** `MajikSignatureMap.empty()`, `MajikSignatureMap.fromJSON(json)`, `MajikSignatureMap.from(input)` (accepts an instance, JSON, `.mjksmap` bytes, or Blob)
|
|
838
|
+
|
|
839
|
+
**Validation:** `validate()` (throws), `isValid()` (boolean)
|
|
840
|
+
|
|
841
|
+
`resolveEntry()`'s status values: `"path_match"` (found, content unchanged), `"path_tampered"` (found at that path, content no longer matches), `"relocated"` (not at the given path, but found elsewhere by content hash), `"not_found"`.
|
|
842
|
+
|
|
843
|
+
See [Binary Container Formats](#binary-container-formats-mjksig--mjksmap) for details on `.mjksmap`.
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
### Signature Order Verification API
|
|
848
|
+
|
|
849
|
+
Verifies that a set of expected signers signed in a required chronological sequence. TSA-attested timestamps (`tsa.payload.timestamp`) are preferred automatically over self-reported ones (`timestamp`); every result flags whether any comparison relied on a self-reported, non-attested clock.
|
|
850
|
+
|
|
851
|
+
#### `MajikSignature.verifyFileOrder(file, expectedOrder, options?)`
|
|
852
|
+
|
|
853
|
+
- `expectedOrder: (MajikKey | ExpectedSigner)[]` — array position is the expected chronological position; `MajikKey` instances and `ExpectedSigner` objects can be mixed freely and are normalized internally
|
|
854
|
+
- `options?.strict?: boolean` — default `false`. When `true`, any signer present in the envelope but absent from `expectedOrder` fails the overall result
|
|
855
|
+
|
|
856
|
+
**Returns:** `Promise<SignatureOrderResult>`
|
|
857
|
+
|
|
858
|
+
```typescript
|
|
859
|
+
{
|
|
860
|
+
valid: boolean; // true only if everyone expected signed, all valid, order respected (and, in strict mode, no extra signers)
|
|
861
|
+
allExpectedSigned: boolean;
|
|
862
|
+
allValid: boolean;
|
|
863
|
+
orderRespected: boolean;
|
|
864
|
+
strict: boolean;
|
|
865
|
+
unexpectedSigners: string[]; // populated only when strict is true
|
|
866
|
+
pendingSigners: string[];
|
|
867
|
+
invalidSigners: string[];
|
|
868
|
+
violations: OrderViolation[]; // { earlier, later, earlierTimestamp, laterTimestamp }
|
|
869
|
+
usesUnattestedTimestamp: boolean;
|
|
870
|
+
softTieWarnings: SoftTieWarning[]; // identical timestamps between two signers — order indistinguishable, doesn't fail `valid`
|
|
871
|
+
signers: SignerOrderStatus[];
|
|
872
|
+
reason?: string;
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
#### `MajikSignature.verifyFileDetachedOrder(file, envelope, expectedOrder, options?)`
|
|
877
|
+
Same semantics as `verifyFileOrder()`, against a detached envelope (instance, JSON, `.mjksig` bytes, or Blob).
|
|
878
|
+
|
|
879
|
+
#### `MajikSignature.normalizeExpectedOrder(expectedOrder)`
|
|
880
|
+
Normalizes a mixed array of `MajikKey` instances and/or `ExpectedSigner` objects into a plain `ExpectedSigner[]`. Exposed standalone in case you want to build and cache the normalized order ahead of time, without immediately verifying.
|
|
881
|
+
|
|
882
|
+
> Order comparisons only run between signers who **both** signed and **both** produced a valid signature — an invalid signature's timestamp isn't trustworthy, so it's excluded from the ordering check but still reported via `invalidSigners`. Order verification checks the relative sequence of your `expectedOrder` list; in non-strict mode it does not detect an unlisted signer signing *between* two expected signers — use `strict: true` if the presence of any signer outside your expected set must itself be treated as a failure.
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
886
|
+
### Chain Anchoring API (Experimental)
|
|
887
|
+
|
|
888
|
+
> ⚠️ **These APIs are explicitly marked experimental in source and are not yet API-stable.** Expect breaking changes across minor versions.
|
|
889
|
+
|
|
890
|
+
#### `MajikSignature.canAnchor(file, options?)`
|
|
891
|
+
Checks whether a file is eligible for chain anchoring (requires the envelope to be sealed).
|
|
892
|
+
|
|
893
|
+
**Returns:** `Promise<{ permitted: boolean; reason?: string }>`
|
|
894
|
+
|
|
895
|
+
#### `MajikSignature.buildChainAnchorMemo(sealHash)`
|
|
896
|
+
Builds the canonical, domain-separated memo string (`"majik-notary-v-1:" + sealHash`) intended to be submitted on-chain by an external integration layer. Does not talk to any chain itself.
|
|
897
|
+
|
|
898
|
+
#### `MajikSignature.registerChainAnchor(file, anchor, options?)`
|
|
899
|
+
Embeds an **already-confirmed** `MajikChainAnchor` into the file's envelope. The caller is responsible for submitting and confirming the transaction beforehand (e.g. via a separate chain-integration product). Upserts by `anchor.id`, so retried registration doesn't produce duplicates.
|
|
900
|
+
|
|
901
|
+
**Returns:** `Promise<Blob>`
|
|
902
|
+
|
|
903
|
+
#### `MajikSignature.getChainAnchors(file, options?)`
|
|
904
|
+
**Returns:** `Promise<MajikChainAnchor[]>` — empty array if none, or if the file has no envelope.
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
570
908
|
### Image Stamping (Experimental)
|
|
571
909
|
|
|
572
910
|
> ⚠️ **These APIs are explicitly marked experimental in source and are not yet API-stable.** Expect breaking changes across minor versions.
|
|
@@ -583,6 +921,15 @@ Full round-trip to/from the `MajikSignatureJSON` shape.
|
|
|
583
921
|
#### `signature.serialize()` / `MajikSignature.deserialize(base64)`
|
|
584
922
|
Compact base64 transport format — useful for HTTP headers or database columns.
|
|
585
923
|
|
|
924
|
+
#### `envelope.toJSON()` / `MajikSignatureEnvelope.fromJSON(json)`
|
|
925
|
+
Full round-trip to/from the `MultiSigEnvelope` shape, including transparent promotion of legacy bare single-sig JSON.
|
|
926
|
+
|
|
927
|
+
#### `envelope.serialize()` / `MajikSignatureEnvelope.deserialize(base64)`
|
|
928
|
+
Base64 transport for a full multi-sig envelope.
|
|
929
|
+
|
|
930
|
+
#### `envelope.toMJKSIG()` / `MajikSignatureEnvelope.fromMJKSIG(input)`
|
|
931
|
+
Self-describing binary container for a detached envelope — see [Binary Container Formats](#binary-container-formats-mjksig--mjksmap).
|
|
932
|
+
|
|
586
933
|
```typescript
|
|
587
934
|
const signature = await MajikSignature.sign(content, key);
|
|
588
935
|
|
|
@@ -615,7 +962,9 @@ Handlers are tried in order; the first one whose `canHandle()` matches wins. If
|
|
|
615
962
|
| HTML, Markdown, JSON, plain text, source code | Text | Appended, format-appropriate metadata block |
|
|
616
963
|
| Anything else | Fallback | Universal binary trailer: `[original][signature JSON][8-byte length][8-byte magic "MAJIKSIG"]` |
|
|
617
964
|
|
|
618
|
-
All handlers guarantee: files remain fully usable after signing (PDFs still open, videos still play, Office files stay editable), signing is idempotent (safe to re-sign), and `strip()` always reproduces exactly the bytes that were originally hashed — including deterministic ZIP re-canonicalization for Office formats, so that identical content always strips to identical bytes regardless of when it was last re-zipped.
|
|
965
|
+
All handlers guarantee: files remain fully usable after signing (PDFs still open, videos still play, Office files stay editable), signing is idempotent (safe to re-sign), and `strip()` always reproduces exactly the bytes that were originally hashed — including deterministic ZIP re-canonicalization for Office formats (the ZIP is always rebuilt via a full unzip/rezip pass on `strip()`, even when no signature entry exists yet), so that identical content always strips to identical bytes regardless of when it was last re-zipped.
|
|
966
|
+
|
|
967
|
+
The MP4/MOV handler additionally preserves box order and byte-for-byte trailing/leftover data at every nesting level (top-level boxes, `moov` children, and `udta` children) rather than silently discarding anything it doesn't recognize — unparsed trailing bytes are always carried forward rather than dropped.
|
|
619
968
|
|
|
620
969
|
---
|
|
621
970
|
|
|
@@ -641,7 +990,7 @@ A single signer's envelope (`MajikSignatureJSON`):
|
|
|
641
990
|
|
|
642
991
|
`allowlistHash` and `tsa` are only present when applicable — omitted entirely otherwise, never `null`.
|
|
643
992
|
|
|
644
|
-
What's actually embedded into a file is a **`MultiSigEnvelope`**, wrapping one or more of the above
|
|
993
|
+
What's actually embedded into a file (or produced detached) is a **`MultiSigEnvelope`**, wrapping one or more of the above — modeled at runtime by `MajikSignatureEnvelope`:
|
|
645
994
|
|
|
646
995
|
```json
|
|
647
996
|
{
|
|
@@ -651,12 +1000,31 @@ What's actually embedded into a file is a **`MultiSigEnvelope`**, wrapping one o
|
|
|
651
1000
|
"allowlistSignerId": "fingerprint-of-issuer",
|
|
652
1001
|
"sealHash": "128-hex-char-sha3-512-hash",
|
|
653
1002
|
"sealTimestamp": "2026-01-01T00:00:00.000Z",
|
|
654
|
-
"sealedBy": "fingerprint-of-issuer"
|
|
1003
|
+
"sealedBy": "fingerprint-of-issuer",
|
|
1004
|
+
"chainAnchors": [ /* MajikChainAnchor[], optional */ ]
|
|
655
1005
|
}
|
|
656
1006
|
```
|
|
657
1007
|
|
|
658
1008
|
Files signed before multi-sig support existed contain a bare `MajikSignatureJSON` object at the root instead of this wrapper. The library detects and promotes this shape transparently — every public API always returns/accepts `MultiSigEnvelope` semantics, and old signatures continue to verify unmodified.
|
|
659
1009
|
|
|
1010
|
+
A batch manifest (`MjksMapJSON`, backing `MajikSignatureMap`) is a flat list of per-path entries, each carrying its own detached envelope:
|
|
1011
|
+
|
|
1012
|
+
```json
|
|
1013
|
+
{
|
|
1014
|
+
"version": 1,
|
|
1015
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
1016
|
+
"entries": [
|
|
1017
|
+
{
|
|
1018
|
+
"path": "docs/report.pdf",
|
|
1019
|
+
"contentHash": "base64-sha256-of-original-content",
|
|
1020
|
+
"size": 245678,
|
|
1021
|
+
"mimeType": "application/pdf",
|
|
1022
|
+
"envelope": { "...": "MultiSigEnvelope for this file" }
|
|
1023
|
+
}
|
|
1024
|
+
]
|
|
1025
|
+
}
|
|
1026
|
+
```
|
|
1027
|
+
|
|
660
1028
|
**Approximate serialized sizes (per signer):**
|
|
661
1029
|
|
|
662
1030
|
| Format | Size |
|
|
@@ -668,19 +1036,54 @@ The dominant contributor is `mlDsaSignature` (~6 KB base64) and `signerMlDsaPubl
|
|
|
668
1036
|
|
|
669
1037
|
---
|
|
670
1038
|
|
|
1039
|
+
## Binary Container Formats (MJKSIG / MJKSMAP)
|
|
1040
|
+
|
|
1041
|
+
Two dedicated, versioned, self-identifying binary containers exist for out-of-band travel and on-disk storage — distinct from `serialize()`/`deserialize()` (plain base64 of the JSON, no header), which remains for lightweight in-app round-tripping.
|
|
1042
|
+
|
|
1043
|
+
Both share the same header layout: `[magic bytes][1-byte version][1-byte reserved][4-byte big-endian payload length][payload JSON]`.
|
|
1044
|
+
|
|
1045
|
+
| Format | Magic | Magic Length | Header Length | Media Type | Extension |
|
|
1046
|
+
| ------ | ----- | ------------ | -------------- | ---------- | --------- |
|
|
1047
|
+
| `.mjksig` — a single detached envelope | `MJKSIG` | 6 bytes | 12 bytes | `application/vnd.majikah.mjksig` | `.mjksig` |
|
|
1048
|
+
| `.mjksmap` — a batch manifest | `MJKSMAP` | 7 bytes | 13 bytes | `application/vnd.majikah.mjksmap` | `.mjksmap` |
|
|
1049
|
+
|
|
1050
|
+
Both formats validate magic bytes, supported version, and declared payload length **before** attempting to parse the JSON payload — a truncated or corrupted buffer fails fast with a clear `MajikSignatureSerializationError` rather than an obscure `JSON.parse` error.
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
// MJKSIG
|
|
1054
|
+
const bytes = envelope.toMJKSIGBytes(); // sync Uint8Array — Node scripts, direct fs writes
|
|
1055
|
+
const blob = envelope.toMJKSIG(); // Blob — browser downloads, zip packaging
|
|
1056
|
+
const restored = await MajikSignatureEnvelope.fromMJKSIG(blob); // accepts Blob or Uint8Array
|
|
1057
|
+
const isMjksig = await MajikSignatureEnvelope.isMJKSIG(blob); // cheap magic-byte sniff, no parse
|
|
1058
|
+
const version = await MajikSignatureEnvelope.getMJKSIGVersion(blob);
|
|
1059
|
+
|
|
1060
|
+
// MJKSMAP
|
|
1061
|
+
const mapBytes = map.toMJKSMAPBytes();
|
|
1062
|
+
const mapBlob = map.toMJKSMAP();
|
|
1063
|
+
const restoredMap = await MajikSignatureMap.fromMJKSMAP(mapBlob);
|
|
1064
|
+
const isMjksmap = await MajikSignatureMap.isMJKSMAP(mapBlob);
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
`MajikSignatureEnvelope.from(input)` and `MajikSignatureMap.from(input)` are universal entry points that accept an instance, its plain JSON shape, raw bytes, or a Blob — every method that accepts a detached envelope or map (`verifyFileDetached()`, `signFileDetached()`'s `existingEnvelope` option, `verifyFilesFromMjksMap()`, etc.) normalizes through these internally, so callers don't need to know or care which shape they currently have on hand.
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
671
1071
|
## Error Handling
|
|
672
1072
|
|
|
673
1073
|
Majik Signature throws a typed error hierarchy rather than generic `Error` objects, so you can catch precisely what you need:
|
|
674
1074
|
|
|
675
1075
|
| Error Class | Thrown when... |
|
|
676
1076
|
| ------------------------------------- | ------------------------------------------------------------------------- |
|
|
677
|
-
| `MajikSignatureError` | Base class; also thrown for general/unexpected failures
|
|
1077
|
+
| `MajikSignatureError` | Base class; also thrown for general/unexpected failures (e.g. signing a sealed envelope, missing envelope on seal/anchor)|
|
|
678
1078
|
| `MajikSignatureKeyError` | The key is locked, lacks signing keys, or isn't the required issuer |
|
|
679
|
-
| `MajikSignatureVerificationError` | Verification fails unexpectedly (not the same as `valid: false`)
|
|
680
|
-
| `MajikSignatureSerializationError` | JSON/base64 parsing or encoding fails
|
|
1079
|
+
| `MajikSignatureVerificationError` | Verification fails unexpectedly (not the same as `valid: false`), including a failed TSA signature check |
|
|
1080
|
+
| `MajikSignatureSerializationError` | JSON/base64/MJKSIG/MJKSMAP parsing or encoding fails, including malformed binary headers |
|
|
681
1081
|
| `MajikSignatureAllowlistError` | A non-listed signer attempts to sign a restricted file |
|
|
1082
|
+
| `MajikSignatureValidationError` | A structural shape check fails — malformed envelope, malformed batch manifest entry, empty/duplicate batch paths, empty allowlist, mismatched seal fields, invalid `expectedOrder` input, etc. |
|
|
1083
|
+
|
|
1084
|
+
Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }` for a signature that *fails cryptographic verification* — that's an expected, handled outcome, not an exception. Exceptions are reserved for misuse (locked keys, malformed envelopes, disallowed signers, sealed files, malformed batch input, etc.).
|
|
682
1085
|
|
|
683
|
-
|
|
1086
|
+
Order and batch verification methods follow the same philosophy: `SignatureOrderResult` and `FileVerifyResult` both report failure states (`pendingSigners`, `invalidSigners`, `violations`, `"tampered"`, `"not_in_map"`, etc.) as normal return values rather than throwing, since "this didn't pass" is an expected outcome you'll want to render in a UI — not an exceptional code path.
|
|
684
1087
|
|
|
685
1088
|
---
|
|
686
1089
|
|
|
@@ -697,6 +1100,9 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
|
|
|
697
1100
|
- **Allowlist integrity** — tampering with a restricted file's allowlist invalidates the issuer's own signature
|
|
698
1101
|
- **Seal integrity** — sealed envelopes reject all further signing attempts, including from the issuer
|
|
699
1102
|
- **Embed integrity** — file embedding always signs original bytes; the container format is never part of what's signed
|
|
1103
|
+
- **Detachment integrity** — detached verification always strips the target file first, so a stray embedded envelope never interferes with verifying against a separately-supplied envelope
|
|
1104
|
+
- **Batch relocation resistance** — `MajikSignatureMap` resolves files by content hash when a path lookup misses, so renaming/moving a signed batch after the fact doesn't break verification
|
|
1105
|
+
- **Order-check independence** — signing-order verification checks each expected signer against the public keys **you supplied**, not the keys self-asserted inside their own envelope entry, so a forged identity claim can't also forge its way into a valid order result
|
|
700
1106
|
|
|
701
1107
|
### What is Your Responsibility
|
|
702
1108
|
|
|
@@ -704,6 +1110,8 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
|
|
|
704
1110
|
- **Byte-for-byte content consistency** — the same bytes must be passed to both `sign()` and `verify()`. For strings, both sides must use UTF-8; for JSON, both sides must use the same `JSON.stringify()` output.
|
|
705
1111
|
- **Key upgrade** — legacy `MajikKey` accounts without signing keys must be re-imported via `importFromMnemonicBackup()` before signing. Check with `key.hasSigningKeys`.
|
|
706
1112
|
- **TSA trust** — the library verifies a TSA signature cryptographically, but trusting *which* TSA identity to accept is your application's decision.
|
|
1113
|
+
- **Timestamp trust level in order verification** — a self-reported (non-TSA) timestamp is tamper-evident but not independently attested; a signer could set their local clock to anything. Always check `result.usesUnattestedTimestamp` before treating an order result as strong proof rather than a claim.
|
|
1114
|
+
- **Chain anchor submission and confirmation** — this SDK never talks to a blockchain itself. Submitting the memo from `buildChainAnchorMemo()` and confirming the transaction is entirely your (or a companion product's) responsibility before calling `registerChainAnchor()`.
|
|
707
1115
|
|
|
708
1116
|
### What NOT to Do
|
|
709
1117
|
|
|
@@ -714,16 +1122,20 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
|
|
|
714
1122
|
- ❌ **DON'T** use `contentType` as a security mechanism — it is advisory only and not enforced
|
|
715
1123
|
- ❌ **DON'T** assume a Tier-2 trailer signature survives re-muxing or re-encoding — use native-metadata formats where durability matters
|
|
716
1124
|
- ❌ **DON'T** treat the experimental image-stamping APIs as stable in production
|
|
1125
|
+
- ❌ **DON'T** treat a non-strict `verifyFileOrder()` pass as proof that *no one else* signed — use `strict: true` when the complete signer set matters, not just the relative order of the signers you named
|
|
1126
|
+
- ❌ **DON'T** register a chain anchor before the corresponding transaction is actually confirmed externally — `registerChainAnchor()` trusts the `MajikChainAnchor` you pass it and does not itself verify on-chain state
|
|
717
1127
|
|
|
718
1128
|
### What TO Do
|
|
719
1129
|
|
|
720
1130
|
- ✅ **DO** verify `result.signerId` for every entry returned by `verifyFile()` against a known trusted fingerprint
|
|
721
1131
|
- ✅ **DO** use `verifyWithKey()` / `verifyFile(key)` when you have the signer's `MajikKey` — it handles key extraction safely
|
|
722
1132
|
- ✅ **DO** lock the key immediately after signing — `key.lock()` purges secret keys from memory
|
|
723
|
-
- ✅ **DO** use `signFile()` for media and documents to keep signature and content together
|
|
1133
|
+
- ✅ **DO** use `signFile()` for media and documents to keep signature and content together, or `signFileDetached()` when the envelope needs to live and travel separately from the file
|
|
724
1134
|
- ✅ **DO** use `isSigned()` as a fast guard before calling `verifyFile()` in hot paths
|
|
725
1135
|
- ✅ **DO** use `canSign()` to give users a clear reason *before* they attempt to sign a restricted file
|
|
726
1136
|
- ✅ **DO** use `CONTENT_TYPES` constants for standard content type labels
|
|
1137
|
+
- ✅ **DO** use `signBatchDetached()` with `continueOnError: true` for large batches where a handful of unreadable files shouldn't block the rest — and inspect `failures` afterward
|
|
1138
|
+
- ✅ **DO** check `result.softTieWarnings` even on a passing order-verification result — it's a legitimate caveat worth surfacing, not just a failure signal
|
|
727
1139
|
|
|
728
1140
|
---
|
|
729
1141
|
|
|
@@ -735,7 +1147,6 @@ Majik Signature is the cryptographic signing layer shared across Majikah's produ
|
|
|
735
1147
|
|
|
736
1148
|
[](https://signature.majikah.solutions)
|
|
737
1149
|
|
|
738
|
-
|
|
739
1150
|
The standalone desktop and web application built on top of this SDK. It's the fastest way to sign and verify files without writing any code:
|
|
740
1151
|
|
|
741
1152
|
- Sign virtually any file — documents, PDFs, Office files, images, audio, video, source code, archives, and more — entirely **locally on your device**. No account, no upload, no internet connection required for offline signing and verification.
|
|
@@ -743,7 +1154,7 @@ The standalone desktop and web application built on top of this SDK. It's the fa
|
|
|
743
1154
|
- **Audio Stamping** — embed producer tags, voice tags, or audio watermarks directly into signed audio, with a full mixing timeline (volume, pan, pitch, EQ, trim, loop).
|
|
744
1155
|
- **Trusted Timestamps** — every account gets 5 free Trusted Timestamps every 24 hours; local timestamps remain fully supported offline.
|
|
745
1156
|
- **Batch processing** — drag-and-drop folders, recursive ZIP processing, individual or bulk sealing, per-file verification results.
|
|
746
|
-
- **Multi-party workflows** — signing allowlists, open or restricted modes, progress tracking for pending vs. completed signatures.
|
|
1157
|
+
- **Multi-party workflows** — signing allowlists, open or restricted modes, progress tracking for pending vs. completed signatures, and chronological signing-order verification.
|
|
747
1158
|
- Built with Tauri for a lightweight, fast, secure desktop experience — available on the **Microsoft Store**, with a full-featured **web app** as well.
|
|
748
1159
|
|
|
749
1160
|
### 🧾 Majik Buwiz
|
|
@@ -788,7 +1199,6 @@ If you want to contribute or help extend support to more platforms or file forma
|
|
|
788
1199
|
|
|
789
1200
|
Developed by **Josef Elijah Fabian (Zelijah)** | [Majikah Solutions OPC](https://majikah.solutions/about)
|
|
790
1201
|
|
|
791
|
-
|
|
792
1202
|
**Developer**: [Josef Elijah Fabian](https://github.com/jedlsf)
|
|
793
1203
|
**GitHub**: [https://github.com/Majikah](https://github.com/Majikah)
|
|
794
1204
|
**Project Repository**: [https://github.com/Majikah/majik-signature](https://github.com/Majikah/majik-signature)
|
package/dist/core/types.d.ts
CHANGED
|
@@ -53,6 +53,26 @@ export interface MajikTSAPayload {
|
|
|
53
53
|
signerFingerprint: string;
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Wire-optimized signature envelope — omits signerEdPublicKey / signerMlDsaPublicKey.
|
|
58
|
+
*
|
|
59
|
+
* Safe to use ONLY when the verifier resolves the signer's public keys from
|
|
60
|
+
* an external, trusted source (e.g. your MUID/key registry) rather than
|
|
61
|
+
* trusting whatever is embedded in the payload. This is the correct model
|
|
62
|
+
* for anything that already requires a live network round-trip to verify
|
|
63
|
+
* (like MajikSLink), but is NOT a drop-in replacement for offline/portable
|
|
64
|
+
* file signatures where self-containment matters.
|
|
65
|
+
*/
|
|
66
|
+
export interface MajikSignatureCompactJSON {
|
|
67
|
+
v: 1;
|
|
68
|
+
signerId: string;
|
|
69
|
+
contentHash: string;
|
|
70
|
+
contentType?: string;
|
|
71
|
+
timestamp: string;
|
|
72
|
+
edSignature: string;
|
|
73
|
+
mlDsaSignature: string;
|
|
74
|
+
allowlistHash?: string;
|
|
75
|
+
}
|
|
56
76
|
export interface MajikTSARequest {
|
|
57
77
|
digest: {
|
|
58
78
|
algorithm: "SHA-256";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
import type { MajikKey } from "@majikah/majik-key";
|
|
7
|
-
import type { BatchFileInput, BatchSignOptions, BatchVerifyInput, BatchVerifyOptions, EnvelopeInfo, ExpectedSigner, FileVerifyResult, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
|
|
7
|
+
import type { BatchFileInput, BatchSignOptions, BatchVerifyInput, BatchVerifyOptions, EnvelopeInfo, ExpectedSigner, FileVerifyResult, MajikSignatureCompactJSON, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
|
|
8
8
|
import { MajikSignatureEmbed } from "./core/embed/majik-embed";
|
|
9
9
|
import type { ImageVerificationResult, ImageSignOptions, ImageSignatureStub } from "./core/stamp";
|
|
10
10
|
import { MajikChainAnchor, MajikChainAnchorMemo } from "./anchor/types";
|
|
@@ -500,5 +500,28 @@ export declare class MajikSignature {
|
|
|
500
500
|
static getChainAnchors(file: Blob, options?: {
|
|
501
501
|
mimeType?: string;
|
|
502
502
|
}): Promise<MajikChainAnchor[]>;
|
|
503
|
+
/**
|
|
504
|
+
* Strip the embedded public keys, producing the wire/storage-optimized form.
|
|
505
|
+
* The verifier must supply the signer's public keys out-of-band via
|
|
506
|
+
* fromCompact() / verifyCompact() — never trust keys recovered from the
|
|
507
|
+
* compact payload itself, because there are none.
|
|
508
|
+
*/
|
|
509
|
+
toCompact(): MajikSignatureCompactJSON;
|
|
510
|
+
/**
|
|
511
|
+
* Rehydrate a full MajikSignature from a compact payload + externally
|
|
512
|
+
* resolved public keys. Throws if signerId doesn't match the supplied keys —
|
|
513
|
+
* this is a cheap sanity check, not a substitute for verify().
|
|
514
|
+
*/
|
|
515
|
+
static fromCompact(compact: MajikSignatureCompactJSON, publicKeys: Pick<MajikSignerPublicKeys, "edPublicKey" | "mlDsaPublicKey">): MajikSignature;
|
|
516
|
+
/**
|
|
517
|
+
* Verify content against a compact envelope. signerId is checked against
|
|
518
|
+
* publicKeys.signerId before any crypto runs, so a mismatched lookup fails
|
|
519
|
+
* fast with a clear reason instead of a cryptic signature failure.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* const keys = await resolvePublicKeysForMuid(slink.muid); // your registry
|
|
523
|
+
* const result = MajikSignature.verifyCompact(canonical, slink.signatureJSON, keys);
|
|
524
|
+
*/
|
|
525
|
+
static verifyCompact(content: Uint8Array | string, compact: MajikSignatureCompactJSON, publicKeys: MajikSignerPublicKeys): VerificationResult;
|
|
503
526
|
private static _isMajikKey;
|
|
504
527
|
}
|
package/dist/majik-signature.js
CHANGED
|
@@ -810,6 +810,71 @@ export class MajikSignature {
|
|
|
810
810
|
static async getChainAnchors(file, options) {
|
|
811
811
|
return MajikSignatureEmbed.getChainAnchors(file, options);
|
|
812
812
|
}
|
|
813
|
+
// ── COMPACT FORMAT ─────────────────────────────────────────────────────────
|
|
814
|
+
/**
|
|
815
|
+
* Strip the embedded public keys, producing the wire/storage-optimized form.
|
|
816
|
+
* The verifier must supply the signer's public keys out-of-band via
|
|
817
|
+
* fromCompact() / verifyCompact() — never trust keys recovered from the
|
|
818
|
+
* compact payload itself, because there are none.
|
|
819
|
+
*/
|
|
820
|
+
toCompact() {
|
|
821
|
+
return {
|
|
822
|
+
v: this._version,
|
|
823
|
+
signerId: this._signerId,
|
|
824
|
+
contentHash: this._contentHash,
|
|
825
|
+
contentType: this._contentType,
|
|
826
|
+
timestamp: this._timestamp,
|
|
827
|
+
edSignature: this._edSignature,
|
|
828
|
+
mlDsaSignature: this._mlDsaSignature,
|
|
829
|
+
allowlistHash: this._allowlistHash,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Rehydrate a full MajikSignature from a compact payload + externally
|
|
834
|
+
* resolved public keys. Throws if signerId doesn't match the supplied keys —
|
|
835
|
+
* this is a cheap sanity check, not a substitute for verify().
|
|
836
|
+
*/
|
|
837
|
+
static fromCompact(compact, publicKeys) {
|
|
838
|
+
if (!publicKeys?.edPublicKey || !publicKeys?.mlDsaPublicKey) {
|
|
839
|
+
throw new MajikSignatureKeyError("fromCompact() requires the signer's public keys — resolve them by compact.signerId from your key registry / MUID service.");
|
|
840
|
+
}
|
|
841
|
+
const full = {
|
|
842
|
+
version: compact.v,
|
|
843
|
+
signerId: compact.signerId,
|
|
844
|
+
signerEdPublicKey: bytesToBase64(publicKeys.edPublicKey),
|
|
845
|
+
signerMlDsaPublicKey: bytesToBase64(publicKeys.mlDsaPublicKey),
|
|
846
|
+
contentHash: compact.contentHash,
|
|
847
|
+
contentType: compact.contentType,
|
|
848
|
+
timestamp: compact.timestamp,
|
|
849
|
+
edSignature: compact.edSignature,
|
|
850
|
+
mlDsaSignature: compact.mlDsaSignature,
|
|
851
|
+
allowlistHash: compact.allowlistHash,
|
|
852
|
+
};
|
|
853
|
+
return MajikSignature.fromJSON(full);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Verify content against a compact envelope. signerId is checked against
|
|
857
|
+
* publicKeys.signerId before any crypto runs, so a mismatched lookup fails
|
|
858
|
+
* fast with a clear reason instead of a cryptic signature failure.
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* const keys = await resolvePublicKeysForMuid(slink.muid); // your registry
|
|
862
|
+
* const result = MajikSignature.verifyCompact(canonical, slink.signatureJSON, keys);
|
|
863
|
+
*/
|
|
864
|
+
static verifyCompact(content, compact, publicKeys) {
|
|
865
|
+
if (compact.signerId !== publicKeys.signerId) {
|
|
866
|
+
return {
|
|
867
|
+
valid: false,
|
|
868
|
+
signerId: compact.signerId,
|
|
869
|
+
contentHash: compact.contentHash,
|
|
870
|
+
timestamp: compact.timestamp,
|
|
871
|
+
contentType: compact.contentType,
|
|
872
|
+
reason: `signerId mismatch: envelope is "${compact.signerId}", provided publicKeys are for "${publicKeys.signerId}"`,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
const full = MajikSignature.fromCompact(compact, publicKeys);
|
|
876
|
+
return MajikSignature.verify(content, full, publicKeys);
|
|
877
|
+
}
|
|
813
878
|
// ── Private helpers ───────────────────────────────────────────────────────
|
|
814
879
|
static _isMajikKey(v) {
|
|
815
880
|
return typeof v.fingerprint === "string";
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@majikah/majik-signature",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Majik Signature is a hybrid post-quantum content signing and verification library for the Majikah ecosystem. Built on top of Majik Key, it provides tamper-proof, forgery-resistant digital signatures for any content format — using a dual-algorithm architecture that combines classical Ed25519 with post-quantum ML-DSA-87 (FIPS-204).",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.8",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|