@majikah/majik-signature 0.2.0 → 0.2.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.
|
@@ -42,58 +42,58 @@ export class Mp4Handler {
|
|
|
42
42
|
async embed(bytes, signatureJson) {
|
|
43
43
|
const clean = await this.strip(bytes);
|
|
44
44
|
const majkBox = this._buildBox(MP4_BOX_TYPE, textEncode(signatureJson));
|
|
45
|
-
const boxes = this._parseBoxes(clean, 0, clean.length);
|
|
45
|
+
const { boxes, trailing: topTrailing } = this._parseBoxes(clean, 0, clean.length);
|
|
46
46
|
const moovIdx = boxes.findIndex((b) => b.type === "moov");
|
|
47
47
|
if (moovIdx < 0) {
|
|
48
|
-
// No moov box — use trailer fallback
|
|
49
48
|
const { appendTrailer } = await import("../utils");
|
|
50
49
|
return appendTrailer(clean, signatureJson);
|
|
51
50
|
}
|
|
52
51
|
const moovBox = boxes[moovIdx];
|
|
53
|
-
const moovChildren = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
|
|
52
|
+
const { boxes: moovChildren, trailing: moovTrailing } = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
|
|
54
53
|
const udtaIdx = moovChildren.findIndex((b) => b.type === "udta");
|
|
55
|
-
|
|
54
|
+
const { boxes: existingUdtaChildren, trailing: udtaTrailing } = udtaIdx >= 0
|
|
55
|
+
? this._parseBoxes(moovChildren[udtaIdx].data, 0, moovChildren[udtaIdx].data.length)
|
|
56
|
+
: { boxes: [], trailing: new Uint8Array(0) };
|
|
57
|
+
const newUdta = this._buildBox("udta", concatBytes(...existingUdtaChildren.map((b) => b.raw), majkBox, // ← box-aligned, right after real children
|
|
58
|
+
udtaTrailing));
|
|
59
|
+
const newMoovChildren = [];
|
|
56
60
|
if (udtaIdx >= 0) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
// Retain the exact original index of the udta box to avoid byte shifting
|
|
62
|
+
for (let i = 0; i < moovChildren.length; i++) {
|
|
63
|
+
newMoovChildren.push(i === udtaIdx ? newUdta : moovChildren[i].raw);
|
|
64
|
+
}
|
|
61
65
|
}
|
|
62
66
|
else {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (i === udtaIdx)
|
|
69
|
-
continue; // skip old udta (we'll add new one)
|
|
70
|
-
newMoovChildren.push(moovChildren[i].raw);
|
|
67
|
+
// If it didn't exist previously, it's safe to append it to the end
|
|
68
|
+
for (let i = 0; i < moovChildren.length; i++) {
|
|
69
|
+
newMoovChildren.push(moovChildren[i].raw);
|
|
70
|
+
}
|
|
71
|
+
newMoovChildren.push(newUdta);
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren));
|
|
74
|
-
// Rebuild file
|
|
73
|
+
const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren, moovTrailing));
|
|
75
74
|
const parts = [];
|
|
76
75
|
for (let i = 0; i < boxes.length; i++) {
|
|
77
|
-
|
|
78
|
-
parts.push(newMoov);
|
|
79
|
-
else
|
|
80
|
-
parts.push(boxes[i].raw);
|
|
76
|
+
parts.push(i === moovIdx ? newMoov : boxes[i].raw);
|
|
81
77
|
}
|
|
82
|
-
return concatBytes(...parts);
|
|
78
|
+
return concatBytes(...parts, topTrailing);
|
|
83
79
|
}
|
|
84
80
|
async extract(bytes) {
|
|
85
81
|
if (!this.canHandle(bytes))
|
|
86
82
|
return null;
|
|
87
83
|
try {
|
|
88
|
-
const boxes = this._parseBoxes(bytes, 0, bytes.length);
|
|
84
|
+
const { boxes } = this._parseBoxes(bytes, 0, bytes.length);
|
|
89
85
|
const moov = boxes.find((b) => b.type === "moov");
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
// Fallback: If no moov box exists, check for a Tier-2 trailer
|
|
87
|
+
if (!moov) {
|
|
88
|
+
const { extractTrailer } = await import("../utils");
|
|
89
|
+
const trailer = extractTrailer(bytes);
|
|
90
|
+
return trailer ? trailer.signatureJson : null;
|
|
91
|
+
}
|
|
92
|
+
const { boxes: moovChildren } = this._parseBoxes(moov.data, 0, moov.data.length);
|
|
93
93
|
const udta = moovChildren.find((b) => b.type === "udta");
|
|
94
94
|
if (!udta)
|
|
95
95
|
return null;
|
|
96
|
-
const udtaChildren = this._parseBoxes(udta.data, 0, udta.data.length);
|
|
96
|
+
const { boxes: udtaChildren } = this._parseBoxes(udta.data, 0, udta.data.length);
|
|
97
97
|
const majk = udtaChildren.find((b) => b.type === MP4_BOX_TYPE);
|
|
98
98
|
if (!majk)
|
|
99
99
|
return null;
|
|
@@ -107,32 +107,36 @@ export class Mp4Handler {
|
|
|
107
107
|
if (!this.canHandle(bytes))
|
|
108
108
|
return bytes;
|
|
109
109
|
try {
|
|
110
|
-
const boxes = this._parseBoxes(bytes, 0, bytes.length);
|
|
110
|
+
const { boxes, trailing: topTrailing } = this._parseBoxes(bytes, 0, bytes.length);
|
|
111
111
|
const moovIdx = boxes.findIndex((b) => b.type === "moov");
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// Fallback: If no moov box exists, strip the Tier-2 trailer if present
|
|
113
|
+
if (moovIdx < 0) {
|
|
114
|
+
const { extractTrailer } = await import("../utils");
|
|
115
|
+
const trailer = extractTrailer(bytes);
|
|
116
|
+
return trailer ? trailer.original : bytes;
|
|
117
|
+
}
|
|
114
118
|
const moovBox = boxes[moovIdx];
|
|
115
|
-
const moovChildren = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
|
|
119
|
+
const { boxes: moovChildren, trailing: moovTrailing } = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
|
|
116
120
|
const udtaIdx = moovChildren.findIndex((b) => b.type === "udta");
|
|
117
121
|
if (udtaIdx < 0)
|
|
118
122
|
return bytes;
|
|
119
123
|
const udtaBox = moovChildren[udtaIdx];
|
|
120
|
-
const udtaChildren = this._parseBoxes(udtaBox.data, 0, udtaBox.data.length);
|
|
124
|
+
const { boxes: udtaChildren, trailing: udtaTrailing } = this._parseBoxes(udtaBox.data, 0, udtaBox.data.length);
|
|
121
125
|
const filteredUdta = udtaChildren.filter((b) => b.type !== MP4_BOX_TYPE);
|
|
122
126
|
let newMoovChildren;
|
|
123
|
-
if
|
|
124
|
-
|
|
127
|
+
// Only remove the udta box entirely if it is completely barren of data and trailing bytes
|
|
128
|
+
if (filteredUdta.length === 0 && udtaTrailing.length === 0) {
|
|
125
129
|
newMoovChildren = moovChildren
|
|
126
130
|
.filter((_, i) => i !== udtaIdx)
|
|
127
131
|
.map((b) => b.raw);
|
|
128
132
|
}
|
|
129
133
|
else {
|
|
130
|
-
const newUdta = this._buildBox("udta", concatBytes(...filteredUdta.map((b) => b.raw)));
|
|
134
|
+
const newUdta = this._buildBox("udta", concatBytes(...filteredUdta.map((b) => b.raw), udtaTrailing));
|
|
131
135
|
newMoovChildren = moovChildren.map((b, i) => i === udtaIdx ? newUdta : b.raw);
|
|
132
136
|
}
|
|
133
|
-
const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren));
|
|
137
|
+
const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren, moovTrailing));
|
|
134
138
|
const parts = boxes.map((b, i) => (i === moovIdx ? newMoov : b.raw));
|
|
135
|
-
return concatBytes(...parts);
|
|
139
|
+
return concatBytes(...parts, topTrailing);
|
|
136
140
|
}
|
|
137
141
|
catch {
|
|
138
142
|
return bytes;
|
|
@@ -146,14 +150,11 @@ export class Mp4Handler {
|
|
|
146
150
|
let size = readUint32BE(bytes, offset);
|
|
147
151
|
const type = textDecode(bytes.slice(offset + 4, offset + 8));
|
|
148
152
|
if (size === 1) {
|
|
149
|
-
// 64-bit extended size (large box)
|
|
150
|
-
// Read as two 32-bit values
|
|
151
153
|
const hi = readUint32BE(bytes, offset + 8);
|
|
152
154
|
const lo = readUint32BE(bytes, offset + 12);
|
|
153
155
|
size = hi * 0x100000000 + lo;
|
|
154
156
|
}
|
|
155
157
|
else if (size === 0) {
|
|
156
|
-
// Box extends to end of file
|
|
157
158
|
size = end - offset;
|
|
158
159
|
}
|
|
159
160
|
if (size < 8 || offset + size > end)
|
|
@@ -164,7 +165,10 @@ export class Mp4Handler {
|
|
|
164
165
|
boxes.push({ type, data, raw });
|
|
165
166
|
offset += size;
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
+
// Anything left over (padding, malformed box, unparseable trailing bytes)
|
|
169
|
+
// — preserve it verbatim instead of silently dropping it.
|
|
170
|
+
const trailing = bytes.slice(offset, end);
|
|
171
|
+
return { boxes, trailing };
|
|
168
172
|
}
|
|
169
173
|
_buildBox(type, data) {
|
|
170
174
|
const size = 8 + data.length;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* single-sig files — all code here always operates on MultiSigEnvelope.
|
|
21
21
|
*/
|
|
22
22
|
import type { MajikKey } from "@majikah/majik-key";
|
|
23
|
-
import type { EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, MajikSignatureJSON, MajikSignerPublicKeys, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types";
|
|
23
|
+
import type { EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, MajikSignatureEnvelope, MajikSignatureJSON, MajikSignerPublicKeys, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types";
|
|
24
24
|
import { FormatHandlerRegistry } from "./registry";
|
|
25
25
|
import { MajikChainAnchor } from "../../anchor/types";
|
|
26
26
|
export interface MajikSignatureAdapter {
|
|
@@ -63,6 +63,24 @@ export declare class MajikSignatureEmbed {
|
|
|
63
63
|
expectedSigners?: ExpectedSigner[];
|
|
64
64
|
}, debug?: boolean): Promise<EmbedResult & {
|
|
65
65
|
signature: T;
|
|
66
|
+
envelope: MajikSignatureEnvelope;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Sign a file and return the envelope detached.
|
|
70
|
+
* Extracts any existing envelope (or uses a provided one), validates constraints,
|
|
71
|
+
* signs the stripped bytes, and returns the updated MultiSigEnvelope.
|
|
72
|
+
* Does NOT embed the envelope back into the file.
|
|
73
|
+
*/
|
|
74
|
+
static signDetached(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & {
|
|
75
|
+
contentType?: string;
|
|
76
|
+
timestamp?: string;
|
|
77
|
+
expectedSigners?: ExpectedSigner[];
|
|
78
|
+
existingEnvelope?: MajikSignatureEnvelope;
|
|
79
|
+
}, debug?: boolean): Promise<{
|
|
80
|
+
blob: Blob;
|
|
81
|
+
envelope: MajikSignatureEnvelope;
|
|
82
|
+
handler: string;
|
|
83
|
+
mimeType: string;
|
|
66
84
|
}>;
|
|
67
85
|
/**
|
|
68
86
|
* Extract the MultiSigEnvelope from a file.
|
|
@@ -81,6 +99,17 @@ export declare class MajikSignatureEmbed {
|
|
|
81
99
|
static verifyWithKey(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
82
100
|
expectedSignerId?: string;
|
|
83
101
|
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Verify a file against a provided, detached MultiSigEnvelope.
|
|
104
|
+
* Skips extraction but still strips the file in case it contains an embedded envelope,
|
|
105
|
+
* ensuring verification runs against the clean original bytes.
|
|
106
|
+
*/
|
|
107
|
+
static verifyDetached(file: Blob, envelope: MajikSignatureEnvelope, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
108
|
+
expectedSignerId?: string;
|
|
109
|
+
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
110
|
+
static verifyDetachedWithKey(file: Blob, envelope: MajikSignatureEnvelope, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
111
|
+
expectedSignerId?: string;
|
|
112
|
+
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
84
113
|
/**
|
|
85
114
|
* Seal a multi-sig envelope, preventing any further signatures.
|
|
86
115
|
*
|
|
@@ -166,7 +166,91 @@ export class MajikSignatureEmbed {
|
|
|
166
166
|
// ── Step 8: Embed ──────────────────────────────────────────────────────
|
|
167
167
|
const resultBytes = await handler.embed(originalBytes, JSON.stringify(nextEnvelope));
|
|
168
168
|
const blob = bytesToBlob(resultBytes, mimeType);
|
|
169
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
blob,
|
|
171
|
+
handler: handler.name,
|
|
172
|
+
mimeType,
|
|
173
|
+
signature: signature,
|
|
174
|
+
envelope: nextEnvelope,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// ── signDetached ───────────────────────────────────────────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* Sign a file and return the envelope detached.
|
|
180
|
+
* Extracts any existing envelope (or uses a provided one), validates constraints,
|
|
181
|
+
* signs the stripped bytes, and returns the updated MultiSigEnvelope.
|
|
182
|
+
* Does NOT embed the envelope back into the file.
|
|
183
|
+
*/
|
|
184
|
+
static async signDetached(file, key, MajikSig, options, debug = false) {
|
|
185
|
+
const bytes = await blobToBytes(file);
|
|
186
|
+
const mimeType = options?.mimeType ?? detectMimeType(bytes, file.type);
|
|
187
|
+
const handler = options?.forceFallback
|
|
188
|
+
? new FallbackHandler()
|
|
189
|
+
: DEFAULT_REGISTRY.resolve(bytes, mimeType);
|
|
190
|
+
// ── Step 1: Resolve the working envelope ───────────────────────────────
|
|
191
|
+
let envelope;
|
|
192
|
+
if (options?.existingEnvelope) {
|
|
193
|
+
envelope = options.existingEnvelope;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const existingRaw = await handler.extract(bytes);
|
|
197
|
+
envelope = existingRaw
|
|
198
|
+
? parseEnvelope(existingRaw)
|
|
199
|
+
: { version: 1, signatures: [] };
|
|
200
|
+
}
|
|
201
|
+
// ── Step 2: Reject sealed envelopes ────────────────────────────────────
|
|
202
|
+
if (envelope.sealHash) {
|
|
203
|
+
throw new MajikSignatureError("Cannot sign a sealed envelope. The issuer has locked this file against further signatures.");
|
|
204
|
+
}
|
|
205
|
+
// ── Step 3: Allowlist enforcement ──────────────────────────────────────────
|
|
206
|
+
const isIssuer = envelope.allowlistSignerId === key.fingerprint;
|
|
207
|
+
if (!isIssuer) {
|
|
208
|
+
const allowlistCheck = checkAllowlist(envelope, key);
|
|
209
|
+
if (!allowlistCheck.permitted) {
|
|
210
|
+
throw new MajikSignatureAllowlistError(`Signer "${key.fingerprint}" is not permitted to sign this file. ` +
|
|
211
|
+
`The file has a signing allowlist established by "${envelope.allowlistSignerId}".`, key.fingerprint);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ── Step 4: Get clean original bytes ───────────────────────────────────
|
|
215
|
+
const originalBytes = await handler.strip(bytes);
|
|
216
|
+
if (debug) {
|
|
217
|
+
const recomputedHash = bytesToBase64(hashContent(originalBytes));
|
|
218
|
+
console.log("signDetached — original bytes hash:", recomputedHash);
|
|
219
|
+
}
|
|
220
|
+
// ── Step 5: Compute allowlistHash if establishing or re-signing ─────────
|
|
221
|
+
const isFirstSigner = envelope.signatures.length === 0;
|
|
222
|
+
const establishingAllowlist = isFirstSigner &&
|
|
223
|
+
options?.expectedSigners &&
|
|
224
|
+
options.expectedSigners.length > 0;
|
|
225
|
+
const isIssuerResigning = !isFirstSigner &&
|
|
226
|
+
!!envelope.allowlist &&
|
|
227
|
+
envelope.allowlist.length > 0 &&
|
|
228
|
+
envelope.allowlistSignerId === key.fingerprint;
|
|
229
|
+
const allowlistHashValue = establishingAllowlist
|
|
230
|
+
? hashAllowlist(options.expectedSigners)
|
|
231
|
+
: isIssuerResigning
|
|
232
|
+
? hashAllowlist(envelope.allowlist)
|
|
233
|
+
: undefined;
|
|
234
|
+
// ── Step 6: Sign ───────────────────────────────────────────────────────
|
|
235
|
+
const signature = await MajikSig.sign(originalBytes, key, {
|
|
236
|
+
contentType: options?.contentType,
|
|
237
|
+
timestamp: options?.timestamp,
|
|
238
|
+
...(allowlistHashValue !== undefined
|
|
239
|
+
? { allowlistHash: allowlistHashValue }
|
|
240
|
+
: {}),
|
|
241
|
+
});
|
|
242
|
+
// ── Step 7: Build updated envelope ────────────────────────────────────
|
|
243
|
+
let nextEnvelope = upsertSignature(envelope, signature.toJSON());
|
|
244
|
+
if (establishingAllowlist) {
|
|
245
|
+
nextEnvelope = {
|
|
246
|
+
...nextEnvelope,
|
|
247
|
+
allowlist: options.expectedSigners,
|
|
248
|
+
allowlistSignerId: key.fingerprint,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// ── Step 8: Return DETACHED (No Embedding) ────────────────────────────
|
|
252
|
+
const blob = bytesToBlob(originalBytes, mimeType);
|
|
253
|
+
return { blob, handler: handler.name, mimeType, envelope: nextEnvelope };
|
|
170
254
|
}
|
|
171
255
|
// ── extract ────────────────────────────────────────────────────────────────
|
|
172
256
|
/**
|
|
@@ -273,6 +357,73 @@ export class MajikSignatureEmbed {
|
|
|
273
357
|
const publicKeys = MajikSig.publicKeysFromMajikKey(key);
|
|
274
358
|
return MajikSignatureEmbed.verify(file, publicKeys, MajikSig, options, debug);
|
|
275
359
|
}
|
|
360
|
+
// ── verifyDetached ─────────────────────────────────────────────────────────
|
|
361
|
+
/**
|
|
362
|
+
* Verify a file against a provided, detached MultiSigEnvelope.
|
|
363
|
+
* Skips extraction but still strips the file in case it contains an embedded envelope,
|
|
364
|
+
* ensuring verification runs against the clean original bytes.
|
|
365
|
+
*/
|
|
366
|
+
static async verifyDetached(file, envelope, publicKeys, MajikSig, options, debug = false) {
|
|
367
|
+
const bytes = await blobToBytes(file);
|
|
368
|
+
const mimeType = options?.mimeType ?? detectMimeType(bytes, file.type);
|
|
369
|
+
const handler = DEFAULT_REGISTRY.resolve(bytes, mimeType);
|
|
370
|
+
// Strip the file to ensure we verify against clean bytes
|
|
371
|
+
const originalBytes = await handler.strip(bytes);
|
|
372
|
+
if (debug) {
|
|
373
|
+
const recomputedHash = bytesToBase64(hashContent(originalBytes));
|
|
374
|
+
console.log("verifyDetached — original bytes hash:", recomputedHash);
|
|
375
|
+
}
|
|
376
|
+
// ── Allowlist integrity check ──────────────────────────────────────────
|
|
377
|
+
if (envelope.allowlist && envelope.allowlistSignerId) {
|
|
378
|
+
const recomputedAllowlistHash = hashAllowlist(envelope.allowlist);
|
|
379
|
+
const establisherSig = envelope.signatures.find((s) => s.signerId === envelope.allowlistSignerId);
|
|
380
|
+
if (!establisherSig) {
|
|
381
|
+
return [
|
|
382
|
+
{
|
|
383
|
+
valid: false,
|
|
384
|
+
reason: `Allowlist establisher "${envelope.allowlistSignerId}" has no signature in this envelope`,
|
|
385
|
+
timestamp: new Date().toISOString(),
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
if (establisherSig.allowlistHash !== recomputedAllowlistHash) {
|
|
390
|
+
return [
|
|
391
|
+
{
|
|
392
|
+
valid: false,
|
|
393
|
+
reason: "Allowlist integrity check failed — allowlist may have been tampered with",
|
|
394
|
+
timestamp: new Date().toISOString(),
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// ── Filter by expectedSignerId if provided ─────────────────────────────
|
|
400
|
+
const sigsToVerify = options?.expectedSignerId
|
|
401
|
+
? envelope.signatures.filter((s) => s.signerId === options.expectedSignerId)
|
|
402
|
+
: envelope.signatures;
|
|
403
|
+
if (sigsToVerify.length === 0) {
|
|
404
|
+
return [
|
|
405
|
+
{
|
|
406
|
+
valid: false,
|
|
407
|
+
reason: options?.expectedSignerId
|
|
408
|
+
? `No signature found for signerId "${options.expectedSignerId}"`
|
|
409
|
+
: "Envelope contains no signatures",
|
|
410
|
+
timestamp: new Date().toISOString(),
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
}
|
|
414
|
+
// ── Verify each signature ──────────────────────────────────────────────
|
|
415
|
+
const results = [];
|
|
416
|
+
for (const sig of sigsToVerify) {
|
|
417
|
+
const result = MajikSig.verify(originalBytes, sig, publicKeys);
|
|
418
|
+
results.push({ ...result, handler: handler.name });
|
|
419
|
+
}
|
|
420
|
+
return results;
|
|
421
|
+
}
|
|
422
|
+
// ── verifyDetachedWithKey ──────────────────────────────────────────────────
|
|
423
|
+
static async verifyDetachedWithKey(file, envelope, key, MajikSig, options, debug = false) {
|
|
424
|
+
const publicKeys = MajikSig.publicKeysFromMajikKey(key);
|
|
425
|
+
return MajikSignatureEmbed.verifyDetached(file, envelope, publicKeys, MajikSig, options, debug);
|
|
426
|
+
}
|
|
276
427
|
// ── seal ───────────────────────────────────────────────────────────────────
|
|
277
428
|
/**
|
|
278
429
|
* Seal a multi-sig envelope, preventing any further signatures.
|
package/dist/core/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { MajikChainAnchor } from "../anchor/types";
|
|
6
6
|
import type { ContentType } from "./constants";
|
|
7
7
|
export type { ContentType };
|
|
8
|
+
export type MajikSignatureEnvelope = MultiSigEnvelope;
|
|
8
9
|
/**
|
|
9
10
|
* The serializable per-signer signature envelope.
|
|
10
11
|
* Everything a verifier needs — no private keys required.
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
import type { MajikKey } from "@majikah/majik-key";
|
|
7
|
-
import type { EnvelopeInfo, ExpectedSigner, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
|
|
7
|
+
import type { EnvelopeInfo, ExpectedSigner, MajikSignatureEnvelope, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
|
|
8
|
+
import { MajikSignatureEmbed } from "./core/embed/majik-embed";
|
|
8
9
|
import type { ImageVerificationResult, ImageSignOptions, ImageSignatureStub } from "./core/stamp";
|
|
9
10
|
import { MajikChainAnchor, MajikChainAnchorMemo } from "./anchor/types";
|
|
10
11
|
/**
|
|
@@ -130,12 +131,26 @@ export declare class MajikSignature {
|
|
|
130
131
|
timestamp?: string;
|
|
131
132
|
mimeType?: string;
|
|
132
133
|
expectedSigners?: ExpectedSigner[];
|
|
133
|
-
}):
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
}): ReturnType<typeof MajikSignatureEmbed.signAndEmbed<MajikSignature>>;
|
|
135
|
+
/**
|
|
136
|
+
* Sign a file and return the signature envelope detached.
|
|
137
|
+
*
|
|
138
|
+
* Strips the file of any embedded envelopes, incorporates your new signature
|
|
139
|
+
* into the multi-sig structure, but does NOT embed it back.
|
|
140
|
+
* Useful for external verification workflows where payloads and envelopes travel out-of-band.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* const { blob, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
|
|
144
|
+
* existingEnvelope: outOfBandEnvelope // Optionally pass state from an external source
|
|
145
|
+
* });
|
|
146
|
+
*/
|
|
147
|
+
static signFileDetached(file: Blob, key: MajikKey, options?: {
|
|
148
|
+
contentType?: string;
|
|
149
|
+
timestamp?: string;
|
|
150
|
+
mimeType?: string;
|
|
151
|
+
expectedSigners?: ExpectedSigner[];
|
|
152
|
+
existingEnvelope?: MajikSignatureEnvelope;
|
|
153
|
+
}): ReturnType<typeof MajikSignatureEmbed.signDetached>;
|
|
139
154
|
/**
|
|
140
155
|
* Verify a file's embedded signatures.
|
|
141
156
|
* Returns one VerificationResult per signer. Old single-sig files return a single-item array.
|
|
@@ -145,6 +160,16 @@ export declare class MajikSignature {
|
|
|
145
160
|
expectedSignerId?: string;
|
|
146
161
|
mimeType?: string;
|
|
147
162
|
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
163
|
+
/**
|
|
164
|
+
* Verify a file against a detached signature envelope.
|
|
165
|
+
* Skips extraction and verifies the stripped file bytes directly against the provided envelope.
|
|
166
|
+
* Returns one VerificationResult per signer.
|
|
167
|
+
* Pass options.expectedSignerId to verify only a specific signer.
|
|
168
|
+
*/
|
|
169
|
+
static verifyFileDetached(file: Blob, envelope: MajikSignatureEnvelope, keyOrPublicKeys: MajikKey | MajikSignerPublicKeys, options?: {
|
|
170
|
+
expectedSignerId?: string;
|
|
171
|
+
mimeType?: string;
|
|
172
|
+
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
148
173
|
/**
|
|
149
174
|
* Embed this MajikSignature instance into a file.
|
|
150
175
|
* The signature must cover the original file bytes BEFORE embedding.
|
package/dist/majik-signature.js
CHANGED
|
@@ -402,7 +402,22 @@ export class MajikSignature {
|
|
|
402
402
|
* });
|
|
403
403
|
*/
|
|
404
404
|
static async signFile(file, key, options) {
|
|
405
|
-
return MajikSignatureEmbed.signAndEmbed(file, key, MajikSignature, options);
|
|
405
|
+
return await MajikSignatureEmbed.signAndEmbed(file, key, MajikSignature, options);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Sign a file and return the signature envelope detached.
|
|
409
|
+
*
|
|
410
|
+
* Strips the file of any embedded envelopes, incorporates your new signature
|
|
411
|
+
* into the multi-sig structure, but does NOT embed it back.
|
|
412
|
+
* Useful for external verification workflows where payloads and envelopes travel out-of-band.
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* const { blob, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
|
|
416
|
+
* existingEnvelope: outOfBandEnvelope // Optionally pass state from an external source
|
|
417
|
+
* });
|
|
418
|
+
*/
|
|
419
|
+
static async signFileDetached(file, key, options) {
|
|
420
|
+
return MajikSignatureEmbed.signDetached(file, key, MajikSignature, options);
|
|
406
421
|
}
|
|
407
422
|
/**
|
|
408
423
|
* Verify a file's embedded signatures.
|
|
@@ -415,6 +430,18 @@ export class MajikSignature {
|
|
|
415
430
|
}
|
|
416
431
|
return MajikSignatureEmbed.verify(file, keyOrPublicKeys, MajikSignature, options, debug);
|
|
417
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Verify a file against a detached signature envelope.
|
|
435
|
+
* Skips extraction and verifies the stripped file bytes directly against the provided envelope.
|
|
436
|
+
* Returns one VerificationResult per signer.
|
|
437
|
+
* Pass options.expectedSignerId to verify only a specific signer.
|
|
438
|
+
*/
|
|
439
|
+
static async verifyFileDetached(file, envelope, keyOrPublicKeys, options, debug = false) {
|
|
440
|
+
if (MajikSignature._isMajikKey(keyOrPublicKeys)) {
|
|
441
|
+
return MajikSignatureEmbed.verifyDetachedWithKey(file, envelope, keyOrPublicKeys, MajikSignature, options, debug);
|
|
442
|
+
}
|
|
443
|
+
return MajikSignatureEmbed.verifyDetached(file, envelope, keyOrPublicKeys, MajikSignature, options, debug);
|
|
444
|
+
}
|
|
418
445
|
/**
|
|
419
446
|
* Embed this MajikSignature instance into a file.
|
|
420
447
|
* The signature must cover the original file bytes BEFORE embedding.
|
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.2",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"test:watch": "vitest"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@majikah/majik-key": "^0.3.
|
|
54
|
+
"@majikah/majik-key": "^0.3.3",
|
|
55
55
|
"@noble/post-quantum": "^0.6.1",
|
|
56
56
|
"@stablelib/ed25519": "^2.1.0",
|
|
57
57
|
"@stablelib/sha256": "^2.0.1",
|