SOURCE_URL: https://majikah.solutions/products/majik-signature/docs/how-it-works-documentation --- title: "How Majik Signature Works" id: "how-it-works-documentation" group: "majik-signature" type: "products" version: "0.10.0" lastUpdated: "2026-08-17T00:00:00.000Z" difficulty: "Advanced" time: "20-25 minutes" tags: [cryptography, ed25519, ml-dsa-87, fips-204, post-quantum, hybrid-signatures, sha-256, sha3-512, canonical-payload, key-derivation, argon2id, bip39, seed-phrase, technical, batch, zip, file-level-signing, multi-signature, allowlist, sealing, trusted-timestamps, tsa, windows-hello] --- # How Majik Signature Works > Technical deep-dive into Majik Signature's cryptographic architecture, key derivation, signing pipeline, multi-signature envelope format, sealing, trusted timestamps, batch processing model, and verification process ## Overview Deep dive into the cryptographic internals of Majik Signature. Learn how hybrid Ed25519 + ML-DSA-87 signatures are created, how the canonical payload is constructed, how keys are derived from your seed phrase, how multi-party signing allowlists and sealing work, how Trusted Timestamps are structured, how batch and ZIP signing pipelines work at the file level, and how verification proves authenticity. This guide is for developers, security researchers, and technically curious users who want to understand the system at a fundamental level. ### System Overview Majik Signature is a hybrid post-quantum digital signature system built on two independent cryptographic algorithms: Ed25519 (classical, elliptic curve) and ML-DSA-87 (post-quantum, lattice-based, NIST FIPS-204). Every signature is produced by both algorithms signing the same canonical payload, and verification requires both to pass. This defense-in-depth approach ensures that a break in one algorithm is not sufficient to forge a signature — an attacker must break both simultaneously, which is infeasible even for quantum computers. The system is built on Majik Key — a BIP-39 seed phrase identity that deterministically derives both Ed25519 and ML-DSA-87 signing keypairs, and that also owns the Argon2id-based encryption protecting those keys at rest. On top of core signing, the library implements multi-party signing (with cryptographically enforced allowlists), envelope sealing, and Trusted Timestamps (TSA) as first-class parts of the envelope format — not bolted-on extensions. All cryptographic operations happen locally using well-audited, dependency-free primitives, and the library is isomorphic — it runs identically in Node.js, browsers, Tauri, Deno, and Bun. In batch mode, this same signing and verification pipeline runs independently per file — each file carries its own self-contained signature envelope. ### Key Derivation from Seed Phrase Before you can sign anything, you need a Majik Key account. This is a BIP-39 mnemonic seed phrase — 12/24-word or 24-word, in any official BIP-39 language — that deterministically derives two signing keypairs. The derivation process is: (1) A mnemonic is generated using cryptographically secure random number generation (CSPRNG via crypto.getRandomValues) — 128 bits of entropy for a 12/24-word mnemonic, or 256 bits for a 24-word mnemonic. (2) The mnemonic is converted to a 64-byte seed via BIP-39 PBKDF2-SHA512 (2048 iterations, password = mnemonic words, salt = 'mnemonic') — this step produces a 64-byte seed regardless of whether the source mnemonic was 12 or 24 words. (3) Two keypairs are deterministically derived from this seed: Ed25519 keypair — seed[0..32] generates an Ed25519 keypair. This same keypair is also convertible to X25519 (Curve25519) for encryption use in Majik Message, but the Ed25519 form is what Majik Signature uses for signing. The public key is hashed (SHA-256) to create the account fingerprint. ML-DSA-87 keypair — the full seed[0..64] generates an ML-DSA-87 keypair (NIST FIPS-204, parameter set 87). (4) Both private keys are encrypted at rest with Argon2id (64 MB memory, 3 iterations, 4 threads) inside Majik Key, using either a passphrase you enter or — on the Windows Desktop App, if you opt in — a random high-entropy secret gated by Credential Store, and stored locally. The same mnemonic always produces the same keypairs, regardless of whether it's 12 or 24 words — this is how account recovery works. ### Key Derivation Breakdown | Step | Input | Process | Output | | --- | --- | --- | --- | | 1. Mnemonic generation | CSPRNG entropy (128 bits for 12/24-word, 256 bits for 24-word) | BIP-39 word list mapping | 12/24-word or 24-word mnemonic | | 2. Seed generation | 12/24-word or 24-word mnemonic | BIP-39 PBKDF2-SHA512 (2048 iterations) | 64-byte seed (same output size regardless of mnemonic length) | | 3. Ed25519 keypair | seed[0..32] | Ed25519 keypair generation from seed | Ed25519 public (32 bytes) + secret (64 bytes) | | 4. ML-DSA-87 keypair | seed[0..64] | ML-DSA-87 (FIPS-204) keypair generation from seed | ML-DSA-87 public (2592 bytes) + secret (4032 bytes) | | 5. Fingerprint | Ed25519 public key | SHA-256 hash → base64 encode | 44-char fingerprint string | | 6. Private key encryption | Ed25519 + ML-DSA-87 secret keys | Argon2id (64 MB, 3 iterations, 4 threads) + AES-256-GCM, inside Majik Key — keyed by a typed passphrase or, on Windows, an optional Credential Store–gated random secret | Encrypted local keystore | *Caption: How Ed25519 and ML-DSA-87 keys are derived from a 12/24-word or 24-word seed phrase* > **NOTE:** Deterministic key generation. The same mnemonic always produces the same Ed25519 and ML-DSA-87 keypairs, whether it's 12 or 24 words. This is how account recovery works — as long as you have your seed phrase, you can regenerate your signing keys on any device. Legacy Majik Key accounts created before signing support existed may lack signing keys entirely; these must be re-imported via mnemonic backup before they can sign (check key.hasSigningKeys). ### The Signing Pipeline (Step-by-Step) When you sign content or a file, the following pipeline executes entirely on your device: Step 0: Pre-signing checks — the signer key must be unlocked (private keys present in memory, whether unlocked via passphrase or Credential Store). For files, any existing embedded envelope is stripped first, so re-signing or re-embedding a file is always safe — signatures never stack or corrupt each other. Step 1: Content hashing — the content is hashed with SHA-256 to produce a 32-byte content hash. For files, this is the raw original file bytes before any signature embedding. For strings, this is the UTF-8 encoded bytes. Step 2: Canonical payload construction — a domain-separated canonical payload string is built (see below). Step 3: Ed25519 signature — the canonical payload is signed with the Ed25519 secret key → produces a 64-byte Ed25519 signature. Step 4: ML-DSA-87 signature — the same canonical payload is signed with the ML-DSA-87 secret key → produces a ~4595-byte ML-DSA-87 signature. Step 5: Envelope assembly — both signatures, the signer's public keys, the content hash, timestamp, content type, and (when applicable) an allowlist hash are assembled into a per-signer MajikSignatureJSON object. Step 6: Multi-sig wrapping — the per-signer envelope is added to (or starts) a MultiSigEnvelope, which is what actually gets embedded into a file or serialized. Step 7: Embedding — for files, the MultiSigEnvelope is embedded using a format-specific handler (native metadata where possible) or the universal binary trailer fallback. For raw content, the signature is instead serialized directly to JSON or base64 (detached signature) — content signing does not use the file-embedding step. ### The Canonical Payload — Domain Separation and Binding Both Ed25519 and ML-DSA-87 sign the exact same canonical payload string: "majik-signature-v1:" + JSON({ v, id, ts, ct, hash[, alh] }). This single string binds together everything that matters about the signature: Domain separation — the "majik-signature-v1:" prefix prevents cross-protocol signature reuse; a Majik Signature payload can never be mistaken for a payload from sealing ("majik-seal-v1:") or TSA ("majikah-tsa-v-1:"), since each uses a distinct domain-separated prefix. Signer binding — the signer fingerprint (id) is part of the signed data, so a valid signature can never be transferred to a different signer. Timestamp binding — the signing timestamp (ts) is part of the signed data, so it cannot be altered after signing. Content binding — the SHA-256 content hash (hash) is part of the signed data, so any byte change to the content invalidates both signatures. Type binding — the advisory content type (ct) is part of the signed data. Allowlist binding (alh) — present only on the signature that establishes a signing allowlist: the SHA-256 hash of the canonical allowlist is embedded in that signer's own payload, so tampering with the allowlist after the fact breaks the issuer's signature. Crucially, alh is omitted entirely — not set to null — on every signature that isn't establishing an allowlist. This is a deliberate backward-compatibility guarantee: every signature produced before multi-signature support existed still verifies today, because its payload bytes are completely unchanged. ### Canonical Payload Fields | Field | Type | Example | Purpose | | --- | --- | --- | --- | | v | integer | 1 | Envelope version — allows future format upgrades | | id | string (base64) | "dGhpcy4uLg==" | Signer fingerprint (SHA-256 of Ed25519 public key) | | ts | string (ISO 8601) | "2026-03-16T15:30:00.000Z" | Signing timestamp — cannot be altered after signing | | ct | string (optional) | "audio/wav" | Content type — advisory, prevents cross-type reuse | | hash | string (base64) | "YWJjZGVmMTIzNDU2..." | SHA-256 of content — tamper detection | | alh | string (base64, optional) | "c2hhMjU2b2ZhbGxvd2xpc3Q=" | SHA-256 of the canonical allowlist — present ONLY when this signer is establishing an allowlist; omitted (not null) otherwise | | vu | string (ISO 8601) | "2027-09-16T15:30:00.000Z" | Optional ISO 8601 expiry. When present, verify() treats the signature as invalid once the current time is past this value. Absent = never expires (matches all pre-existing signatures — fully backward compatible). Covered by the canonical signing payload when present, so it cannot be stripped or extended post-hoc without breaking both signatures. | *Caption: Breakdown of the JSON object embedded in the canonical payload* ### Canonical Payload Example ```javascript // Example canonical payload for a signed file (open signing, no allowlist) const payload = "majik-signature-v1:" + JSON.stringify({ v: 1, id: "dGhpc2lzYW5leGFtcGxlZmluZ2VycHJpbnQ=", // base64 fingerprint ts: "2026-03-16T15:30:00.000Z", // ISO 8601 timestamp ct: "audio/wav", // content type hash: "YWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTA=" // base64 SHA-256 // "alh" is omitted entirely here — this signer is not establishing an allowlist }); // This payload is signed independently by both Ed25519 and ML-DSA-87 ``` ### The Per-Signer Envelope (JSON Structure) After signing, the canonical payload, both signatures, and all metadata are assembled into a per-signer MajikSignatureJSON object. This object is self-contained — it includes everything needed to verify that one signer's signature without external data. Its fields are: version (integer, always 1). signerId (string, base64, 44 chars) — signer fingerprint. signerEdPublicKey (string, base64, ~44 chars) — signer's Ed25519 public key (32 bytes). signerMlDsaPublicKey (string, base64, ~3456 chars) — signer's ML-DSA-87 public key (2592 bytes). contentHash (string, base64, 44 chars) — SHA-256 of content (32 bytes). contentType (string, optional) — advisory MIME type. timestamp (string, ISO 8601). edSignature (string, base64, ~88 chars) — Ed25519 signature (64 bytes). mlDsaSignature (string, base64, ~6127 chars) — ML-DSA-87 signature (4595 bytes). allowlistHash (string, base64, optional) — present only on the issuer's signature. tsa (object, optional) — a full MajikTimestamp, present only if a Trusted Timestamp was attached. A single per-signer envelope serializes to approximately 10 KB of JSON (~14 KB base64) — most of which is the ML-DSA-87 signature and public key. This per-signer envelope is what a file's actual embedded structure wraps — see the Multi-Signature Envelope section below. ### Per-Signer Envelope Example (Truncated) ```json { "version": 1, "signerId": "dGhpc2lzYW5leGFtcGxlZmluZ2VycHJpbnQ=", "signerEdPublicKey": "YWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTA=", "signerMlDsaPublicKey": "dmVyeWxvbmdzdHJpbmdvZmJhc2U2NGVuY29kZWRieXRlcy4uLg==", "contentHash": "YWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTA=", "contentType": "audio/wav", "timestamp": "2026-03-16T15:30:00.000Z", "edSignature": "c2lnbmF0dXJlYnl0ZXNmcm9tZWQyNTUxOWFsZ29yaXRobQ==", "mlDsaSignature": "bG9uZ3Bvc3RxdWFudHVtc2lnbmF0dXJlYnl0ZXMuLi4=" // "allowlistHash" and "tsa" appear here only when applicable } ``` ### The Multi-Signature Envelope — What Actually Gets Embedded A file signed by Majik Signature never stores a single bare signature — it stores a MultiSigEnvelope: a wrapper containing an array of per-signer envelopes, plus optional allowlist and seal metadata. This is true even for a file with only one signer; the wrapper is always present so that adding a second signer later never requires restructuring the file. The wrapper's fields are: version — wrapper format version. signatures — an array of MajikSignatureJSON objects, one per signer, in the order they signed. allowlist (optional) — an array of ExpectedSigner entries ({ signerId, edPublicKey, mlDsaPublicKey }), present only for restricted-signing files. allowlistSignerId (optional) — the fingerprint of the issuer who established the allowlist. sealHash, sealTimestamp, sealedBy (all optional) — present only once the file has been sealed. Files signed before multi-signature support existed contain a bare MajikSignatureJSON object at the file's signature location instead of this wrapper. The library detects this legacy shape and transparently promotes it to MultiSigEnvelope semantics at read time — every public API always returns/accepts the wrapper shape, and old signatures continue to verify completely unmodified. ### MultiSigEnvelope Structure ```json { "version": 1, "signatures": [ /* MajikSignatureJSON[] — one per signer, in signing order */ ], "allowlist": [ /* ExpectedSigner[], present only for restricted signing */ ], "allowlistSignerId": "fingerprint-of-issuer", "sealHash": "128-hex-char-sha3-512-hash", "sealTimestamp": "2026-01-01T00:00:00.000Z", "sealedBy": "fingerprint-of-issuer" } ``` ### Signing Allowlists — Restricted Multi-Party Signing By default, any Majik Key holder can add a signature to an open-signing file. To restrict who may sign, the first signer (the issuer) supplies an expectedSigners list when signing — an array built from each intended signer's public keys via expectedSignerFromKey(), which does not require unlocking their key. This list becomes the file's allowlist, and its SHA-256 hash is embedded as allowlistHash in the issuer's own canonical payload — cryptographically committing the issuer to that exact allowlist. Every subsequent signing attempt is checked against the allowlist before any cryptographic operation runs: a signer must match all three fields — signerId, Ed25519 public key, and ML-DSA-87 public key — or the attempt is rejected with a typed MajikSignatureAllowlistError. Because the allowlist hash is inside the issuer's signed payload, modifying the allowlist after the fact — adding, removing, or swapping an expected signer — invalidates the issuer's signature the next time it's verified, even though the issuer never re-signs anything. A file only counts as a restricted multi-sig file when it has an allowlist naming more than one expected signer; a single-signer restricted file or an open-signing file with multiple signers does not meet this definition. ### Sealing — Finalizing a Multi-Signature Document Once a document has collected the signatures it needs, the issuer — and only the issuer — can seal it. Sealing computes a SHA3-512 hash over every current signatory's fingerprint plus a seal timestamp, domain-separated with the "majik-seal-v1:" prefix, and stores the result as sealHash, sealTimestamp, and sealedBy on the MultiSigEnvelope. This is a hash-based integrity lock, not a new Ed25519/ML-DSA-87 signature — it doesn't need to be, since its only job is to detect changes to the signatory set after finalization, and SHA3-512 is more than sufficient for that. A sealed envelope rejects all further signing attempts — including from the issuer themselves. Verifying a seal (verifySeal()) recomputes the SHA3-512 hash from the envelope's current signatory list and compares it to sealHash; this checks the structural integrity of the signatory set, but does not verify the individual Ed25519/ML-DSA-87 signatures — that still requires calling the file verification pipeline separately. ### Trusted Timestamps (TSA) — Structure and Verification A MajikSignature can optionally carry a MajikTimestamp — proof that an independent timestamp authority observed the signature at a specific time. The flow is: (1) The client builds a MajikTSARequest from an existing signature — essentially just { digest: { algorithm: "SHA-256", value } }, derived from the signature's own content hash. (2) This request is sent to a TSA server, which holds its own Majik Key. (3) The TSA server signs a canonical payload domain-separated with "majikah-tsa-v-1:", binding the digest, a server-generated nonce, and a server-authoritative timestamp — producing a full hybrid Ed25519 + ML-DSA-87 signature over that payload. Because the TSA signature is itself a complete Majik Signature, it inherits the exact same hybrid security guarantees as any other signature in the system. (4) The resulting MajikTimestamp is sent back to the client, which attaches it to the original signature via addTSA(). This attachment step validates that the TSA's signed digest actually matches the signature's content hash, and throws if a TSA is already present (a TSA, once attached, cannot be replaced) or if the TSA's own Ed25519/ML-DSA-87 signature fails to verify. After attachment, hasTSA reports presence and verifyTSA() can independently re-check the TSA's signature — useful after deserializing a signature from storage, since deserialization does not automatically re-verify anything. ### Domain Separation Prefixes | Prefix | Used By | Binds | | --- | --- | --- | | "majik-signature-v1:" | Core content/file signing | signer id, timestamp, content type, content hash, (optional) allowlist hash | | "majik-seal-v1:" | Envelope sealing | SHA3-512 hash of all current signatories + seal timestamp | | "majikah-tsa-v-1:" | Trusted Timestamps | content digest, server-generated nonce, server-authoritative timestamp | *Caption: Each protocol within Majik Signature uses a distinct signed-payload prefix, preventing any cross-protocol signature reuse* ### Batch Signing Pipeline — File-Level Architecture When you batch-sign a folder or ZIP archive, the signing pipeline runs independently per file — not on the folder or ZIP as a container. This is a distinct concept from multi-signature: batch signing means many files, each carrying its own (possibly single-signer) envelope; multi-signature means many signers on one file's envelope. The two can combine freely — a batch of files can each be routed through a multi-party signing and sealing workflow. The batch pipeline works as follows: Step 0: Input extraction — if the input is a ZIP, it is extracted in memory. Folders and nested ZIPs are recursively traversed. The result is a flat list of (filepath, bytes) pairs preserving relative paths. Step 1: Per-file signing — each file in the list is independently sent through the full signing pipeline. Each file receives its own unique MultiSigEnvelope containing its own content hash, timestamp, and signer information. Signatures across files in the same batch are independent — they are not linked to each other or to the container. Step 2: Result collection — per-file signing outcomes are collected: success (file + embedded signature), failure (file + error reason), or skipped. Progress is reported per file to the UI in real time. Step 3: Output repackaging — successfully signed files are repackaged into a new ZIP that preserves the original directory structure. The output ZIP is not itself signed — only the individual files within it carry signatures. Step 4: Sealing (optional) — individual files or all files can be sealed after signing to mark their signatory sets as finalized. > **NOTE:** Why file-level signing instead of container signing? Signing the ZIP container as a single binary would mean the signature covers the compressed bytes — not the original file content. Any change to compression settings, ZIP metadata, or file ordering would invalidate the signature even if the file content is unchanged. More importantly, container-level signing prevents individual file verification: you cannot extract a single file and verify it independently without the original ZIP. File-level signing gives each file a self-contained, independently verifiable signature — a much stronger security and usability guarantee. For Office formats (DOCX/XLSX/PPTX/ODT/ODS/ODP), which are themselves ZIP containers, the library performs deterministic ZIP re-canonicalization so that stripping always reproduces identical bytes regardless of when the file was last re-zipped. ### File Format Embedding Handlers Majik Signature auto-detects a file's format from magic bytes and MIME type, then tries a chain of format-specific handlers in order — the first one whose canHandle() matches wins. If no dedicated handler matches, the universal trailer fallback always applies, meaning every file type is signable, even ones with no dedicated handler. All handlers guarantee three things: the file remains fully usable after signing (a signed PDF still opens, a signed video still plays, a signed DOCX stays editable), signing is idempotent (any existing envelope is stripped before a new one is embedded, so re-signing never stacks or corrupts envelopes), and stripping always reproduces exactly the original bytes that were hashed and signed. ### Supported File Format Handlers | Format(s) | Handler | Embedding Mechanism | | --- | --- | --- | | PDF | PDF | Binary trailer appended after the last %%EOF marker (PDF 1.7 §7.5.6 compliant) | | PNG | PNG | iTXt metadata chunk | | WAV | WAV | RIFF LIST INFO chunk, custom ISIG sub-chunk | | MP3 | MP3 | ID3v2 TXXX frame | | MP4, MOV, M4A, M4V | MP4/MOV | moov/udta box, custom majk box type | | DOCX, XLSX, PPTX, ODT, ODS, ODP | Office | Dedicated majik-signature.json entry inside the ZIP container | | MKV, WebM | MKV | Custom Matroska metadata tag | | JPEG, FLAC | JPEG / FLAC | Native format metadata (non-destructive; same round-trip guarantees as other handlers) | | HTML, Markdown, JSON, plain text, source code | Text | Appended, format-appropriate metadata block | | Anything else | Fallback | Universal binary trailer: [original bytes][signature JSON][8-byte length][8-byte magic "MAJIKSIG"] | *Caption: Handlers are tried in order; the universal trailer is the always-available fallback* ### The Verification Pipeline (Step-by-Step) When you verify a signature, the following pipeline executes: Step 1: Signature extraction — for files, the MultiSigEnvelope is extracted using the matching format handler and parsed as JSON; legacy bare-envelope files are promoted transparently. For text, a base64 detached signature is deserialized. Step 2: Signature stripping — for files, the envelope is stripped to recover exactly the original bytes that were signed. For text, this step is skipped (text is already plaintext). Step 3: Content hashing — the original content is hashed with SHA-256. Step 4: Hash comparison — the computed hash is compared to the contentHash in each per-signer envelope. If they don't match for a given signer, that signer's verification fails immediately with a Content tampered reason — no need to check its signatures. Step 5: Canonical payload reconstruction — for each signer, the canonical payload is rebuilt from that signer's own metadata (including allowlistHash when present). Step 6: Ed25519 verification — each signer's Ed25519 signature is verified against the reconstructed payload using that signer's Ed25519 public key. Step 7: ML-DSA-87 verification — each signer's ML-DSA-87 signature is verified against the same payload using that signer's ML-DSA-87 public key. Step 8: Per-signer verdict — for each signer, if both algorithms pass and the hash matches, that signer's result is valid; otherwise it's invalid with a reason. Because a file can carry more than one signer, verifyFile() returns an array of results, one per signer — legacy single-signature files simply return a one-item array, so code that only reads results[0] keeps working. Step 9: TSA verification (optional) — if a signer's envelope carries a tsa object, its own Ed25519/ML-DSA-87 signature is independently checked against the TSA's public keys, and its digest is compared against that signer's content hash. ### Per-Signer Verification Verdict Logic | Hash Match? | Ed25519 Pass? | ML-DSA-87 Pass? | Signer Known? | Verdict | | --- | --- | --- | --- | --- | | ✓ | ✓ | ✓ | ✓ | Signature verified (green) | | ✓ | ✓ | ✓ | ✕ | Valid signature · unknown signer (amber) | | ✓ | ✕ | — | — | Verification failed (red) | | ✓ | — | ✕ | — | Verification failed (red) | | ✕ | — | — | — | Content tampered (red) | *Caption: How each signer's individual verdict is determined* > **NOTE:** Verifying a seal (structural check that the signatory set hasn't changed since sealing) and verifying signatures (cryptographic check of each signer's Ed25519/ML-DSA-87 signature) are two separate operations. A sealed file should generally have both checked: the seal confirms no signatory was added or removed after finalization, while per-signer verification confirms each individual signature is cryptographically valid. ### Error Handling Model Majik Signature distinguishes between two categories of "failure." A cryptographic signature that simply doesn't verify — wrong key, tampered content, expired trust — is an expected, handled outcome: verification functions return { valid: false, reason } rather than throwing. Exceptions are reserved for misuse: a locked key passed to a signing function, a malformed envelope, a non-allowlisted signer attempting to sign a restricted file, or an attempt to sign an already-sealed file. The library exposes a typed error hierarchy so calling code can catch precisely what it needs: MajikSignatureError (base class, also used for general/unexpected failures), MajikSignatureKeyError (locked key, missing signing keys, or wrong issuer), MajikSignatureVerificationError (verification fails unexpectedly, distinct from a normal valid: false result), MajikSignatureSerializationError (JSON/base64 parsing or encoding failure), and MajikSignatureAllowlistError (a non-listed signer attempts to sign a restricted file). ### Hybrid Security Model — Why Two Algorithms? Majik Signature uses a hybrid approach: every signature is produced by two independent algorithms, and verification requires both to pass. This provides defense-in-depth against cryptographic breaks. Classical security (Ed25519): Ed25519 provides 128-bit classical security. It is fast, compact (64-byte signatures), and battle-tested — used by SSH, TLS, and Signal. However, Ed25519 is vulnerable to quantum computers running Shor's Algorithm, which can break elliptic curve cryptography in polynomial time. Post-quantum security (ML-DSA-87): ML-DSA-87 (FIPS-204, Module-Lattice-Based Digital Signature Algorithm, parameter set 87) is a NIST-standardized post-quantum signature scheme based on lattice cryptography. It provides security against both classical and quantum computers. The underlying problem (Module Learning With Errors) is believed to be hard even for quantum computers. ML-DSA-87 provides NIST Category 5 security — equivalent to AES-256 against quantum attacks. Hybrid downgrade resistance: For a signature to be forged, an attacker must break both Ed25519 and ML-DSA-87 simultaneously. Breaking one is not sufficient — the other still protects the signature. This ensures your signatures remain secure even if one algorithm is unexpectedly broken. The same hybrid guarantee also applies to sealing indirectly (via the signatures being sealed) and directly to Trusted Timestamps, since a TSA token is itself a full hybrid Majik Signature. ### Ed25519 vs. ML-DSA-87 Comparison | Property | Ed25519 | ML-DSA-87 | | --- | --- | --- | | Type | Elliptic Curve (Curve25519) | Lattice-based (Module-LWE) | | Security level | 128-bit classical | Category 5 post-quantum (~AES-256 PQ) | | Quantum resistance | Vulnerable to Shor's Algorithm | Resistant to quantum attacks | | Public key size | 32 bytes | 2592 bytes | | Secret key size | 64 bytes | 4032 bytes | | Signature size | 64 bytes | 4595 bytes | | Sign speed | ~0.05 ms (very fast) | ~1 ms (fast) | | Verify speed | ~0.15 ms (very fast) | ~0.5 ms (fast) | | Standardization | RFC 8032 | NIST FIPS-204 | *Caption: Detailed comparison of the two signature algorithms* ### Argon2id Key Protection Your Ed25519 and ML-DSA-87 private keys are encrypted at rest using Argon2id — a memory-hard key derivation function resistant to GPU and ASIC cracking. This protection is owned by Majik Key, the shared identity layer underneath Majik Signature, Majik Universal ID, and Majik Message — Majik Signature consumes it rather than implementing its own key encryption. When you create an account, you set a passphrase (or, on the Windows Desktop App, optionally opt in to Credential Store instead — see below). This passphrase, or the Credential Store–guarded secret, is fed into Argon2id with the following parameters: Memory cost: 64 MB (65536 KB) — forces attackers to use significant RAM per guess, making parallel cracking expensive. Iterations: 3 — number of passes through memory. Parallelism: 4 threads — allows legitimate users to derive the key quickly on modern CPUs while limiting attacker parallelism. Output: 32-byte AES-256 key. The derived key is used to encrypt your private keys with AES-256-GCM before storing them locally. The ciphertext, IV, and salt are stored alongside the encrypted keys. When you unlock your account, you enter your passphrase (or authenticate with Credential Store), Argon2id re-derives the AES key, and your private keys are decrypted into memory. Locking the key afterward (key.lock()) purges the decrypted secret keys from memory. Why Argon2id? It's the winner of the Password Hashing Competition (PHC) and is designed to resist GPU/ASIC cracking — much stronger than PBKDF2 or bcrypt for passphrase-based key derivation. > **NOTE:** Credential Store passwordless unlock. On the Windows Desktop App, you can opt in to unlocking your Majik Key with Credential Store (face, fingerprint, or PIN) instead of typing a passphrase. Under the hood, this does not bypass Argon2id — instead, a cryptographically random 64-byte (128 hex character) secret is generated in place of a human-chosen passphrase and fed into the exact same Argon2id derivation described above. Credential Store simply gates access to that random secret, removing the risk of a weak, human-guessable passphrase while keeping the same memory-hard protection. This is additive and best-effort: if Credential Store setup or authentication fails for any reason, the account falls back to the normal passphrase flow and continues to work. ### Strong Passphrase Required > ⚠️ **WARNING:** If you're using a typed passphrase rather than Credential Store, it is the only defense protecting your Argon2id-encrypted private keys in local storage. If an attacker gains access to your device's local keystore and your passphrase is weak (e.g., 'password123'), they can brute-force the Argon2id derivation and recover your private keys. Use a strong, unique passphrase with at least 12 characters, mixed case, numbers, and symbols — or use a diceware passphrase with 6+ words. If available, opting in to Credential Store removes this risk entirely by replacing the human-chosen passphrase with a random 64-byte secret. ### Offline-First Design All core cryptographic operations in Majik Signature happen locally on your device. Signing: The signing pipeline (hashing, payload construction, Ed25519 signature, ML-DSA-87 signature, envelope assembly, embedding) runs entirely locally with no native bindings. No network calls are made — this includes batch operations, processing an entire folder of 500 files makes zero network calls. Verification: The verification pipeline runs entirely locally with no network calls, including batch verification of ZIP archives and seal verification. Key storage: Your private keys are stored locally — never transmitted to any server. Public keys: The signer's public keys are either embedded in the signature envelope (TOFU) or stored in your local contact directory — no server lookup required. This offline-first design means you can sign and verify files on air-gapped systems, in high-security environments, or when the internet is unavailable. The one exception: Trusted Timestamps require sending a small digest-only request to a TSA server and receiving back a signed token — this is the only network-dependent operation in the entire signing and verification pipeline. > **NOTE:** Majik Signature does not have a server component for core signing or verification. Everything happens locally on your device. The only server interactions are the static web app delivery from signature.majikah.solutions (after which the app works entirely offline) and, optionally, Trusted Timestamp requests to a TSA server. ### Security Assumptions and Threat Model Majik Signature's security relies on the following assumptions: Cryptographic hardness: Ed25519 and ML-DSA-87 are computationally hard to break with current and near-future computing power (including quantum computers for ML-DSA-87), and SHA3-512 is collision-resistant for sealing. Implementation correctness: The underlying cryptographic primitives correctly implement the algorithms and are free of critical bugs. Random number generation: The runtime's CSPRNG provides cryptographically secure random numbers for key generation, and — where Credential Store passwordless unlock is used — for the random secret it gates. Device security: Your device is not compromised while your account is unlocked — an attacker with access to your device's memory can extract decrypted private keys. Passphrase strength (when used): Your passphrase is strong enough to resist brute-force attacks against Argon2id, if you're not using Credential Store passwordless unlock. Seed phrase secrecy: Your seed phrase is kept secret — anyone with your seed phrase can regenerate your private keys and forge signatures in your name. Hash collision resistance: SHA-256 (content) and SHA3-512 (sealing) are collision-resistant — an attacker cannot find two different inputs producing the same hash. TSA trust: The library cryptographically verifies a TSA's signature, but deciding which TSA identity to trust in the first place is the application's responsibility. ### What Majik Signature Protects Against Content tampering: Any byte change to signed content invalidates that signer's signatures — cryptographically guaranteed. This applies equally to individual files and to files signed as part of a batch — each file's signature independently covers its own content. Signature forgery (classical): An attacker without a signer's Ed25519 private key cannot forge a valid Ed25519 signature — 128-bit classical security. Signature forgery (post-quantum): Even with a quantum computer, an attacker cannot forge a valid ML-DSA-87 signature — NIST Category 5 post-quantum security. Timestamp manipulation: The signing timestamp is part of the signed payload — cannot be altered without re-signing. Signer impersonation: A valid signature cannot be transferred to a different signer — the fingerprint is part of the signed payload. Replay attacks: Signatures are bound to specific content via the SHA-256 hash — a signature for one file cannot be reused for a different file. Downgrade attacks: Verification requires both Ed25519 and ML-DSA-87 to pass — an attacker cannot force verification to use only one algorithm. Allowlist tampering: Modifying a restricted file's allowlist invalidates the issuer's own signature. Post-seal tampering with the signatory set: Adding or removing a signer after sealing is detectable via the SHA3-512 seal hash, and no further signatures are accepted once sealed. ### What Majik Signature Does NOT Protect Against Device compromise: If malware or an attacker gains access to your unlocked device, they can extract your decrypted private keys from memory and forge signatures. Seed phrase theft: If someone steals your seed phrase, they can regenerate your private keys and forge signatures in your name. Weak passphrase (when used): If your passphrase is weak and an attacker steals your local keystore, they can brute-force Argon2id and recover your private keys — this risk does not apply if you've opted in to Credential Store passwordless unlock, since the underlying secret is randomly generated rather than human-chosen. Social engineering: Majik Signature cannot prevent you from signing malicious content or trusting an unknown signer. Local timestamp accuracy: Local (non-TSA) timestamps are self-reported by the signer — there is no independent authority backing them; use a Trusted Timestamp when independent proof of signing time matters. Real-world identity mapping: Majik Signature proves content was signed by a specific cryptographic key, not by a specific person or organization. Identity verification is your responsibility. Choice of trusted TSA: The library verifies a TSA cryptographically, but does not decide which TSA identity your application should trust. ### Security Guarantees Summary | Property | Guaranteed? | Notes | | --- | --- | --- | | Content integrity | ✓ Yes | Any byte change invalidates that signer's signatures | | Signer binding | ✓ Yes | Signature cannot be transferred to a different key | | Timestamp binding | ✓ Yes | Timestamp cannot be altered without re-signing | | Forgery resistance (classical) | ✓ Yes | Ed25519 provides 128-bit classical security | | Forgery resistance (post-quantum) | ✓ Yes | ML-DSA-87 provides Category 5 PQ security | | Downgrade resistance | ✓ Yes | Both algorithms must pass — no single-algorithm bypass | | Replay resistance | ✓ Yes | Signature bound to specific content via hash | | Per-file integrity in batch | ✓ Yes | Each file signed independently — granular verification | | Allowlist integrity | ✓ Yes | Tampering with the allowlist invalidates the issuer's signature | | Seal integrity | ✓ Yes | SHA3-512 hash detects any change to the signatory set post-seal | | TSA signature integrity | ✓ Yes | TSA token is itself a full hybrid Majik Signature | | Local timestamp accuracy | ✕ No | Self-reported — use TSA for independent proof | | Real-world identity | ✕ No | Proves key ownership, not person/organization | | Device security | ✕ No | Assumes device is not compromised while unlocked | | Seed phrase secrecy | ✕ No | User must protect their seed phrase (12/24-word or 24-word) | | Which TSA to trust | ✕ No | Application-level decision, not enforced by the library | *Caption: What Majik Signature guarantees and what it does not* ### Performance Characteristics Majik Signature is designed to be fast enough for interactive use on modern devices. Signing performance: For small files (Batch performance: Files in a batch are processed sequentially. For a batch of 100 small files, total signing time is approximately 100 × (ML-DSA-87 ~1 ms + SHA-256 hash time). ZIP extraction and repackaging adds a small overhead proportional to total file size. Multi-signer overhead: Each additional signer on a file adds one more ~10 KB per-signer envelope to the MultiSigEnvelope's signatures array — verification time scales linearly with signer count. Verification performance: Verification is slightly faster than signing because Ed25519 and ML-DSA-87 verification are faster than signing. For small files, verification completes in under 1 second. For large files, hashing again dominates. ### Future Cryptographic Agility Majik Signature's envelope format includes a version field on both the per-signer envelope and the MultiSigEnvelope wrapper to support future cryptographic upgrades. If Ed25519 or ML-DSA-87 is broken, or if newer post-quantum algorithms are standardized, a new envelope version can be introduced with different algorithms. Old signatures remain verifiable using their original algorithms — the same forward-compatible design that already lets pre-multi-signature files verify unmodified today. Possible future upgrades: Ed448 (higher classical security) or SPHINCS+ (hash-based post-quantum signatures) as alternatives to Ed25519. ML-DSA-65 or ML-DSA-44 (smaller signature sizes) for constrained environments. Falcon (NTRU-based post-quantum signatures) as an alternative to ML-DSA-87. SHA-3 (Keccak) as an alternative to SHA-256 for content hashing. The v1 envelope will always be supported for backward compatibility.