@majikah/majik-signature 0.2.8 → 0.2.9
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 +3 -1
- package/dist/core/embed/majik-embed.d.ts +14 -5
- package/dist/core/embed/majik-embed.js +11 -7
- package/dist/core/payload.d.ts +5 -3
- package/dist/core/payload.js +8 -4
- package/dist/core/types.d.ts +54 -24
- package/dist/core/validator.d.ts +2 -0
- package/dist/core/validator.js +7 -0
- package/dist/majik-signature.d.ts +100 -12
- package/dist/majik-signature.js +114 -6
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Majik Signature
|
|
2
2
|
|
|
3
3
|
[](https://thezelijah.world) 
|
|
4
|
-
   [](https://opensource.org/licenses/Apache-2.0) 
|
|
4
|
+
   [](https://opensource.org/licenses/Apache-2.0)  [](https://www.iana.org/assignments/media-types/application/vnd.majikah.mjksig)
|
|
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
|
|
|
@@ -196,6 +196,8 @@ Verification is fully **public** — anyone with the signer's public keys can ve
|
|
|
196
196
|
|
|
197
197
|
### Detached Signing & Batch Workflows
|
|
198
198
|
|
|
199
|
+
[](https://www.iana.org/assignments/media-types/application/vnd.majikah.mjksig)
|
|
200
|
+
|
|
199
201
|
- **Detached envelopes** — sign a file and receive the envelope separately, for external verification pipelines where payload and signature travel independently
|
|
200
202
|
- **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
203
|
- **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
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* reduced to file-format orchestration: read bytes → resolve handler →
|
|
21
21
|
* extract/strip → delegate to the envelope class → re-embed.
|
|
22
22
|
*/
|
|
23
|
-
import type { MajikKey } from "@majikah/majik-key";
|
|
23
|
+
import type { ISODateString, MajikKey } from "@majikah/majik-key";
|
|
24
24
|
import type { BatchFileInput, BatchSignOptions, BatchSignResult, BatchVerifyInput, BatchVerifyOptions, BatchVerifySummary, EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, FileVerifyResult, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types";
|
|
25
25
|
import { MajikSignatureEnvelope } from "../../core/envelope";
|
|
26
26
|
import { FormatHandlerRegistry } from "./registry";
|
|
@@ -40,7 +40,7 @@ export interface MajikSignatureStaticAdapter {
|
|
|
40
40
|
sign(content: Uint8Array | string, key: MajikKey, options?: SignOptions & {
|
|
41
41
|
allowlistHash?: string;
|
|
42
42
|
}): Promise<MajikSignatureAdapter>;
|
|
43
|
-
verify(content: Uint8Array | string, signature: MajikSignatureAdapter | MajikSignatureJSON, publicKeys: MajikSignerPublicKeys): VerificationResult;
|
|
43
|
+
verify(content: Uint8Array | string, signature: MajikSignatureAdapter | MajikSignatureJSON, publicKeys: MajikSignerPublicKeys, now?: Date): VerificationResult;
|
|
44
44
|
publicKeysFromMajikKey(key: MajikKey): MajikSignerPublicKeys;
|
|
45
45
|
fromJSON(json: MajikSignatureJSON | string): MajikSignatureAdapter;
|
|
46
46
|
}
|
|
@@ -69,8 +69,10 @@ export declare class MajikSignatureEmbed {
|
|
|
69
69
|
*/
|
|
70
70
|
static signAndEmbed<T extends MajikSignatureAdapter>(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & {
|
|
71
71
|
contentType?: string;
|
|
72
|
-
timestamp?:
|
|
72
|
+
timestamp?: ISODateString;
|
|
73
73
|
expectedSigners?: ExpectedSigner[];
|
|
74
|
+
/** ISO 8601 expiry for this signature. Omit for one that never expires. */
|
|
75
|
+
validUntil?: ISODateString;
|
|
74
76
|
}, debug?: boolean): Promise<EmbedResult & {
|
|
75
77
|
signature: T;
|
|
76
78
|
envelope: MajikSignatureEnvelope;
|
|
@@ -95,7 +97,9 @@ export declare class MajikSignatureEmbed {
|
|
|
95
97
|
*/
|
|
96
98
|
static signDetached<T extends MajikSignatureAdapter = MajikSignatureAdapter>(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & {
|
|
97
99
|
contentType?: string;
|
|
98
|
-
timestamp?:
|
|
100
|
+
timestamp?: ISODateString;
|
|
101
|
+
/** ISO 8601 expiry for this signature. Omit for one that never expires. */
|
|
102
|
+
validUntil?: ISODateString;
|
|
99
103
|
expectedSigners?: ExpectedSigner[];
|
|
100
104
|
existingEnvelope?: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob;
|
|
101
105
|
tsa?: MajikTimestamp;
|
|
@@ -140,9 +144,11 @@ export declare class MajikSignatureEmbed {
|
|
|
140
144
|
*/
|
|
141
145
|
static verify(file: Blob, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
142
146
|
expectedSignerId?: string;
|
|
147
|
+
now?: Date;
|
|
143
148
|
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
144
149
|
static verifyWithKey(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
145
150
|
expectedSignerId?: string;
|
|
151
|
+
now?: Date;
|
|
146
152
|
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
147
153
|
/**
|
|
148
154
|
* Verify a file against a provided, detached envelope (instance, blob or JSON).
|
|
@@ -151,9 +157,12 @@ export declare class MajikSignatureEmbed {
|
|
|
151
157
|
*/
|
|
152
158
|
static verifyDetached(file: Blob, envelopeInput: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
153
159
|
expectedSignerId?: string;
|
|
154
|
-
|
|
160
|
+
now?: Date;
|
|
161
|
+
}, // FIX
|
|
162
|
+
debug?: boolean): Promise<VerificationResult[]>;
|
|
155
163
|
static verifyDetachedWithKey(file: Blob, envelopeInput: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
156
164
|
expectedSignerId?: string;
|
|
165
|
+
now?: Date;
|
|
157
166
|
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
158
167
|
/**
|
|
159
168
|
* Verify a batch of extracted files against a MajikSignatureMap.
|
|
@@ -37,7 +37,7 @@ import { FallbackHandler } from "./fallback";
|
|
|
37
37
|
import { bytesToBase64, hashContent } from "../hash";
|
|
38
38
|
import { MajikSignatureError, MajikSignatureValidationError } from "../errors";
|
|
39
39
|
import { MajikSignatureMap } from "../mjksmap";
|
|
40
|
-
import { verifySignatureOrder } from "../order";
|
|
40
|
+
import { verifySignatureOrder, } from "../order";
|
|
41
41
|
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
42
42
|
const DEFAULT_REGISTRY = new FormatHandlerRegistry()
|
|
43
43
|
.register(new PdfHandler())
|
|
@@ -104,6 +104,7 @@ export class MajikSignatureEmbed {
|
|
|
104
104
|
const signature = await MajikSig.sign(originalBytes, key, {
|
|
105
105
|
contentType: options?.contentType,
|
|
106
106
|
timestamp: options?.timestamp,
|
|
107
|
+
validUntil: options?.validUntil,
|
|
107
108
|
...(allowlistHashValue !== undefined
|
|
108
109
|
? { allowlistHash: allowlistHashValue }
|
|
109
110
|
: {}),
|
|
@@ -161,6 +162,7 @@ export class MajikSignatureEmbed {
|
|
|
161
162
|
const signature = await MajikSig.sign(originalBytes, key, {
|
|
162
163
|
contentType: options?.contentType,
|
|
163
164
|
timestamp: options?.timestamp,
|
|
165
|
+
validUntil: options?.validUntil,
|
|
164
166
|
...(allowlistHashValue !== undefined
|
|
165
167
|
? { allowlistHash: allowlistHashValue }
|
|
166
168
|
: {}),
|
|
@@ -246,6 +248,7 @@ export class MajikSignatureEmbed {
|
|
|
246
248
|
contentType: options?.contentType,
|
|
247
249
|
timestamp: options?.timestamp,
|
|
248
250
|
expectedSigners: options?.expectedSigners,
|
|
251
|
+
validUntil: options?.validUntil,
|
|
249
252
|
}, debug);
|
|
250
253
|
const sig = envelope.findSignature(key.fingerprint);
|
|
251
254
|
if (!sig) {
|
|
@@ -329,7 +332,7 @@ export class MajikSignatureEmbed {
|
|
|
329
332
|
},
|
|
330
333
|
];
|
|
331
334
|
}
|
|
332
|
-
return MajikSignatureEmbed._verifySignatures(envelope, originalBytes, publicKeys, MajikSig, handler.name, options?.expectedSignerId);
|
|
335
|
+
return MajikSignatureEmbed._verifySignatures(envelope, originalBytes, publicKeys, MajikSig, handler.name, options?.expectedSignerId, options?.now);
|
|
333
336
|
}
|
|
334
337
|
// ── verifyWithKey ──────────────────────────────────────────────────────────
|
|
335
338
|
static async verifyWithKey(file, key, MajikSig, options, debug = false) {
|
|
@@ -342,7 +345,8 @@ export class MajikSignatureEmbed {
|
|
|
342
345
|
* Still strips the file in case it also contains an embedded envelope,
|
|
343
346
|
* ensuring verification runs against the clean original bytes.
|
|
344
347
|
*/
|
|
345
|
-
static async verifyDetached(file, envelopeInput, publicKeys, MajikSig, options,
|
|
348
|
+
static async verifyDetached(file, envelopeInput, publicKeys, MajikSig, options, // FIX
|
|
349
|
+
debug = false) {
|
|
346
350
|
const { bytes, handler } = await MajikSignatureEmbed._prepare(file, options);
|
|
347
351
|
const envelope = await MajikSignatureEnvelope.from(envelopeInput);
|
|
348
352
|
const originalBytes = await handler.strip(bytes);
|
|
@@ -359,7 +363,7 @@ export class MajikSignatureEmbed {
|
|
|
359
363
|
},
|
|
360
364
|
];
|
|
361
365
|
}
|
|
362
|
-
return MajikSignatureEmbed._verifySignatures(envelope, originalBytes, publicKeys, MajikSig, handler.name, options?.expectedSignerId);
|
|
366
|
+
return MajikSignatureEmbed._verifySignatures(envelope, originalBytes, publicKeys, MajikSig, handler.name, options?.expectedSignerId, options?.now);
|
|
363
367
|
}
|
|
364
368
|
// ── verifyDetachedWithKey ──────────────────────────────────────────────────
|
|
365
369
|
static async verifyDetachedWithKey(file, envelopeInput, key, MajikSig, options, debug = false) {
|
|
@@ -425,7 +429,7 @@ export class MajikSignatureEmbed {
|
|
|
425
429
|
});
|
|
426
430
|
continue;
|
|
427
431
|
}
|
|
428
|
-
const verifyResults = MajikSignatureEmbed._verifySignatures(envelope, originalBytes, publicKeys, MajikSig, "mjksmap", options?.expectedSignerId);
|
|
432
|
+
const verifyResults = MajikSignatureEmbed._verifySignatures(envelope, originalBytes, publicKeys, MajikSig, "mjksmap", options?.expectedSignerId, options?.now);
|
|
429
433
|
const allValid = verifyResults.every((r) => r.valid);
|
|
430
434
|
results.push({
|
|
431
435
|
path: file.path,
|
|
@@ -667,7 +671,7 @@ export class MajikSignatureEmbed {
|
|
|
667
671
|
* verify each remaining signature, and stamp the handler name onto each
|
|
668
672
|
* result. Previously duplicated near-verbatim in both methods.
|
|
669
673
|
*/
|
|
670
|
-
static _verifySignatures(envelope, originalBytes, publicKeys, MajikSig, handlerName, expectedSignerId) {
|
|
674
|
+
static _verifySignatures(envelope, originalBytes, publicKeys, MajikSig, handlerName, expectedSignerId, now) {
|
|
671
675
|
const sigsToVerify = expectedSignerId
|
|
672
676
|
? envelope.signatures.filter((s) => s.signerId === expectedSignerId)
|
|
673
677
|
: envelope.signatures;
|
|
@@ -683,7 +687,7 @@ export class MajikSignatureEmbed {
|
|
|
683
687
|
];
|
|
684
688
|
}
|
|
685
689
|
return sigsToVerify.map((sig) => ({
|
|
686
|
-
...MajikSig.verify(originalBytes, sig, publicKeys),
|
|
690
|
+
...MajikSig.verify(originalBytes, sig, publicKeys, now),
|
|
687
691
|
handler: handlerName,
|
|
688
692
|
}));
|
|
689
693
|
}
|
package/dist/core/payload.d.ts
CHANGED
|
@@ -41,14 +41,16 @@ export interface PayloadFields {
|
|
|
41
41
|
* Must be omitted entirely (not null) for all other signatures.
|
|
42
42
|
*/
|
|
43
43
|
allowlistHash?: string;
|
|
44
|
+
validUntil?: string;
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* Build the canonical byte payload that both algorithms sign and verify.
|
|
47
48
|
* Deterministic: identical inputs always produce identical bytes.
|
|
48
49
|
*
|
|
49
|
-
* `alh`
|
|
50
|
-
* This is the load-bearing backward-compat guarantee:
|
|
51
|
-
*
|
|
50
|
+
* `alh` and `vu` are conditionally spread — present only when allowlistHash /
|
|
51
|
+
* validUntil are provided. This is the load-bearing backward-compat guarantee:
|
|
52
|
+
* old signatures never had these keys in their payload, so we must not add
|
|
53
|
+
* them (even as null) when verifying old signatures.
|
|
52
54
|
*/
|
|
53
55
|
export declare function buildSigningPayload(fields: PayloadFields): Uint8Array;
|
|
54
56
|
export declare function buildTSACanonicalBytes(payload: MajikTSAPayload): Uint8Array;
|
package/dist/core/payload.js
CHANGED
|
@@ -29,14 +29,15 @@
|
|
|
29
29
|
* payload bytes identical to what pre-multi-sig signers produced, so all
|
|
30
30
|
* existing signatures continue to verify correctly.
|
|
31
31
|
*/
|
|
32
|
-
import { MAJIK_SIGNATURE_DOMAIN, MAJIK_SIGNATURE_VERSION, MAJIK_TSA_DOMAIN } from "./constants";
|
|
32
|
+
import { MAJIK_SIGNATURE_DOMAIN, MAJIK_SIGNATURE_VERSION, MAJIK_TSA_DOMAIN, } from "./constants";
|
|
33
33
|
/**
|
|
34
34
|
* Build the canonical byte payload that both algorithms sign and verify.
|
|
35
35
|
* Deterministic: identical inputs always produce identical bytes.
|
|
36
36
|
*
|
|
37
|
-
* `alh`
|
|
38
|
-
* This is the load-bearing backward-compat guarantee:
|
|
39
|
-
*
|
|
37
|
+
* `alh` and `vu` are conditionally spread — present only when allowlistHash /
|
|
38
|
+
* validUntil are provided. This is the load-bearing backward-compat guarantee:
|
|
39
|
+
* old signatures never had these keys in their payload, so we must not add
|
|
40
|
+
* them (even as null) when verifying old signatures.
|
|
40
41
|
*/
|
|
41
42
|
export function buildSigningPayload(fields) {
|
|
42
43
|
const meta = JSON.stringify({
|
|
@@ -50,6 +51,9 @@ export function buildSigningPayload(fields) {
|
|
|
50
51
|
...(fields.allowlistHash !== undefined
|
|
51
52
|
? { alh: fields.allowlistHash }
|
|
52
53
|
: {}),
|
|
54
|
+
// Conditionally include vu — same reasoning: omit entirely so all
|
|
55
|
+
// pre-expiry signatures reproduce byte-identical payloads (backward compat).
|
|
56
|
+
...(fields.validUntil !== undefined ? { vu: fields.validUntil } : {}),
|
|
53
57
|
});
|
|
54
58
|
const prefix = new TextEncoder().encode(MAJIK_SIGNATURE_DOMAIN);
|
|
55
59
|
const body = new TextEncoder().encode(meta);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* types.ts
|
|
3
3
|
* Public types for the MajikSignature library.
|
|
4
4
|
*/
|
|
5
|
-
import { ISODateString } from "@majikah/majik-key";
|
|
6
|
-
import { MajikChainAnchor } from "../anchor/types";
|
|
5
|
+
import type { ISODateString, MajikKeyFingerprint } from "@majikah/majik-key";
|
|
6
|
+
import type { MajikChainAnchor } from "../anchor/types";
|
|
7
7
|
import type { ContentType } from "./constants";
|
|
8
8
|
import type { MajikSignatureEnvelope } from "./envelope";
|
|
9
9
|
export type { ContentType };
|
|
10
10
|
export type MajikSignatureEnvelopeJSON = MultiSigEnvelope;
|
|
11
|
+
export type ED25519Signature = string;
|
|
12
|
+
export type MLDSA87Signature = string;
|
|
11
13
|
/**
|
|
12
14
|
* The serializable per-signer signature envelope.
|
|
13
15
|
* Everything a verifier needs — no private keys required.
|
|
@@ -16,7 +18,7 @@ export interface MajikSignatureJSON {
|
|
|
16
18
|
/** Envelope version — must equal MAJIK_SIGNATURE_VERSION */
|
|
17
19
|
version: 1;
|
|
18
20
|
/** MajikKey fingerprint (SHA-256 of X25519 public key, base64) */
|
|
19
|
-
signerId:
|
|
21
|
+
signerId: MajikKeyFingerprint;
|
|
20
22
|
/** Ed25519 public key, base64 (32 bytes) */
|
|
21
23
|
signerEdPublicKey: string;
|
|
22
24
|
/** ML-DSA-87 public key, base64 (2592 bytes) */
|
|
@@ -26,11 +28,11 @@ export interface MajikSignatureJSON {
|
|
|
26
28
|
/** Advisory content type — e.g. "audio/wav", "application/pdf" */
|
|
27
29
|
contentType?: string;
|
|
28
30
|
/** ISO 8601 timestamp of when the signature was created */
|
|
29
|
-
timestamp:
|
|
31
|
+
timestamp: ISODateString;
|
|
30
32
|
/** Ed25519 signature over the canonical payload, base64 (64 bytes) */
|
|
31
|
-
edSignature:
|
|
33
|
+
edSignature: ED25519Signature;
|
|
32
34
|
/** ML-DSA-87 signature over the canonical payload, base64 (4595 bytes) */
|
|
33
|
-
mlDsaSignature:
|
|
35
|
+
mlDsaSignature: MLDSA87Signature;
|
|
34
36
|
/**
|
|
35
37
|
* SHA-256 hash of the canonical allowlist JSON, base64 (44 chars).
|
|
36
38
|
* Present only on the envelope of the signer who established the allowlist.
|
|
@@ -39,6 +41,14 @@ export interface MajikSignatureJSON {
|
|
|
39
41
|
* Absent on all other signers and on any signature made before multi-sig support.
|
|
40
42
|
*/
|
|
41
43
|
allowlistHash?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional ISO 8601 expiry. When present, verify() treats the signature
|
|
46
|
+
* as invalid once the current time is past this value. Absent = never
|
|
47
|
+
* expires (matches all pre-existing signatures — fully backward compatible).
|
|
48
|
+
* Covered by the canonical signing payload when present, so it cannot be
|
|
49
|
+
* stripped or extended post-hoc without breaking both signatures.
|
|
50
|
+
*/
|
|
51
|
+
validUntil?: ISODateString;
|
|
42
52
|
tsa?: MajikTimestamp;
|
|
43
53
|
}
|
|
44
54
|
export interface MajikTSAPayload {
|
|
@@ -65,13 +75,14 @@ export interface MajikTSAPayload {
|
|
|
65
75
|
*/
|
|
66
76
|
export interface MajikSignatureCompactJSON {
|
|
67
77
|
v: 1;
|
|
68
|
-
signerId:
|
|
78
|
+
signerId: MajikKeyFingerprint;
|
|
69
79
|
contentHash: string;
|
|
70
80
|
contentType?: string;
|
|
71
|
-
timestamp:
|
|
72
|
-
edSignature:
|
|
73
|
-
mlDsaSignature:
|
|
81
|
+
timestamp: ISODateString;
|
|
82
|
+
edSignature: ED25519Signature;
|
|
83
|
+
mlDsaSignature: MLDSA87Signature;
|
|
74
84
|
allowlistHash?: string;
|
|
85
|
+
validUntil?: ISODateString;
|
|
75
86
|
}
|
|
76
87
|
export interface MajikTSARequest {
|
|
77
88
|
digest: {
|
|
@@ -91,7 +102,7 @@ export interface MajikTimestamp {
|
|
|
91
102
|
*/
|
|
92
103
|
export interface ExpectedSigner {
|
|
93
104
|
/** MajikKey fingerprint (SHA-256 of X25519 public key, base64) */
|
|
94
|
-
signerId:
|
|
105
|
+
signerId: MajikKeyFingerprint;
|
|
95
106
|
/** Ed25519 public key, base64 (32 bytes) */
|
|
96
107
|
edPublicKey: string;
|
|
97
108
|
/** ML-DSA-87 public key, base64 (2592 bytes) */
|
|
@@ -123,7 +134,7 @@ export interface MultiSigEnvelope {
|
|
|
123
134
|
* their signature verification.
|
|
124
135
|
* Absent when allowlist is absent.
|
|
125
136
|
*/
|
|
126
|
-
allowlistSignerId?:
|
|
137
|
+
allowlistSignerId?: MajikKeyFingerprint;
|
|
127
138
|
/** All per-signer envelopes. One entry per signer, keyed logically by signerId. */
|
|
128
139
|
signatures: MajikSignatureJSON[];
|
|
129
140
|
/**
|
|
@@ -137,7 +148,7 @@ export interface MultiSigEnvelope {
|
|
|
137
148
|
* ISO 8601 timestamp of when the seal was applied.
|
|
138
149
|
* Included in the seal hash input — changing this breaks the seal.
|
|
139
150
|
*/
|
|
140
|
-
sealTimestamp?:
|
|
151
|
+
sealTimestamp?: ISODateString;
|
|
141
152
|
/**
|
|
142
153
|
* Fingerprint of the signer who applied the seal.
|
|
143
154
|
* Must equal allowlistSignerId — only the issuer can seal.
|
|
@@ -151,7 +162,7 @@ export interface MultiSigEnvelope {
|
|
|
151
162
|
*/
|
|
152
163
|
export interface MajikSignerPublicKeys {
|
|
153
164
|
/** MajikKey fingerprint */
|
|
154
|
-
signerId:
|
|
165
|
+
signerId: MajikKeyFingerprint;
|
|
155
166
|
/** Ed25519 public key bytes (32 bytes) */
|
|
156
167
|
edPublicKey: Uint8Array;
|
|
157
168
|
/** ML-DSA-87 public key bytes (2592 bytes) */
|
|
@@ -164,7 +175,7 @@ export interface SignOptions {
|
|
|
164
175
|
/** Advisory content type label */
|
|
165
176
|
contentType?: string;
|
|
166
177
|
/** Override timestamp (useful for deterministic tests) */
|
|
167
|
-
timestamp?:
|
|
178
|
+
timestamp?: ISODateString;
|
|
168
179
|
/**
|
|
169
180
|
* Restrict future signers to these keys only.
|
|
170
181
|
* Only honoured when this is the first signature on a file (no existing
|
|
@@ -173,20 +184,28 @@ export interface SignOptions {
|
|
|
173
184
|
* Each entry must include signerId + edPublicKey + mlDsaPublicKey (base64).
|
|
174
185
|
*/
|
|
175
186
|
expectedSigners?: ExpectedSigner[];
|
|
187
|
+
/**
|
|
188
|
+
* ISO 8601 timestamp. If set, verify() will fail with an "expired" reason
|
|
189
|
+
* once the current time passes this value. Optional — omit for a
|
|
190
|
+
* signature that never expires.
|
|
191
|
+
*/
|
|
192
|
+
validUntil?: string;
|
|
176
193
|
}
|
|
177
194
|
/**
|
|
178
195
|
* Result returned by MajikSignature.verify() and all file-level verify methods.
|
|
179
196
|
*/
|
|
180
197
|
export interface VerificationResult {
|
|
181
198
|
valid: boolean;
|
|
182
|
-
signerId?:
|
|
199
|
+
signerId?: MajikKeyFingerprint;
|
|
183
200
|
contentHash?: string;
|
|
184
|
-
timestamp:
|
|
201
|
+
timestamp: ISODateString;
|
|
185
202
|
contentType?: string;
|
|
186
203
|
/** Present when result came from a file verify — which handler processed it */
|
|
187
204
|
handler?: string;
|
|
188
205
|
/** Present when valid is false — human-readable failure reason */
|
|
189
206
|
reason?: string;
|
|
207
|
+
/** True only when the sole reason valid=false is expiry (crypto checked out fine). */
|
|
208
|
+
expired?: boolean;
|
|
190
209
|
}
|
|
191
210
|
/**
|
|
192
211
|
* Result returned by verifySeal().
|
|
@@ -195,7 +214,7 @@ export interface SealVerificationResult {
|
|
|
195
214
|
/** Whether the seal hash is valid and matches all current signatories */
|
|
196
215
|
valid: boolean;
|
|
197
216
|
/** Fingerprint of who sealed the envelope */
|
|
198
|
-
sealedBy?:
|
|
217
|
+
sealedBy?: MajikKeyFingerprint;
|
|
199
218
|
/** ISO 8601 timestamp of when the seal was applied */
|
|
200
219
|
sealTimestamp?: string;
|
|
201
220
|
/** Human-readable failure reason when valid is false */
|
|
@@ -209,9 +228,9 @@ export interface SealInfo {
|
|
|
209
228
|
/** SHA3-512 hash of the canonical seal payload, hex-encoded (128 chars) */
|
|
210
229
|
sealHash: string;
|
|
211
230
|
/** ISO 8601 timestamp of when the seal was applied */
|
|
212
|
-
sealTimestamp:
|
|
231
|
+
sealTimestamp: ISODateString;
|
|
213
232
|
/** Fingerprint of the issuer who applied the seal */
|
|
214
|
-
sealedBy:
|
|
233
|
+
sealedBy: MajikKeyFingerprint;
|
|
215
234
|
}
|
|
216
235
|
/**
|
|
217
236
|
* Full information about a single signatory.
|
|
@@ -219,7 +238,7 @@ export interface SealInfo {
|
|
|
219
238
|
*/
|
|
220
239
|
export interface SignatoryInfo {
|
|
221
240
|
/** MajikKey fingerprint */
|
|
222
|
-
signerId:
|
|
241
|
+
signerId: MajikKeyFingerprint;
|
|
223
242
|
/** Ed25519 public key, base64 */
|
|
224
243
|
edPublicKey: string;
|
|
225
244
|
/** ML-DSA-87 public key, base64 */
|
|
@@ -227,7 +246,7 @@ export interface SignatoryInfo {
|
|
|
227
246
|
/** Whether this signatory has already signed */
|
|
228
247
|
hasSigned: boolean;
|
|
229
248
|
/** ISO 8601 timestamp of their signature — present only when hasSigned is true */
|
|
230
|
-
signedAt?:
|
|
249
|
+
signedAt?: ISODateString;
|
|
231
250
|
}
|
|
232
251
|
/**
|
|
233
252
|
* Result returned by getSignatories() and its aliases.
|
|
@@ -357,8 +376,14 @@ export interface BatchFileInput {
|
|
|
357
376
|
}
|
|
358
377
|
export interface BatchSignOptions {
|
|
359
378
|
contentType?: string;
|
|
360
|
-
timestamp?:
|
|
379
|
+
timestamp?: ISODateString;
|
|
361
380
|
expectedSigners?: ExpectedSigner[];
|
|
381
|
+
/**
|
|
382
|
+
* ISO 8601 expiry applied to every signature in the batch. Optional —
|
|
383
|
+
* omit for signatures that never expire. Same field, same semantics as
|
|
384
|
+
* SignOptions.validUntil, just applied uniformly across the batch.
|
|
385
|
+
*/
|
|
386
|
+
validUntil?: ISODateString;
|
|
362
387
|
/** "map" (default) produces one MajikSignatureMap covering the whole
|
|
363
388
|
* batch. "separate" produces one .mjksig Blob per file. */
|
|
364
389
|
mode?: "map" | "separate";
|
|
@@ -405,7 +430,12 @@ export interface FileVerifyResult {
|
|
|
405
430
|
relocatedFrom?: string;
|
|
406
431
|
}
|
|
407
432
|
export interface BatchVerifyOptions {
|
|
408
|
-
expectedSignerId?:
|
|
433
|
+
expectedSignerId?: MajikKeyFingerprint;
|
|
434
|
+
/**
|
|
435
|
+
* Time to check validUntil against. Defaults to the current time.
|
|
436
|
+
* Every per-file verification in the batch uses this same value.
|
|
437
|
+
*/
|
|
438
|
+
now?: Date;
|
|
409
439
|
/**
|
|
410
440
|
* If true, a file with status "not_in_map" is a hard error for the whole
|
|
411
441
|
* batch call (throws). Default false — missing files are reported per-file
|
package/dist/core/validator.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* validator.ts
|
|
3
3
|
* Input validation and assertion helpers for MajikSignature.
|
|
4
4
|
*/
|
|
5
|
+
import { ISODateString } from "@majikah/majik-key";
|
|
5
6
|
import type { ExpectedSigner, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSAPayload, MultiSigEnvelope } from "./types";
|
|
6
7
|
export declare class MajikSignatureValidator {
|
|
7
8
|
static assert(condition: unknown, message: string, field?: string): asserts condition;
|
|
@@ -22,6 +23,7 @@ export declare class MajikSignatureValidator {
|
|
|
22
23
|
*/
|
|
23
24
|
static validateSeal(env: MultiSigEnvelope): void;
|
|
24
25
|
static validateSealHash(hash: string): void;
|
|
26
|
+
static validateValidUntil(value: ISODateString | undefined): void;
|
|
25
27
|
static validateMultiSigEnvelope(env: unknown): asserts env is MultiSigEnvelope;
|
|
26
28
|
static validateMajikTSAPayload(payload: unknown): asserts payload is MajikTSAPayload;
|
|
27
29
|
static validateMajikTimestamp(tsa: unknown): asserts tsa is MajikTimestamp;
|
package/dist/core/validator.js
CHANGED
|
@@ -153,6 +153,13 @@ export class MajikSignatureValidator {
|
|
|
153
153
|
if (typeof hash !== "string" || hash.length !== SEAL_HASH_HEX_LEN)
|
|
154
154
|
throw new MajikSignatureValidationError(`sealHash must be exactly ${SEAL_HASH_HEX_LEN} hex chars (SHA3-512)`, "sealHash");
|
|
155
155
|
}
|
|
156
|
+
static validateValidUntil(value) {
|
|
157
|
+
if (value === undefined)
|
|
158
|
+
return;
|
|
159
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
160
|
+
throw new MajikSignatureValidationError("validUntil must be a valid ISO 8601 timestamp string", "validUntil");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
156
163
|
// ── MultiSigEnvelope validator ───────────────────────────────────────────────
|
|
157
164
|
static validateMultiSigEnvelope(env) {
|
|
158
165
|
if (typeof env !== "object" || env === null)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* MajikSignature — hybrid Ed25519 + ML-DSA-87 content signing and verification.
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
|
-
import type { MajikKey } from "@majikah/majik-key";
|
|
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";
|
|
6
|
+
import type { ISODateString, MajikKey, MajikKeyFingerprint } from "@majikah/majik-key";
|
|
7
|
+
import type { BatchFileInput, BatchSignOptions, BatchVerifyInput, BatchVerifyOptions, ED25519Signature, EnvelopeInfo, ExpectedSigner, FileVerifyResult, MajikSignatureCompactJSON, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, MLDSA87Signature, 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";
|
|
@@ -42,10 +42,21 @@ import { SignatureOrderResult } from "./core/order";
|
|
|
42
42
|
* - Sealing computes a SHA3-512 hash over all current signatories + timestamp
|
|
43
43
|
* - A sealed envelope rejects all further signing attempts
|
|
44
44
|
*
|
|
45
|
+
* Expiry:
|
|
46
|
+
* - A signer may optionally set validUntil (ISO 8601) when signing
|
|
47
|
+
* - Cryptographically committed to via the canonical payload — the field
|
|
48
|
+
* cannot be added, stripped, or extended post-hoc without breaking both
|
|
49
|
+
* Ed25519 and ML-DSA-87
|
|
50
|
+
* - verify() checks expiry only AFTER both signatures pass, so an
|
|
51
|
+
* expired-but-forged signature is reported as invalid, not "expired"
|
|
52
|
+
* - Absent = never expires, matching every signature that predates this
|
|
53
|
+
* feature — fully backward compatible, no migration needed
|
|
54
|
+
*
|
|
45
55
|
* Canonical payload:
|
|
46
|
-
* "majik-signature-v1:" + JSON({ v, id, ts, ct, hash[, alh] })
|
|
47
|
-
* where hash = SHA-256(content), alh = SHA-256(canonical allowlist)
|
|
48
|
-
*
|
|
56
|
+
* "majik-signature-v1:" + JSON({ v, id, ts, ct, hash[, alh][, vu] })
|
|
57
|
+
* where hash = SHA-256(content), alh = SHA-256(canonical allowlist), and
|
|
58
|
+
* vu = validUntil — each omitted (not nulled) when unset, preserving
|
|
59
|
+
* backward compat with signatures made before that field existed.
|
|
49
60
|
*/
|
|
50
61
|
export declare class MajikSignature {
|
|
51
62
|
private readonly _version;
|
|
@@ -58,30 +69,50 @@ export declare class MajikSignature {
|
|
|
58
69
|
private readonly _edSignature;
|
|
59
70
|
private readonly _mlDsaSignature;
|
|
60
71
|
private readonly _allowlistHash?;
|
|
72
|
+
private readonly _validUntil?;
|
|
61
73
|
private _tsa?;
|
|
62
74
|
private constructor();
|
|
63
75
|
get version(): 1;
|
|
64
|
-
get signerId():
|
|
76
|
+
get signerId(): MajikKeyFingerprint;
|
|
65
77
|
get signerEdPublicKey(): string;
|
|
66
78
|
get signerMlDsaPublicKey(): string;
|
|
67
79
|
get contentHash(): string;
|
|
68
80
|
get contentType(): string | undefined;
|
|
69
|
-
get timestamp():
|
|
70
|
-
get edSignature():
|
|
71
|
-
get mlDsaSignature():
|
|
81
|
+
get timestamp(): ISODateString;
|
|
82
|
+
get edSignature(): ED25519Signature;
|
|
83
|
+
get mlDsaSignature(): MLDSA87Signature;
|
|
72
84
|
/**
|
|
73
85
|
* SHA-256 hash of the canonical allowlist, base64.
|
|
74
86
|
* Present only on the envelope of the signer who established the allowlist.
|
|
75
87
|
* Undefined on all other signers and on pre-allowlist signatures.
|
|
76
88
|
*/
|
|
77
89
|
get allowlistHash(): string | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* ISO 8601 timestamp after which this signature is considered expired by
|
|
92
|
+
* verify(). Undefined means the signature never expires — this is the case
|
|
93
|
+
* for every signature created before expiry support existed.
|
|
94
|
+
*/
|
|
95
|
+
get validUntil(): ISODateString | undefined;
|
|
78
96
|
get tsa(): MajikTimestamp | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Structural expiry check against this signature's validUntil — does NOT
|
|
99
|
+
* verify the cryptographic signature. Always false when validUntil is unset.
|
|
100
|
+
* Use verify() for a full trust decision; use this for a quick UI-facing
|
|
101
|
+
* "is this stale" check without needing the original content or public keys.
|
|
102
|
+
*
|
|
103
|
+
* @param now - Time to check against. Defaults to the current time.
|
|
104
|
+
*/
|
|
105
|
+
isExpired(now?: Date): boolean;
|
|
79
106
|
/**
|
|
80
107
|
* Sign content with an unlocked MajikKey.
|
|
81
108
|
*
|
|
82
109
|
* Both Ed25519 and ML-DSA-87 sign the same canonical payload.
|
|
83
110
|
* When options.allowlistHash is provided (injected internally by signAndEmbed),
|
|
84
111
|
* it is included in the payload so both algorithms cover the allowlist.
|
|
112
|
+
* When options.validUntil is provided, it is likewise included in the payload
|
|
113
|
+
* so both algorithms cover the expiry — a verifier cannot strip or extend it
|
|
114
|
+
* without invalidating the signature. Omit it for a signature that never
|
|
115
|
+
* expires.
|
|
85
116
|
*/
|
|
86
117
|
static sign(content: Uint8Array | string, key: MajikKey, options?: SignOptions & {
|
|
87
118
|
allowlistHash?: string;
|
|
@@ -89,8 +120,20 @@ export declare class MajikSignature {
|
|
|
89
120
|
/**
|
|
90
121
|
* Verify a MajikSignature against content and the signer's public keys.
|
|
91
122
|
* Both Ed25519 AND ML-DSA-87 must verify — if either fails, returns invalid.
|
|
123
|
+
*
|
|
124
|
+
* If the envelope carries a validUntil, expiry is checked only after both
|
|
125
|
+
* signatures have already passed — so a forged-but-expired signature is
|
|
126
|
+
* reported as an invalid signature (wrong reason), never mistaken for a
|
|
127
|
+
* merely-expired-but-otherwise-valid one. A signature with no validUntil
|
|
128
|
+
* never expires.
|
|
129
|
+
*
|
|
130
|
+
* @param now - Time to check expiry against. Defaults to the current time;
|
|
131
|
+
* override for deterministic tests or to verify "as of" a past/future moment.
|
|
132
|
+
* @returns valid: false with reason "Signature expired at ..." and
|
|
133
|
+
* expired: true when the only failure is expiry; expired is omitted
|
|
134
|
+
* otherwise.
|
|
92
135
|
*/
|
|
93
|
-
static verify(content: Uint8Array | string, signature: MajikSignature | MajikSignatureJSON, publicKeys: MajikSignerPublicKeys): VerificationResult;
|
|
136
|
+
static verify(content: Uint8Array | string, signature: MajikSignature | MajikSignatureJSON, publicKeys: MajikSignerPublicKeys, now?: Date): VerificationResult;
|
|
94
137
|
validate(): void;
|
|
95
138
|
isValid(): boolean;
|
|
96
139
|
extractPublicKeys(): MajikSignerPublicKeys;
|
|
@@ -121,12 +164,19 @@ export declare class MajikSignature {
|
|
|
121
164
|
* Non-allowlisted signers are rejected before any crypto (MajikSignatureAllowlistError).
|
|
122
165
|
* Sealed files are always rejected.
|
|
123
166
|
*
|
|
167
|
+
* Pass options.validUntil (ISO 8601) to make this specific signature expire —
|
|
168
|
+
* cryptographically bound into the payload, so it cannot be stripped or
|
|
169
|
+
* extended without invalidating the signature. Omit for a signature that
|
|
170
|
+
* never expires. In multi-sig files, expiry is per-signer: each signer's
|
|
171
|
+
* validUntil (or absence of one) applies only to their own entry.
|
|
172
|
+
*
|
|
124
173
|
* @example
|
|
125
174
|
* const { blob } = await MajikSignature.signFile(file, aliceKey, {
|
|
126
175
|
* expectedSigners: [
|
|
127
176
|
* MajikSignature.expectedSignerFromKey(aliceKey),
|
|
128
177
|
* MajikSignature.expectedSignerFromKey(bobKey),
|
|
129
178
|
* ],
|
|
179
|
+
* validUntil: "2027-01-01T00:00:00.000Z",
|
|
130
180
|
* });
|
|
131
181
|
*/
|
|
132
182
|
static signFile(file: Blob, key: MajikKey, options?: {
|
|
@@ -134,6 +184,7 @@ export declare class MajikSignature {
|
|
|
134
184
|
timestamp?: string;
|
|
135
185
|
mimeType?: string;
|
|
136
186
|
expectedSigners?: ExpectedSigner[];
|
|
187
|
+
validUntil?: string;
|
|
137
188
|
}): ReturnType<typeof MajikSignatureEmbed.signAndEmbed<MajikSignature>>;
|
|
138
189
|
/**
|
|
139
190
|
* Sign a file and return the signature envelope detached.
|
|
@@ -146,10 +197,15 @@ export declare class MajikSignature {
|
|
|
146
197
|
* it's added to the envelope — the digest-match and TSA-signature checks
|
|
147
198
|
* happen automatically inside addTSA().
|
|
148
199
|
*
|
|
200
|
+
* Pass options.validUntil (ISO 8601) to make this signature expire — see
|
|
201
|
+
* signFile() for the same semantics; identical here since both funnel
|
|
202
|
+
* through the same underlying sign() call.
|
|
203
|
+
*
|
|
149
204
|
* @example
|
|
150
205
|
* const { blob, envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
|
|
151
|
-
* existingEnvelope: outOfBandEnvelope,
|
|
152
|
-
* tsa: myTsaTimestamp,
|
|
206
|
+
* existingEnvelope: outOfBandEnvelope,
|
|
207
|
+
* tsa: myTsaTimestamp,
|
|
208
|
+
* validUntil: "2027-01-01T00:00:00.000Z",
|
|
153
209
|
* });
|
|
154
210
|
* console.log(signature.hasTSA); // true if tsa was provided and accepted
|
|
155
211
|
*/
|
|
@@ -158,6 +214,7 @@ export declare class MajikSignature {
|
|
|
158
214
|
timestamp?: string;
|
|
159
215
|
mimeType?: string;
|
|
160
216
|
expectedSigners?: ExpectedSigner[];
|
|
217
|
+
validUntil?: string;
|
|
161
218
|
existingEnvelope?: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob;
|
|
162
219
|
tsa?: MajikTimestamp;
|
|
163
220
|
}): ReturnType<typeof MajikSignatureEmbed.signDetached<MajikSignature>>;
|
|
@@ -167,6 +224,10 @@ export declare class MajikSignature {
|
|
|
167
224
|
* batch (default — meant to sit at the root of the zip as one .mjksmap),
|
|
168
225
|
* or as separate .mjksig Blobs per file when options.mode === "separate".
|
|
169
226
|
*
|
|
227
|
+
* options.validUntil, if set, is applied identically to every signature in
|
|
228
|
+
* the batch — there is no per-file override. Sign files individually via
|
|
229
|
+
* signFileDetached() if different files need different expiries.
|
|
230
|
+
*
|
|
170
231
|
* @example
|
|
171
232
|
* const result = await MajikSignature.signBatchDetached(
|
|
172
233
|
* [
|
|
@@ -174,6 +235,7 @@ export declare class MajikSignature {
|
|
|
174
235
|
* { path: "docs/appendix.pdf", blob: appendixBlob },
|
|
175
236
|
* ],
|
|
176
237
|
* aliceKey,
|
|
238
|
+
* { validUntil: "2027-01-01T00:00:00.000Z" },
|
|
177
239
|
* );
|
|
178
240
|
* if (result.mode === "map") {
|
|
179
241
|
* zip.file("signatures.mjksmap", await result.mapBlob.arrayBuffer());
|
|
@@ -184,20 +246,27 @@ export declare class MajikSignature {
|
|
|
184
246
|
* Verify a file's embedded signatures.
|
|
185
247
|
* Returns one VerificationResult per signer. Old single-sig files return a single-item array.
|
|
186
248
|
* Pass options.expectedSignerId to verify only a specific signer.
|
|
249
|
+
* Pass options.now to check expiry as of a specific time instead of the
|
|
250
|
+
* current time. Any signer whose validUntil has passed comes back with
|
|
251
|
+
* valid: false, expired: true, and a reason noting the expiry.
|
|
187
252
|
*/
|
|
188
253
|
static verifyFile(file: Blob, keyOrPublicKeys: MajikKey | MajikSignerPublicKeys, options?: {
|
|
189
254
|
expectedSignerId?: string;
|
|
190
255
|
mimeType?: string;
|
|
256
|
+
now?: Date;
|
|
191
257
|
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
192
258
|
/**
|
|
193
259
|
* Verify a file against a detached signature envelope.
|
|
194
260
|
* Skips extraction and verifies the stripped file bytes directly against the provided envelope.
|
|
195
261
|
* Returns one VerificationResult per signer.
|
|
196
262
|
* Pass options.expectedSignerId to verify only a specific signer.
|
|
263
|
+
* Pass options.now to check expiry as of a specific time instead of the
|
|
264
|
+
* current time.
|
|
197
265
|
*/
|
|
198
266
|
static verifyFileDetached(file: Blob, envelope: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, keyOrPublicKeys: MajikKey | MajikSignerPublicKeys, options?: {
|
|
199
267
|
expectedSignerId?: string;
|
|
200
268
|
mimeType?: string;
|
|
269
|
+
now?: Date;
|
|
201
270
|
}, debug?: boolean): Promise<VerificationResult[]>;
|
|
202
271
|
/**
|
|
203
272
|
* Verify a batch of extracted files against a MajikSignatureMap (loaded via
|
|
@@ -205,6 +274,11 @@ export declare class MajikSignature {
|
|
|
205
274
|
* throwing — a missing, tampered, or invalidly-signed file is a normal
|
|
206
275
|
* possible outcome to display, not an exceptional one to catch.
|
|
207
276
|
*
|
|
277
|
+
* A file whose signature has passed its validUntil comes back with
|
|
278
|
+
* status "invalid" and results[].expired === true — same status as any
|
|
279
|
+
* other failed signature, so summarizeBatchVerification()'s allValid check
|
|
280
|
+
* still works unchanged. Pass options.now to check "as of" a specific time.
|
|
281
|
+
*
|
|
208
282
|
* @example
|
|
209
283
|
* const map = await MajikSignatureMap.fromMJKSMAP(mjksmapBlob);
|
|
210
284
|
* const results = await MajikSignature.verifyFilesFromMjksMap(
|
|
@@ -505,12 +579,21 @@ export declare class MajikSignature {
|
|
|
505
579
|
* The verifier must supply the signer's public keys out-of-band via
|
|
506
580
|
* fromCompact() / verifyCompact() — never trust keys recovered from the
|
|
507
581
|
* compact payload itself, because there are none.
|
|
582
|
+
*
|
|
583
|
+
* allowlistHash and validUntil, when present, carry over unchanged — both
|
|
584
|
+
* are already part of what was signed, so compacting doesn't affect their
|
|
585
|
+
* enforcement.
|
|
508
586
|
*/
|
|
509
587
|
toCompact(): MajikSignatureCompactJSON;
|
|
510
588
|
/**
|
|
511
589
|
* Rehydrate a full MajikSignature from a compact payload + externally
|
|
512
590
|
* resolved public keys. Throws if signerId doesn't match the supplied keys —
|
|
513
591
|
* this is a cheap sanity check, not a substitute for verify().
|
|
592
|
+
*
|
|
593
|
+
* allowlistHash and validUntil are carried through unchanged from the
|
|
594
|
+
* compact payload; verify() will still enforce them via the recomputed
|
|
595
|
+
* canonical payload, so an out-of-band edit to either field here would
|
|
596
|
+
* simply fail verification rather than being silently trusted.
|
|
514
597
|
*/
|
|
515
598
|
static fromCompact(compact: MajikSignatureCompactJSON, publicKeys: Pick<MajikSignerPublicKeys, "edPublicKey" | "mlDsaPublicKey">): MajikSignature;
|
|
516
599
|
/**
|
|
@@ -518,6 +601,11 @@ export declare class MajikSignature {
|
|
|
518
601
|
* publicKeys.signerId before any crypto runs, so a mismatched lookup fails
|
|
519
602
|
* fast with a clear reason instead of a cryptic signature failure.
|
|
520
603
|
*
|
|
604
|
+
* Delegates to verify() after rehydration, so allowlistHash and validUntil
|
|
605
|
+
* (when present in the compact payload) are enforced identically to the
|
|
606
|
+
* full-envelope path — including the "expired" reason/flag on a signature
|
|
607
|
+
* past its validUntil.
|
|
608
|
+
*
|
|
521
609
|
* @example
|
|
522
610
|
* const keys = await resolvePublicKeysForMuid(slink.muid); // your registry
|
|
523
611
|
* const result = MajikSignature.verifyCompact(canonical, slink.signatureJSON, keys);
|
package/dist/majik-signature.js
CHANGED
|
@@ -46,10 +46,21 @@ const secureFill = Uint8Array.prototype.fill;
|
|
|
46
46
|
* - Sealing computes a SHA3-512 hash over all current signatories + timestamp
|
|
47
47
|
* - A sealed envelope rejects all further signing attempts
|
|
48
48
|
*
|
|
49
|
+
* Expiry:
|
|
50
|
+
* - A signer may optionally set validUntil (ISO 8601) when signing
|
|
51
|
+
* - Cryptographically committed to via the canonical payload — the field
|
|
52
|
+
* cannot be added, stripped, or extended post-hoc without breaking both
|
|
53
|
+
* Ed25519 and ML-DSA-87
|
|
54
|
+
* - verify() checks expiry only AFTER both signatures pass, so an
|
|
55
|
+
* expired-but-forged signature is reported as invalid, not "expired"
|
|
56
|
+
* - Absent = never expires, matching every signature that predates this
|
|
57
|
+
* feature — fully backward compatible, no migration needed
|
|
58
|
+
*
|
|
49
59
|
* Canonical payload:
|
|
50
|
-
* "majik-signature-v1:" + JSON({ v, id, ts, ct, hash[, alh] })
|
|
51
|
-
* where hash = SHA-256(content), alh = SHA-256(canonical allowlist)
|
|
52
|
-
*
|
|
60
|
+
* "majik-signature-v1:" + JSON({ v, id, ts, ct, hash[, alh][, vu] })
|
|
61
|
+
* where hash = SHA-256(content), alh = SHA-256(canonical allowlist), and
|
|
62
|
+
* vu = validUntil — each omitted (not nulled) when unset, preserving
|
|
63
|
+
* backward compat with signatures made before that field existed.
|
|
53
64
|
*/
|
|
54
65
|
export class MajikSignature {
|
|
55
66
|
_version;
|
|
@@ -62,6 +73,7 @@ export class MajikSignature {
|
|
|
62
73
|
_edSignature;
|
|
63
74
|
_mlDsaSignature;
|
|
64
75
|
_allowlistHash;
|
|
76
|
+
_validUntil;
|
|
65
77
|
_tsa;
|
|
66
78
|
constructor(data) {
|
|
67
79
|
this._version = data.version;
|
|
@@ -74,6 +86,7 @@ export class MajikSignature {
|
|
|
74
86
|
this._edSignature = data.edSignature;
|
|
75
87
|
this._mlDsaSignature = data.mlDsaSignature;
|
|
76
88
|
this._allowlistHash = data.allowlistHash;
|
|
89
|
+
this._validUntil = data.validUntil;
|
|
77
90
|
this._tsa = data.tsa;
|
|
78
91
|
}
|
|
79
92
|
// ── Getters ─────────────────────────────────────────────────────────────────
|
|
@@ -112,9 +125,29 @@ export class MajikSignature {
|
|
|
112
125
|
get allowlistHash() {
|
|
113
126
|
return this._allowlistHash;
|
|
114
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* ISO 8601 timestamp after which this signature is considered expired by
|
|
130
|
+
* verify(). Undefined means the signature never expires — this is the case
|
|
131
|
+
* for every signature created before expiry support existed.
|
|
132
|
+
*/
|
|
133
|
+
get validUntil() {
|
|
134
|
+
return this._validUntil;
|
|
135
|
+
}
|
|
115
136
|
get tsa() {
|
|
116
137
|
return this._tsa;
|
|
117
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Structural expiry check against this signature's validUntil — does NOT
|
|
141
|
+
* verify the cryptographic signature. Always false when validUntil is unset.
|
|
142
|
+
* Use verify() for a full trust decision; use this for a quick UI-facing
|
|
143
|
+
* "is this stale" check without needing the original content or public keys.
|
|
144
|
+
*
|
|
145
|
+
* @param now - Time to check against. Defaults to the current time.
|
|
146
|
+
*/
|
|
147
|
+
isExpired(now = new Date()) {
|
|
148
|
+
return (this._validUntil !== undefined &&
|
|
149
|
+
now.getTime() > Date.parse(this._validUntil));
|
|
150
|
+
}
|
|
118
151
|
// ── SIGN ────────────────────────────────────────────────────────────────────
|
|
119
152
|
/**
|
|
120
153
|
* Sign content with an unlocked MajikKey.
|
|
@@ -122,11 +155,16 @@ export class MajikSignature {
|
|
|
122
155
|
* Both Ed25519 and ML-DSA-87 sign the same canonical payload.
|
|
123
156
|
* When options.allowlistHash is provided (injected internally by signAndEmbed),
|
|
124
157
|
* it is included in the payload so both algorithms cover the allowlist.
|
|
158
|
+
* When options.validUntil is provided, it is likewise included in the payload
|
|
159
|
+
* so both algorithms cover the expiry — a verifier cannot strip or extend it
|
|
160
|
+
* without invalidating the signature. Omit it for a signature that never
|
|
161
|
+
* expires.
|
|
125
162
|
*/
|
|
126
163
|
static async sign(content, key, options, debug = false) {
|
|
127
164
|
MajikSignatureValidator.validateContent(content);
|
|
128
165
|
MajikSignatureValidator.assertDefined(key, "key");
|
|
129
166
|
MajikSignatureValidator.validateContentType(options?.contentType);
|
|
167
|
+
MajikSignatureValidator.validateValidUntil(options?.validUntil);
|
|
130
168
|
if (key.isLocked)
|
|
131
169
|
throw new MajikSignatureKeyError("MajikKey is locked. Call unlock() before signing.");
|
|
132
170
|
if (!key.hasSigningKeys)
|
|
@@ -148,12 +186,14 @@ export class MajikSignature {
|
|
|
148
186
|
const signerId = key.fingerprint;
|
|
149
187
|
const contentType = options?.contentType;
|
|
150
188
|
const allowlistHash = options?.allowlistHash;
|
|
189
|
+
const validUntil = options?.validUntil; // NEW
|
|
151
190
|
const payload = buildSigningPayload({
|
|
152
191
|
signerId,
|
|
153
192
|
timestamp,
|
|
154
193
|
contentHash,
|
|
155
194
|
contentType,
|
|
156
195
|
allowlistHash,
|
|
196
|
+
validUntil,
|
|
157
197
|
});
|
|
158
198
|
if (debug)
|
|
159
199
|
console.log("Signing Payload:", payload);
|
|
@@ -175,6 +215,7 @@ export class MajikSignature {
|
|
|
175
215
|
edSignature: bytesToBase64(edSigBytes),
|
|
176
216
|
mlDsaSignature: bytesToBase64(mlDsaSigBytes),
|
|
177
217
|
...(allowlistHash !== undefined ? { allowlistHash } : {}),
|
|
218
|
+
...(validUntil !== undefined ? { validUntil } : {}), // NEW
|
|
178
219
|
};
|
|
179
220
|
return new MajikSignature(envelope);
|
|
180
221
|
}
|
|
@@ -201,8 +242,20 @@ export class MajikSignature {
|
|
|
201
242
|
/**
|
|
202
243
|
* Verify a MajikSignature against content and the signer's public keys.
|
|
203
244
|
* Both Ed25519 AND ML-DSA-87 must verify — if either fails, returns invalid.
|
|
245
|
+
*
|
|
246
|
+
* If the envelope carries a validUntil, expiry is checked only after both
|
|
247
|
+
* signatures have already passed — so a forged-but-expired signature is
|
|
248
|
+
* reported as an invalid signature (wrong reason), never mistaken for a
|
|
249
|
+
* merely-expired-but-otherwise-valid one. A signature with no validUntil
|
|
250
|
+
* never expires.
|
|
251
|
+
*
|
|
252
|
+
* @param now - Time to check expiry against. Defaults to the current time;
|
|
253
|
+
* override for deterministic tests or to verify "as of" a past/future moment.
|
|
254
|
+
* @returns valid: false with reason "Signature expired at ..." and
|
|
255
|
+
* expired: true when the only failure is expiry; expired is omitted
|
|
256
|
+
* otherwise.
|
|
204
257
|
*/
|
|
205
|
-
static verify(content, signature, publicKeys) {
|
|
258
|
+
static verify(content, signature, publicKeys, now = new Date()) {
|
|
206
259
|
try {
|
|
207
260
|
MajikSignatureValidator.validateContent(content);
|
|
208
261
|
MajikSignatureValidator.validateSignerPublicKeys(publicKeys);
|
|
@@ -226,6 +279,7 @@ export class MajikSignature {
|
|
|
226
279
|
contentHash: env.contentHash,
|
|
227
280
|
contentType: env.contentType,
|
|
228
281
|
allowlistHash: env.allowlistHash,
|
|
282
|
+
validUntil: env.validUntil,
|
|
229
283
|
});
|
|
230
284
|
let edOk;
|
|
231
285
|
try {
|
|
@@ -247,6 +301,14 @@ export class MajikSignature {
|
|
|
247
301
|
}
|
|
248
302
|
if (!mlDsaOk)
|
|
249
303
|
return invalid("Invalid ML-DSA-87 signature");
|
|
304
|
+
// NEW — expiry check, only after crypto has checked out
|
|
305
|
+
if (env.validUntil !== undefined &&
|
|
306
|
+
now.getTime() > Date.parse(env.validUntil)) {
|
|
307
|
+
return {
|
|
308
|
+
...invalid(`Signature expired at ${env.validUntil} (verified at ${now.toISOString()})`),
|
|
309
|
+
expired: true,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
250
312
|
return {
|
|
251
313
|
valid: true,
|
|
252
314
|
signerId: env.signerId,
|
|
@@ -304,6 +366,9 @@ export class MajikSignature {
|
|
|
304
366
|
...(this._allowlistHash !== undefined
|
|
305
367
|
? { allowlistHash: this._allowlistHash }
|
|
306
368
|
: {}),
|
|
369
|
+
...(this._validUntil !== undefined
|
|
370
|
+
? { validUntil: this._validUntil }
|
|
371
|
+
: {}),
|
|
307
372
|
...(this._tsa !== undefined ? { tsa: this._tsa } : {}),
|
|
308
373
|
};
|
|
309
374
|
}
|
|
@@ -394,12 +459,19 @@ export class MajikSignature {
|
|
|
394
459
|
* Non-allowlisted signers are rejected before any crypto (MajikSignatureAllowlistError).
|
|
395
460
|
* Sealed files are always rejected.
|
|
396
461
|
*
|
|
462
|
+
* Pass options.validUntil (ISO 8601) to make this specific signature expire —
|
|
463
|
+
* cryptographically bound into the payload, so it cannot be stripped or
|
|
464
|
+
* extended without invalidating the signature. Omit for a signature that
|
|
465
|
+
* never expires. In multi-sig files, expiry is per-signer: each signer's
|
|
466
|
+
* validUntil (or absence of one) applies only to their own entry.
|
|
467
|
+
*
|
|
397
468
|
* @example
|
|
398
469
|
* const { blob } = await MajikSignature.signFile(file, aliceKey, {
|
|
399
470
|
* expectedSigners: [
|
|
400
471
|
* MajikSignature.expectedSignerFromKey(aliceKey),
|
|
401
472
|
* MajikSignature.expectedSignerFromKey(bobKey),
|
|
402
473
|
* ],
|
|
474
|
+
* validUntil: "2027-01-01T00:00:00.000Z",
|
|
403
475
|
* });
|
|
404
476
|
*/
|
|
405
477
|
static async signFile(file, key, options) {
|
|
@@ -416,10 +488,15 @@ export class MajikSignature {
|
|
|
416
488
|
* it's added to the envelope — the digest-match and TSA-signature checks
|
|
417
489
|
* happen automatically inside addTSA().
|
|
418
490
|
*
|
|
491
|
+
* Pass options.validUntil (ISO 8601) to make this signature expire — see
|
|
492
|
+
* signFile() for the same semantics; identical here since both funnel
|
|
493
|
+
* through the same underlying sign() call.
|
|
494
|
+
*
|
|
419
495
|
* @example
|
|
420
496
|
* const { blob, envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
|
|
421
|
-
* existingEnvelope: outOfBandEnvelope,
|
|
422
|
-
* tsa: myTsaTimestamp,
|
|
497
|
+
* existingEnvelope: outOfBandEnvelope,
|
|
498
|
+
* tsa: myTsaTimestamp,
|
|
499
|
+
* validUntil: "2027-01-01T00:00:00.000Z",
|
|
423
500
|
* });
|
|
424
501
|
* console.log(signature.hasTSA); // true if tsa was provided and accepted
|
|
425
502
|
*/
|
|
@@ -432,6 +509,10 @@ export class MajikSignature {
|
|
|
432
509
|
* batch (default — meant to sit at the root of the zip as one .mjksmap),
|
|
433
510
|
* or as separate .mjksig Blobs per file when options.mode === "separate".
|
|
434
511
|
*
|
|
512
|
+
* options.validUntil, if set, is applied identically to every signature in
|
|
513
|
+
* the batch — there is no per-file override. Sign files individually via
|
|
514
|
+
* signFileDetached() if different files need different expiries.
|
|
515
|
+
*
|
|
435
516
|
* @example
|
|
436
517
|
* const result = await MajikSignature.signBatchDetached(
|
|
437
518
|
* [
|
|
@@ -439,6 +520,7 @@ export class MajikSignature {
|
|
|
439
520
|
* { path: "docs/appendix.pdf", blob: appendixBlob },
|
|
440
521
|
* ],
|
|
441
522
|
* aliceKey,
|
|
523
|
+
* { validUntil: "2027-01-01T00:00:00.000Z" },
|
|
442
524
|
* );
|
|
443
525
|
* if (result.mode === "map") {
|
|
444
526
|
* zip.file("signatures.mjksmap", await result.mapBlob.arrayBuffer());
|
|
@@ -451,6 +533,9 @@ export class MajikSignature {
|
|
|
451
533
|
* Verify a file's embedded signatures.
|
|
452
534
|
* Returns one VerificationResult per signer. Old single-sig files return a single-item array.
|
|
453
535
|
* Pass options.expectedSignerId to verify only a specific signer.
|
|
536
|
+
* Pass options.now to check expiry as of a specific time instead of the
|
|
537
|
+
* current time. Any signer whose validUntil has passed comes back with
|
|
538
|
+
* valid: false, expired: true, and a reason noting the expiry.
|
|
454
539
|
*/
|
|
455
540
|
static async verifyFile(file, keyOrPublicKeys, options, debug = false) {
|
|
456
541
|
if (MajikSignature._isMajikKey(keyOrPublicKeys)) {
|
|
@@ -463,6 +548,8 @@ export class MajikSignature {
|
|
|
463
548
|
* Skips extraction and verifies the stripped file bytes directly against the provided envelope.
|
|
464
549
|
* Returns one VerificationResult per signer.
|
|
465
550
|
* Pass options.expectedSignerId to verify only a specific signer.
|
|
551
|
+
* Pass options.now to check expiry as of a specific time instead of the
|
|
552
|
+
* current time.
|
|
466
553
|
*/
|
|
467
554
|
static async verifyFileDetached(file, envelope, keyOrPublicKeys, options, debug = false) {
|
|
468
555
|
if (MajikSignature._isMajikKey(keyOrPublicKeys)) {
|
|
@@ -476,6 +563,11 @@ export class MajikSignature {
|
|
|
476
563
|
* throwing — a missing, tampered, or invalidly-signed file is a normal
|
|
477
564
|
* possible outcome to display, not an exceptional one to catch.
|
|
478
565
|
*
|
|
566
|
+
* A file whose signature has passed its validUntil comes back with
|
|
567
|
+
* status "invalid" and results[].expired === true — same status as any
|
|
568
|
+
* other failed signature, so summarizeBatchVerification()'s allValid check
|
|
569
|
+
* still works unchanged. Pass options.now to check "as of" a specific time.
|
|
570
|
+
*
|
|
479
571
|
* @example
|
|
480
572
|
* const map = await MajikSignatureMap.fromMJKSMAP(mjksmapBlob);
|
|
481
573
|
* const results = await MajikSignature.verifyFilesFromMjksMap(
|
|
@@ -816,6 +908,10 @@ export class MajikSignature {
|
|
|
816
908
|
* The verifier must supply the signer's public keys out-of-band via
|
|
817
909
|
* fromCompact() / verifyCompact() — never trust keys recovered from the
|
|
818
910
|
* compact payload itself, because there are none.
|
|
911
|
+
*
|
|
912
|
+
* allowlistHash and validUntil, when present, carry over unchanged — both
|
|
913
|
+
* are already part of what was signed, so compacting doesn't affect their
|
|
914
|
+
* enforcement.
|
|
819
915
|
*/
|
|
820
916
|
toCompact() {
|
|
821
917
|
return {
|
|
@@ -827,12 +923,18 @@ export class MajikSignature {
|
|
|
827
923
|
edSignature: this._edSignature,
|
|
828
924
|
mlDsaSignature: this._mlDsaSignature,
|
|
829
925
|
allowlistHash: this._allowlistHash,
|
|
926
|
+
validUntil: this._validUntil,
|
|
830
927
|
};
|
|
831
928
|
}
|
|
832
929
|
/**
|
|
833
930
|
* Rehydrate a full MajikSignature from a compact payload + externally
|
|
834
931
|
* resolved public keys. Throws if signerId doesn't match the supplied keys —
|
|
835
932
|
* this is a cheap sanity check, not a substitute for verify().
|
|
933
|
+
*
|
|
934
|
+
* allowlistHash and validUntil are carried through unchanged from the
|
|
935
|
+
* compact payload; verify() will still enforce them via the recomputed
|
|
936
|
+
* canonical payload, so an out-of-band edit to either field here would
|
|
937
|
+
* simply fail verification rather than being silently trusted.
|
|
836
938
|
*/
|
|
837
939
|
static fromCompact(compact, publicKeys) {
|
|
838
940
|
if (!publicKeys?.edPublicKey || !publicKeys?.mlDsaPublicKey) {
|
|
@@ -849,6 +951,7 @@ export class MajikSignature {
|
|
|
849
951
|
edSignature: compact.edSignature,
|
|
850
952
|
mlDsaSignature: compact.mlDsaSignature,
|
|
851
953
|
allowlistHash: compact.allowlistHash,
|
|
954
|
+
validUntil: compact.validUntil,
|
|
852
955
|
};
|
|
853
956
|
return MajikSignature.fromJSON(full);
|
|
854
957
|
}
|
|
@@ -857,6 +960,11 @@ export class MajikSignature {
|
|
|
857
960
|
* publicKeys.signerId before any crypto runs, so a mismatched lookup fails
|
|
858
961
|
* fast with a clear reason instead of a cryptic signature failure.
|
|
859
962
|
*
|
|
963
|
+
* Delegates to verify() after rehydration, so allowlistHash and validUntil
|
|
964
|
+
* (when present in the compact payload) are enforced identically to the
|
|
965
|
+
* full-envelope path — including the "expired" reason/flag on a signature
|
|
966
|
+
* past its validUntil.
|
|
967
|
+
*
|
|
860
968
|
* @example
|
|
861
969
|
* const keys = await resolvePublicKeysForMuid(slink.muid); // your registry
|
|
862
970
|
* const result = MajikSignature.verifyCompact(canonical, slink.signatureJSON, keys);
|
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.9",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@majikah/majik-key": "^0.3.4",
|
|
55
|
-
"@noble/post-quantum": "^0.
|
|
55
|
+
"@noble/post-quantum": "^0.7.0",
|
|
56
56
|
"@stablelib/ed25519": "^2.1.0",
|
|
57
57
|
"@stablelib/sha256": "^2.0.1",
|
|
58
58
|
"fflate": "^0.8.3"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@types/node": "^26.
|
|
61
|
+
"@types/node": "^26.2.0",
|
|
62
62
|
"typescript": "^7.0.2",
|
|
63
63
|
"vitest": "^4.1.9"
|
|
64
64
|
}
|